OAuth 2.1 Authorization Server implementation built to support the MCP Authorization Spec through @modelcontextprotocol/typescript-sdk.
Based on the MCP SDK's partial OAuth 2.1 Authorization Server implementation.
npm install mcp-oauth-server@latest --save-exact- MCP Authorization Spec Compliant: Fully compliant with the MCP Authorization Spec
- OAuth 2.1
- Dynamic Client Registration (RFC 7591)
- Authorization Server Metadata (RFC 8414)
- Protected Resource Metadata (RFC 9728)
- SDK Integration: Implements
OAuthServerProviderfrom @modelcontextprotocol/typescript-sdk - Compatibility: Supports MCP clients not fully compliant with the MCP Authorization Spec, such as clients that don't provide a
resourceindicator (RFC 8707) or any requested scopes - Flexible: Works with in-memory storage (for development) or custom storage backends (for production)
A complete working example of an MCP OAuth flow with a memory-backed OAuth 2.1 Authorization Server can be found in the ./example folder.
Run the demo:
-
Start the server:
pnpm example:server
-
In another terminal, authenticate with the server:
pnpm example:client
The example demonstrates:
- Setting up an OAuth server with in-memory storage in front of a MCP server
- Creating a consent screen
- Handling authorization confirmation
An OAuth 2.1 Server instance that implements the OAuthServerProvider to be used with mcpAuthRouter from the @modelcontextprotocol/typescript-sdk
import { OAuthServer } from 'mcp-oauth-server';
const oauthServer = new OAuthServer({
authorizationUrl: new URL('http://localhost:3000/consent'),
strictResourceUrl: new URL('http://localhost:3000/mcp'),
scopesSupported: ['mcp:tools'],
})Config options:
model: (optional) The storage model to use for the OAuth server. Default:MemoryOAuthServerModel(in-memory, suitable for development). For production, implement your ownOAuthServerModelto use a database.authorizationUrl: (required) The URL to redirect the user to for authorization. This may be a consent screen hosted on the Authorization Server or a custom consent screen hosted on another frontend, like your web application.scopesSupported: (optional) Array of scopes supported by this OAuth server. If the client does not include any scopes in the request, the server will default to all the supported scopes. Some MCP clients do not follow the spec and do not include any scopes in the request.accessTokenLifetime: (optional) The lifetime of the access token in seconds. Default:3600(1 hour)refreshTokenLifetime: (optional) The lifetime of the refresh token in seconds. Default:1209600(2 weeks)clientSecretLifetime: (optional) The number of seconds after which to expire issued client secrets, or0to prevent expiration of client secrets (not recommended). Default:7776000(3 months). Public clients (clients registered withtoken_endpoint_auth_method = 'none') do not have a client secret and live forever.authorizationCodeLifetime: (optional) The lifetime of the authorization code in seconds. Default:300(5 minutes)strictResourceUrl: (optional) The resource indicator (RFC 8707) that must be used for all requests. This should be set to your MCP server URL. Leaving this unset will allow better compatibility with MCP clients that do not follow the spec and do not include a resource indicator in the request.modifyAuthorizationRedirectUrl: (optional) A function to modify the authorization redirect URL. This can be used to add metadata to the authorization redirect URL, like the client name, client URI, or logo URI, which can then be displayed on your consent screen.errorHandler: (optional) A function to handle errors. This can be used to log errors occuring in the OAuth flow.
An interface for the storage model to use for the OAuth server. This is used to store the OAuth server's data, such as clients, tokens, and authorization codes.
import { OAuthServerModel } from 'mcp-oauth-server';
export class PostgresModel implements OAuthServerModel {
async getClient(clientId: string): Promise<OAuthClientInformationFull | undefined> {
const client = await this.db.query('SELECT * FROM clients WHERE client_id = $1', [clientId]);
return client;
}
async registerClient(client: OAuthClientInformationFull): Promise<OAuthClientInformationFull> {
// Modify or omit fields from the client metadata here if needed
return client;
}
async getAccessToken(token: string): Promise<AccessToken | undefined> {
const accessToken = await this.db.query('SELECT * FROM access_tokens WHERE token = $1', [token]);
return accessToken;
}
async saveAccessToken(accessToken: AccessToken): Promise<void> {
await this.db.query('INSERT INTO access_tokens (token, client_id, expires_at, scopes, resource) VALUES ($1, $2, $3, $4, $5)', [accessToken.token, accessToken.client_id, accessToken.expires_at, accessToken.scopes, accessToken.resource]);
}
/* ... */
}Config options:
getClient: (required) Get a client by its client ID.registerClient: (required) Register a new client and return any modifications to the client metadata.getAccessToken: (required) Get an access token by its token.saveAccessToken: (required) Save an access token.revokeAccessToken: (required) Revoke an access token.getRefreshToken: (required) Get a refresh token by its token.saveRefreshToken: (required) Save a refresh token.revokeRefreshToken: (required) Revoke a refresh token.saveAuthorizationCode: (required) Save an authorization code.getAuthorizationCode: (required) Get an authorization code by its code.revokeAuthorizationCode: (required) Revoke an authorization code.
Express middleware that sets up all OAuth 2.1 Authorization Server endpoints (authorization, token, registration, metadata, etc.).
import express from 'express';
import { mcpAuthRouter } from 'mcp-oauth-server';
const app = express();
app.use(mcpAuthRouter({
provider: oauthServer,
issuerUrl: new URL('http://localhost:3000/'),
resourceServerUrl: mcpServerUrl,
scopesSupported: ['mcp:tools'],
}));See router.ts for the full options.
Express middleware that handles the authorization confirmation endpoint. This is called after the user grants consent on your consent screen.
import { authenticateHandler } from 'mcp-oauth-server';
app.post('/confirm', authenticateHandler({
provider: oauthServer,
getUser: (req) => {
// Extract user ID from your authenticated session
// or from the request body if you handle auth in the consent form
return req.body.user_id || req.authenticatedUser?.id;
},
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per windowMs
},
}));Config options:
provider: (required) The OAuthServer instance to use for authenticationgetUser: (required) A function to extract the authenticated user ID from the request. Your own authentication logic should either be done here or in a middleware before this onerateLimit: (optional) The rate limiting configuration for the authorization endpoint. Set tofalseto disable rate limiting for this endpoint
Express middleware that validates Bearer tokens and authenticates requests to protected resources (like your MCP server endpoint).
import { requireBearerAuth } from 'mcp-oauth-server';
app.post('/mcp',
requireBearerAuth({
verifier: oauthServer,
requiredScopes: ['mcp:tools'],
}),
async (req, res) => {
// Access authenticated user ID
const userId = req.auth.userId;
console.log('Authenticated user:', userId);
// Set up MCP Server...
}
);See bearerAuth.ts for the full options.
After authentication, the request will have req.auth with:
userId: The user ID associated with the tokentoken: The access token usedscopes: The scopes granted to the token
Warning
You currently cannot run the Authorization Server (mcpAuthRouter) on a path other than root / path. See modelcontextprotocol/typescript-sdk#1095
- There is no support for the OAuth 2.1 client_credentials grant type, but this should not be a problem as most MCP clients use the
authorization_codegrant type. See modelcontextprotocol/typescript-sdk#899
Note
- Separate Servers: You can run the OAuth Server (Authorization Server) on a different server from the MCP Server (Resource Server) as long as they share the same underlying model/storage backend