Skip to content

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.

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.

Before you begin, ensure you have:

  1. Access to your Scalekit account and the API credentials. If you don’t have a Scalekit account yet, you can signup here.

  2. Installed Scalekit SDK into your project

    Terminal window
    npm install @scalekit/sdk
    import { Scalekit } from '@scalekit-sdk/node';
    const scalekit = new Scalekit(
    '<SCALEKIT_ENVIRONMENT_URL>',
    '<SCALEKIT_CLIENT_ID>',
    '<SCALEKIT_CLIENT_SECRET>',
    );

In the Scalekit dashboard, navigate to MCP servers and click Add MCP server. Configure your server with the following settings:

MCP server registration

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 using aud 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 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' });
}
});

Description of the OAuth Protected Resource Metadata:

FieldDescription
resourceThe 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_serversA 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_supportedA list of methods that are supported for bearer tokens. MCP Clients send the OAuth token as Authorization: Bearer <token> header.
resource_documentationA URL to the documentation of the resource server.
scopes_supportedA 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”

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 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);

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 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
);

Create a simple test to verify your setup:

// Test script to validate token flow
const 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());
};

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' });
}
});

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']
}