OAuth authorization server for MCP servers
Secure your Model Context Protocol (MCP) servers with Scalekit’s drop-in OAuth 2.1 authorization solution
Scalekit provides a production-ready OAuth 2.1 authorization server that implements the MCP authorization specification. This guide shows you how to integrate OAuth-based authentication and authorization into your MCP server with minimal code changes.
Why use Scalekit OAuth for MCP servers?
- Identity-scoped access: Restrict each token to a specific user or agent.
- Granular permissions: Control exactly which tools and data each client can access using fine-grained scopes.
- OAuth 2.1 compliance: Rely on a modern, secure, and widely adopted authorization standard.
- Comprehensive audit trails: Track who accessed what, when, and with which permissions.
For a deeper explanation of why an OAuth layer is essential for remote MCP servers, read the MCP authorization overview.
How it works
Section titled “How it works”The Scalekit OAuth authorization server and your MCP server work together to secure access and enforce permissions.
Scalekit OAuth authorization server
Acts as the identity provider for your MCP server. It:
- Authenticates users and agents
- Issues access tokens with fine-grained scopes
- Manages OAuth 2.1 flows (authorization code, client credentials)
- Supports dynamic client registration for easy onboarding
Your MCP server
Validates incoming access tokens and enforces the permissions encoded in each token. Only requests with valid, authorized tokens are allowed.
This separation of responsibilities ensures a clear boundary: Scalekit handles identity and token issuance, while your MCP server focuses on resource protection.
Getting started
Section titled “Getting started”Prerequisites
Section titled “Prerequisites”Before you begin, ensure you have:
-
Access to your Scalekit account and the API credentials. If you don’t have a Scalekit account yet, you can signup here.
-
Installed Scalekit SDK into your project
Terminal window npm install @scalekit/sdkimport { Scalekit } from '@scalekit-sdk/node';const scalekit = new Scalekit('<SCALEKIT_ENVIRONMENT_URL>','<SCALEKIT_CLIENT_ID>','<SCALEKIT_CLIENT_SECRET>',);
Step 1: Set up your authorization server
Section titled “Step 1: Set up your authorization server”In the Scalekit dashboard, navigate to MCP servers and click Add MCP server. Configure your server with the following settings:
Basic configuration:
- Server name: A display name that users will see during authorization (e.g., “My AI Assistant”)
- Resource identifier: Your MCP server’s unique identifier, typically your server’s URL (e.g.,
https://your-mcp-server.com
). All the tokens that are issued will be scoped to this identifier usingaud
parameter. - Server logo: Upload a 45x45 pixel logo to help users recognize your service
Access control settings:
- Allow dynamic client registration Recommended — Enables MCP clients to register automatically without manual approval. For security reasons, Scalekit ensures that any client that registers via DCR has to implement PKCE-based OAuth 2.1 flow.
- Allow offline access: Enable if your MCP server needs to offer long-term access to resources via refresh tokens for MCP clients that request
offline_access
scope. If you disable this setting, MCP client needs to make a new authorization flow whenever the existing access token expires.
Token configuration:
- Access token lifetime: Recommended 300-3600 seconds (5 minutes to 1 hour)
- Refresh token lifetime: Recommended 300-86400 seconds (5 minutes to 24 hours)
Step 2: Implement resource metadata discovery
Section titled “Step 2: Implement resource metadata discovery”Once you have added your MCP server in the Scalekit dashboard, you will be presented with the protected resource metadata information that you can use to serve the .well-known/oauth-protected-resource
endpoint.
MCP clients discover your authorization server through the OAuth 2.0 protected resource metadata endpoint.
Here is the code sample:
// Required: OAuth Protected Resource Metadata endpointapp.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 clientsapp.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' }); }});
Description of the OAuth Protected Resource Metadata:
Field | Description |
---|---|
resource | The identifier of the resource server. This is the unique identifier of your MCP Server. Every token that Scalekit issues will be scoped to this identifier using aud parameter. |
authorization_servers | A list of authorization servers that are trusted by the resource server. MCP Clients use this information to discover more about the authorization server capability by fetching the authorization server metadata from https://your-org.scalekit.com/.well-known/oauth-authorization-server endpoint. |
bearer_methods_supported | A list of methods that are supported for bearer tokens. MCP Clients send the OAuth token as Authorization: Bearer <token> header. |
resource_documentation | A URL to the documentation of the resource server. |
scopes_supported | A list of scopes that are supported by the resource server. MCP Clients use this information to determine which scopes that they would like the token for as part of the OAuth authorize request. |
Step 3: Implement authorization checks in your MCP server
Section titled “Step 3: Implement authorization checks in your MCP server”Token validation middleware
Section titled “Token validation middleware”Your MCP server needs to validate incoming access tokens. Here’s a complete middleware implementation:
import { jwtVerify, createRemoteJWKSet } from 'jose';
// Configure JWKS endpoint from your Scalekit instanceconst JWKS = createRemoteJWKSet( new URL('https://your-org.scalekit.com/.well-known/jwks'));
// WWW-Authenticate header for 401 responsesconst 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 endpointsapp.use('/mcp', validateToken);
Scope-based authorization
Section titled “Scope-based authorization”Implement granular permissions using OAuth scopes:
const requireScope = (requiredScope) => { return (req, res, next) => { const userScopes = req.auth.scopes || [];
if (!userScopes.includes(requiredScope)) { return res.status(403).json({ error: 'insufficient_scope', error_description: `Required scope: ${requiredScope}`, scope: requiredScope }); }
next(); };};
// Example: Protect specific MCP tools with scopesapp.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);
Step 4: Test the integration
Section titled “Step 4: Test the integration”Create a simple test to verify your setup:
// Test script to validate token flowconst testMCPAuth = async () => { // This would typically be handled by your MCP client 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();
// Test MCP server with token 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());};
Sample endpoints reference
Section titled “Sample endpoints reference”Protected resource metadata
Section titled “Protected resource metadata”MCP Server that you are implementing need to serve the following well-known endpoint that the MCP Clients that are connecting to your server know where to find the OAuth Authorization Server details. This is a static endpoint that returns the metadata about your MCP Server.
GET https://your-mcp-server.com/.well-known/oauth-protected-resource
{ 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:*' ] }
To support legacy MCP Clients, you may also want to optionally support proxying the authorization server metadata endpoint from your MCP Server at /.well-known/oauth-authorization-server
to the Scalekit authorization server metadata endpoint at /.well-known/oauth-authorization-server
.
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' }); }});
Authorization server metadata
Section titled “Authorization server metadata”Scalekit implements and serves the following endpoint so that the MCP Clients that want to get a token from Scalekit can discover the OAuth Authorization Server details. This is a static endpoint that returns the metadata about Scalekit Authorization Server. This will be specific for each MCP Server that you are registering with Scalekit.
GET https://your-org.scalekit.com/resource-id/.well-known/oauth-authorization-server
{ issuer: 'https://your-org.scalekit.com', authorization_endpoint: 'https://your-org.scalekit.com/oauth/authorize', token_endpoint: 'https://your-org.scalekit.com/oauth/token', jwks_uri: 'https://your-org.scalekit.com/jwks', registration_endpoint: 'https://your-org.scalekit.com/resource-id/register', introspection_endpoint: 'https://your-org.scalekit.com/oauth/introspect', revocation_endpoint: 'https://your-org.scalekit.com/oauth/revoke', scopes_supported: [ 'mcp:tools:weather', 'mcp:tools:calendar:read', 'mcp:tools:calendar:write', 'mcp:tools:email:send', 'mcp:resources:*' ], response_types_supported: ['code'], response_modes_supported: ['query'], grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'], token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], subject_types_supported: ['public'], code_challenge_methods_supported: ['S256'], token_endpoint_auth_signing_alg_values_supported: ['RS256']}