Implementing OAuth for MCP servers: A developer’s guide

The Model Context Protocol (MCP) is revolutionizing how AI applications interact with external systems. But as your MCP server moves from prototype to production, one question becomes critical: How do you secure it with an auth and identity layer?

This guide walks you through implementing OAuth 2.1 authentication for your MCP server using Scalekit. We'll cover the complete implementation process, from initial setup to production deployment.

Implementation overview

Whether you’re building a brand new MCP server or retrofitting an existing one, OAuth implementation involves four steps.

  1. Register your MCP Server and configure appropriate OAuth scopes
  2. Implement OAuth protected resource metadata (/.well-known/oauth-protected-resource)
  3. Validate JWT tokens
  4. (Optional) Verify scopes as part of JWT token

Let's walk through each step with practical code examples. You can also follow through with our video demo below.

1. Register your MCP server

Head over to the Scalekit dashboard to register your MCP server. Think of this step as provisioning your server’s identity and defining what permissions it can grant to clients.

Basic configuration

  • Server name: A user-friendly name (e.g., "Weather Assistant API")
  • Resource identifier: Your MCP server’s unique identifier, typically your server’s URL (e.g., https://api.weather-assistant.com)
  • Server logo: Upload a 45x45 pixel logo to help users recognize your service

Access control settings

  • Dynamic client registration: Enables MCP clients to register automatically
  • Offline access: Allow refresh tokens for long-term access
  • Token configuration:
    • Access tokens: 300-3600 seconds (5 minutes to 1 hour)
    • Refresh tokens: 300-86400 seconds (5 minutes to 24 hours)

Define scopes

Plan your scopes carefully so you can control which actions require which permissions. Examples:

  • mcp:tools:weather:read- Read weather data
  • mcp:tools:calendar:write - Modify calendar events
  • mcp:resources:customer-data:read - Access customer information
  • mcp:exec:workflows:* - Execute any workflow

2. Implement OAuth protected resource metadata

MCP clients discover your authorization server through the OAuth protected resource metadata endpoint. This is your server's "business card" for OAuth clients.

OAuth relies on metadata discovery so clients and tools can integrate without hard-coding endpoints. Your MCP server needs to expose an OAuth Protected Resource Metadata endpoint.

Example

// Required: OAuth Protected Resource Metadata endpoint app.get('/.well-known/oauth-protected-resource', (req, res) => { res.json({ resource: 'https://your-mcp-server.com', authorization_servers: ['https://your-org.scalekit.com'], bearer_methods_supported: ['header'], resource_documentation: 'https://your-mcp-server.com/docs', scopes_supported: [ 'mcp:tools:weather', 'mcp:tools:calendar:read', 'mcp:tools:calendar:write', 'mcp:tools:email:send', 'mcp:resources:*' ] }); }); // Optional: Proxy authorization server metadata for legacy clients app.get('/.well-known/oauth-authorization-server', async (req, res) => { try { const response = await fetch( 'https://your-org.scalekit.com/.well-known/oauth-authorization-server' ); const metadata = await response.json(); res.json(metadata); } catch (error) { res.status(500).json({ error: 'Failed to fetch authorization server metadata' }); } });

Key metadata fields

  • resource: Your MCP server's unique identifier (matches the aud claim in JWT tokens)
  • authorization_servers: List of trusted authorization servers
  • bearer_methods_supported: How tokens are transmitted (typically header)
  • scopes_supported: Available permissions for clients to request

You can find a complete reference format in the docs.

3. Implement JWT token validation

This is the heart of your security implementation. Every request to your MCP server should validate the incoming JWT token. Once your server is registered and metadata is available, every incoming request will include a Bearer token in the Authorization header.

Security best practices

  • Always validate the issuer - Ensure tokens come from your trusted authorization server
  • Check the audience - Verify tokens are intended for your server
  • Handle errors gracefully - Return proper WWW-Authenticate headers for client guidance
  • Log security events - Monitor failed authentication attempts

Example (Node.js/Pseudocode):

import { jwtVerify, createRemoteJWKSet } from 'jose'; // Configure JWKS endpoint from your Scalekit instance const JWKS = createRemoteJWKSet( new URL('https://your-org.scalekit.com/.well-known/jwks') ); // WWW-Authenticate header for 401 responses const WWW_AUTHENTICATE_HEADER = [ 'Bearer error="unauthorized"', 'error_description="Authorization required"', `resource_metadata="https://your-mcp-server.com/.well-known/oauth-protected-resource"` ].join(', '); const validateToken = async (req, res, next) => { const authHeader = req.headers.authorization; const token = authHeader?.match(/^Bearer (.+)$/)?.[1]; if (!token) { return res .set('WWW-Authenticate', WWW_AUTHENTICATE_HEADER) .status(401) .json({ error: 'unauthorized', error_description: 'Bearer token required' }); } try { const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://your-org.scalekit.com', audience: 'https://your-mcp-server.com' // Your MCP server identifier }); // Attach token claims to request for downstream use req.auth = { userId: payload.sub, scopes: payload.scope?.split(' ') || [], clientId: payload.client_id, expiresAt: payload.exp }; next(); } catch (error) { console.error('Token validation failed:', error.message); return res .set('WWW-Authenticate', WWW_AUTHENTICATE_HEADER) .status(401) .json({ error: 'invalid_token', error_description: 'Bearer token is invalid or expired' }); } }; // Apply to all MCP endpoints app.use('/mcp', validateToken);

Feel free to adapt this pattern to your preferred language or framework.

4. Implement scope-based authorization

OAuth scopes enable fine-grained permission control. Validating the token’s signature proves the request is from an authenticated client, but you often also need to check if it has permission to perform a specific action. Here's how to implement scope checking:

const requireScope = (requiredScope) => { return (req, res, next) => { const userScopes = req.auth.scopes || []; // Check for exact scope match or wildcard permissions const hasScope = userScopes.some(scope => scope === requiredScope || scope.endsWith(':*') && requiredScope.startsWith(scope.slice(0, -1)) ); if (!hasScope) { return res.status(403).json({ error: 'insufficient_scope', error_description: `Required scope: ${requiredScope}`, scope: requiredScope }); } next(); }; }; // Example: Protect specific MCP tools with scopes app.post('/mcp/tools/weather', requireScope('mcp:tools:weather'), handleWeatherRequest ); app.post('/mcp/tools/calendar', requireScope('mcp:tools:calendar:read'), handleCalendarRead ); app.post('/mcp/tools/send-email', requireScope('mcp:tools:email:send'), handleEmailSend );

Testing your implementation

Create a comprehensive test suite to verify your OAuth implementation:

const testMCPAuth = async () => { // 1. Test token acquisition const tokenResponse = await fetch('https://your-org.scalekit.com/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'your-test-client-id', client_secret: 'your-test-client-secret', scope: 'mcp:tools:weather' }) }); const { access_token } = await tokenResponse.json(); // 2. Test authorized request const mcpResponse = await fetch('https://your-mcp-server.com/mcp/tools/weather', { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'weather/get_forecast', params: { location: 'San Francisco' } }) }); console.log('MCP Response:', await mcpResponse.json()); // 3. Test unauthorized request (wrong scope) const unauthorizedResponse = await fetch('https://your-mcp-server.com/mcp/tools/calendar', { method: 'POST', headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'calendar/list_events', params: {} }) }); console.log('Should be 403:', unauthorizedResponse.status); };

Production considerations

  • Rate limiting: Implement rate limiting on token validation endpoints
  • Token introspection: Consider implementing token introspection for high-security scenarios
  • Audit logging: Log all authentication and authorization events
  • Token rotation: Implement proper refresh token rotation
  • Token caching: Cache JWKS Public Keys for 24 hours to reduce http calls and make token signature verification instantaneous
  • Connection pooling: Reuse HTTP connections for token validation
  • Async processing: Use async/await patterns for non-blocking operations

Error handling

// Comprehensive error handling const handleAuthError = (error, req, res, next) => { if (error.code === 'ERR_JWT_EXPIRED') { return res.status(401).json({ error: 'token_expired', error_description: 'The access token has expired' }); } if (error.code === 'ERR_JWT_INVALID') { return res.status(401).json({ error: 'invalid_token', error_description: 'The access token is malformed' }); } // Generic error response return res.status(500).json({ error: 'server_error', error_description: 'An internal server error occurred' }); }; app.use(handleAuthError);

Conclusion

Implementing OAuth for your MCP server transforms it from a prototype into a production-ready service. The four-step process—registration, metadata implementation, token validation, and scope verification — provides a solid foundation for secure AI integrations.

With OAuth properly implemented, your MCP server can safely integrate with enterprise systems, handle sensitive data, and scale to meet production demands. The investment in proper authorization pays dividends in security, compliance, and user trust.

Ready to move forward? Consider Scalekit as drop-in OAuth authorization server for your MCP servers

If you want a deeper dive or ready-to-use code samples, explore our full guide here.

Further reading

Questions? Email me on ravi [at] scalekit.com and happy to chat.

No items found.
On this page
Share this article
Ready to add auth to your MCP servers?

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
1 FREE SSO/SCIM connection each
1000 Monthly active users
25 Monthly active organizations
Passwordless auth
API auth: 1000 M2M tokens
MCP auth: 1000 M2M tokens