Documentation Index Fetch the complete documentation index at: https://docs.leanmcp.com/llms.txt
Use this file to discover all available pages before exploring further.
OAuth Server & Proxy
The @leanmcp/auth/proxy and @leanmcp/auth/server modules enable you to build OAuth 2.1 authorization servers for your MCP applications. Use them to proxy authentication to external identity providers (Google, GitHub, etc.) while issuing your own tokens.
Features
External Provider Proxy Authenticate users via Google, GitHub, Azure, and more
RFC 8414 Metadata Standard OAuth authorization server metadata
RFC 7591 DCR Dynamic Client Registration for MCP clients
PKCE Required Enforces PKCE per MCP security requirements
Installation
npm install @leanmcp/auth express
Architecture Overview
The OAuth Proxy:
Receives authorization requests from MCP clients
Redirects users to the external identity provider
Exchanges the IdP’s code for tokens
Maps external tokens/user info to your internal tokens
Returns your tokens to the MCP client
OAuth Proxy
The OAuthProxy class handles the complete OAuth flow with external providers.
Basic Setup
import express from 'express' ;
import { OAuthProxy , googleProvider , githubProvider } from '@leanmcp/auth/proxy' ;
const app = express ();
app . use ( express . json ());
app . use ( express . urlencoded ({ extended: true }));
// Configure providers
const providers = [
googleProvider ({
clientId: process . env . GOOGLE_CLIENT_ID ! ,
clientSecret: process . env . GOOGLE_CLIENT_SECRET ! ,
}),
githubProvider ({
clientId: process . env . GITHUB_CLIENT_ID ! ,
clientSecret: process . env . GITHUB_CLIENT_SECRET ! ,
}),
];
// Create proxy
const proxy = new OAuthProxy ({
baseUrl: 'https://api.example.com/auth' ,
sessionSecret: process . env . SESSION_SECRET ! ,
providers ,
tokenMapper : async ( externalTokens , userInfo , provider ) => {
// Map external tokens to your internal tokens
return {
access_token: generateMyToken ( userInfo ),
token_type: 'Bearer' ,
expires_in: 3600 ,
};
},
});
// Mount routes
const middleware = proxy . createMiddleware ();
app . get ( '/auth/authorize' , middleware . authorize );
app . get ( '/auth/callback' , middleware . callback );
app . post ( '/auth/token' , middleware . token );
app . listen ( 3000 );
Configuration
interface OAuthProxyConfig {
/** Base URL for OAuth endpoints (e.g., https://api.example.com/auth) */
baseUrl : string ;
/** Secret for signing session/state tokens */
sessionSecret : string ;
/** Array of configured OAuth providers */
providers : OAuthProviderConfig [];
/** Function to map external tokens to your tokens */
tokenMapper : TokenMapperFunction ;
/** Token TTL in seconds (default: 3600) */
tokenTTL ?: number ;
}
Token Mapper
The tokenMapper function is called after successful external authentication. Use it to create your internal tokens:
const proxy = new OAuthProxy ({
// ...
tokenMapper : async ( externalTokens , userInfo , provider ) => {
// externalTokens: { access_token, refresh_token, ... } from provider
// userInfo: { sub, email, name, ... } from provider's userinfo endpoint
// provider: { id, name, ... } the provider that authenticated
// Look up or create user in your database
let user = await db . users . findByEmail ( userInfo . email );
if ( ! user ) {
user = await db . users . create ({
email: userInfo . email ,
name: userInfo . name ,
provider: provider . id ,
});
}
// Generate your own token
const token = jwt . sign (
{ sub: user . id , email: user . email },
process . env . JWT_SECRET ! ,
{ expiresIn: '1h' }
);
return {
access_token: token ,
token_type: 'Bearer' ,
expires_in: 3600 ,
user_id: user . id ,
};
},
});
Import ready-to-use provider configurations:
Google
import { googleProvider } from '@leanmcp/auth/proxy' ;
googleProvider ({
clientId: process . env . GOOGLE_CLIENT_ID ! ,
clientSecret: process . env . GOOGLE_CLIENT_SECRET ! ,
scopes: [ 'openid' , 'profile' , 'email' ], // optional, these are defaults
});
GitHub
import { githubProvider } from '@leanmcp/auth/proxy' ;
githubProvider ({
clientId: process . env . GITHUB_CLIENT_ID ! ,
clientSecret: process . env . GITHUB_CLIENT_SECRET ! ,
scopes: [ 'read:user' , 'user:email' ], // optional
});
Azure AD
import { azureProvider } from '@leanmcp/auth/proxy' ;
azureProvider ({
clientId: process . env . AZURE_CLIENT_ID ! ,
clientSecret: process . env . AZURE_CLIENT_SECRET ! ,
tenant: process . env . AZURE_TENANT_ID ! , // or 'common' for multi-tenant
});
GitLab
import { gitlabProvider } from '@leanmcp/auth/proxy' ;
gitlabProvider ({
clientId: process . env . GITLAB_CLIENT_ID ! ,
clientSecret: process . env . GITLAB_CLIENT_SECRET ! ,
baseUrl: 'https://gitlab.com' , // or self-hosted URL
});
Slack
import { slackProvider } from '@leanmcp/auth/proxy' ;
slackProvider ({
clientId: process . env . SLACK_CLIENT_ID ! ,
clientSecret: process . env . SLACK_CLIENT_SECRET ! ,
});
Discord
import { discordProvider } from '@leanmcp/auth/proxy' ;
discordProvider ({
clientId: process . env . DISCORD_CLIENT_ID ! ,
clientSecret: process . env . DISCORD_CLIENT_SECRET ! ,
});
Custom Providers
Use customProvider for any OAuth 2.0 compatible identity provider:
import { customProvider } from '@leanmcp/auth/proxy' ;
const myProvider = customProvider ({
id: 'my-idp' ,
name: 'My Identity Provider' ,
authorizationEndpoint: 'https://idp.example.com/oauth/authorize' ,
tokenEndpoint: 'https://idp.example.com/oauth/token' ,
userInfoEndpoint: 'https://idp.example.com/oauth/userinfo' ,
clientId: process . env . MY_IDP_CLIENT_ID ! ,
clientSecret: process . env . MY_IDP_CLIENT_SECRET ! ,
scopes: [ 'openid' , 'profile' , 'email' ],
supportsPkce: true ,
});
Provider Configuration
interface OAuthProviderConfig {
/** Unique identifier for this provider */
id : string ;
/** Display name */
name : string ;
/** OAuth authorization endpoint */
authorizationEndpoint : string ;
/** OAuth token endpoint */
tokenEndpoint : string ;
/** OpenID Connect userinfo endpoint (optional) */
userInfoEndpoint ?: string ;
/** Client ID */
clientId : string ;
/** Client secret */
clientSecret : string ;
/** Scopes to request */
scopes ?: string [];
/** Whether provider supports PKCE */
supportsPkce ?: boolean ;
}
OAuth Authorization Server
For full MCP OAuth compliance, use OAuthAuthorizationServer which adds RFC 8414 metadata and RFC 7591 Dynamic Client Registration:
import express from 'express' ;
import { OAuthAuthorizationServer } from '@leanmcp/auth/server' ;
import { googleProvider } from '@leanmcp/auth/proxy' ;
const app = express ();
const authServer = new OAuthAuthorizationServer ({
issuer: 'https://api.example.com' ,
// Upstream provider for authentication
upstreamProvider: googleProvider ({
clientId: process . env . GOOGLE_CLIENT_ID ! ,
clientSecret: process . env . GOOGLE_CLIENT_SECRET ! ,
}),
// JWT signing secret
signingSecret: process . env . JWT_SECRET ! ,
// Enable Dynamic Client Registration
enableDCR: true ,
// Token TTL in seconds
tokenTTL: 3600 ,
// Optional: Custom token mapper
tokenMapper : async ( upstreamTokens , userInfo ) => {
return {
sub: userInfo . sub ,
email: userInfo . email ,
name: userInfo . name ,
};
},
});
// Mount OAuth routes
app . use ( authServer . createRouter ());
// Serves:
// GET /.well-known/oauth-authorization-server (RFC 8414 metadata)
// POST /oauth/register (RFC 7591 DCR)
// GET /oauth/authorize (Authorization endpoint)
// GET /oauth/callback (Provider callback)
// POST /oauth/token (Token endpoint)
app . listen ( 3000 );
The server automatically exposes OAuth metadata at /.well-known/oauth-authorization-server:
{
"issuer" : "https://api.example.com" ,
"authorization_endpoint" : "https://api.example.com/oauth/authorize" ,
"token_endpoint" : "https://api.example.com/oauth/token" ,
"registration_endpoint" : "https://api.example.com/oauth/register" ,
"scopes_supported" : [ "openid" , "profile" , "email" ],
"response_types_supported" : [ "code" ],
"grant_types_supported" : [ "authorization_code" ],
"code_challenge_methods_supported" : [ "S256" ]
}
Dynamic Client Registration (RFC 7591)
MCP clients can register dynamically:
curl -X POST https://api.example.com/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3001/callback"]
}'
Response:
{
"client_id" : "abc123" ,
"client_secret" : "secret456" ,
"client_name" : "My MCP Client" ,
"redirect_uris" : [ "http://localhost:3001/callback" ]
}
MCP Auth Error Responses
Use createAuthError from @leanmcp/core to return MCP-compliant authentication errors that trigger ChatGPT’s OAuth linking UI:
import { Tool , createAuthError } from '@leanmcp/core' ;
export class MyService {
@ Tool ({ description: 'Get private data' })
async getPrivateData ( args : any , meta ?: any ) {
const token = meta ?. authorization ?. token ;
if ( ! token ) {
return createAuthError ( 'Authentication required' , {
resourceMetadataUrl: ` ${ process . env . PUBLIC_URL } /.well-known/oauth-protected-resource` ,
error: 'invalid_token' ,
errorDescription: 'No access token provided' ,
});
}
// ... proceed with authenticated request
}
}
The createAuthError function returns a response with _meta["mcp/www_authenticate"] that signals to MCP clients (including ChatGPT) to initiate OAuth authentication.
Complete Example
Here’s a complete OAuth proxy server:
import 'dotenv/config' ;
import express from 'express' ;
import { OAuthProxy , googleProvider , githubProvider } from '@leanmcp/auth/proxy' ;
import jwt from 'jsonwebtoken' ;
const app = express ();
app . use ( express . json ());
app . use ( express . urlencoded ({ extended: true }));
const PORT = 3000 ;
const BASE_URL = `http://localhost: ${ PORT } ` ;
// In-memory user database (use a real database in production)
const users = new Map ();
const proxy = new OAuthProxy ({
baseUrl: ` ${ BASE_URL } /auth` ,
sessionSecret: process . env . SESSION_SECRET ! ,
providers: [
googleProvider ({
clientId: process . env . GOOGLE_CLIENT_ID ! ,
clientSecret: process . env . GOOGLE_CLIENT_SECRET ! ,
}),
githubProvider ({
clientId: process . env . GITHUB_CLIENT_ID ! ,
clientSecret: process . env . GITHUB_CLIENT_SECRET ! ,
}),
],
tokenMapper : async ( externalTokens , userInfo , provider ) => {
// Find or create user
const key = ` ${ provider . id } : ${ userInfo . email } ` ;
let user = users . get ( key );
if ( ! user ) {
user = {
id: crypto . randomUUID (),
email: userInfo . email ,
name: userInfo . name ,
provider: provider . id ,
createdAt: new Date (),
};
users . set ( key , user );
console . log ( 'Created user:' , user . email );
}
// Generate JWT
const token = jwt . sign (
{ sub: user . id , email: user . email , name: user . name },
process . env . JWT_SECRET ! ,
{ expiresIn: '1h' }
);
return {
access_token: token ,
token_type: 'Bearer' ,
expires_in: 3600 ,
};
},
});
// OAuth routes
const middleware = proxy . createMiddleware ();
app . get ( '/auth/authorize' , middleware . authorize );
app . get ( '/auth/callback' , middleware . callback );
app . post ( '/auth/token' , middleware . token );
// List available providers
app . get ( '/auth/providers' , ( req , res ) => {
res . json ({
providers: [
{ id: 'google' , name: 'Google' },
{ id: 'github' , name: 'GitHub' },
],
});
});
// Protected API endpoint
app . get ( '/api/me' , ( req , res ) => {
const auth = req . headers . authorization ;
if ( ! auth ?. startsWith ( 'Bearer ' )) {
return res . status ( 401 ). json ({ error: 'Unauthorized' });
}
try {
const payload = jwt . verify ( auth . slice ( 7 ), process . env . JWT_SECRET ! );
res . json ({ user: payload });
} catch {
res . status ( 401 ). json ({ error: 'Invalid token' });
}
});
app . listen ( PORT , () => {
console . log ( `OAuth server running at ${ BASE_URL } ` );
});