MCP Authentication: MCP-specific authentication patterns and integration guides with OAuth 2.1 and Dynamic Client Registration
---
# DOCUMENT BOUNDARY
---
# Overview: MCP server authentication
> Secure your Model Context Protocol (MCP) servers with Scalekit's drop-in OAuth 2.1 authorization solution
Model Context Protocol (MCP) is an open standard that gives AI apps a consistent, secure way to connect to external tools and data sources. A helpful way to picture it is USB‑C for AI integrations: instead of building a custom connector for every service, MCP provides one interface that works across different models, platforms, and backends. That makes it much easier to build agent-style apps that can actually do work, but it also makes authorization a bigger deal, because once an agent can act on your behalf, you need clear, tight control over what it can access and what actions it’s allowed to take. At its core, MCP follows a client-server architecture where a host application can connect to multiple servers: * **MCP hosts**: AI applications like Claude Desktop, IDEs, or custom AI tools that need to access external resources * **MCP clients**: Protocol clients that maintain connections between hosts and servers * **MCP servers**: Lightweight programs that expose specific capabilities (tools, data, or services) through the standardized protocol * **Data sources**: Local files, databases, APIs, and services that MCP servers can access This architecture enables a ecosystem where AI models can seamlessly integrate with hundreds of different services without requiring custom code for each integration. ## The path to secure MCP: OAuth 2.1 integration [Section titled “The path to secure MCP: OAuth 2.1 integration”](#the-path-to-secure-mcp-oauth-21-integration) Recognizing these challenges, the MCP specification evolved to incorporate robust authorization mechanisms. The Model Context Protocol provides authorization capabilities at the transport level, enabling MCP clients to make requests to restricted MCP servers on behalf of resource owners. The **MCP specification chose OAuth 2.1 as its authorization framework** for several compelling reasons | | | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Industry standard | OAuth 2.1 is a well-established, widely-adopted standard for delegated authorization, with extensive tooling and ecosystem support. | | Security best practices | OAuth 2.1 incorporates lessons learned from OAuth 2.0, removing deprecated flows and enforcing security measures like PKCE (Proof Key for Code Exchange). | | Flexibility | Supports multiple grant types suitable for different MCP use cases: **Authorization code**: When AI agents act on behalf of human users **Client credentials**: For machine-to-machine integrations | | Ecosystem compatibility | Works with existing identity providers and authorization servers, making it easier for enterprises to integrate MCP into their existing security infrastructure. | This authorization mechanism is based on established specifications listed below, but implements a selected subset of their features to ensure security and interoperability while maintaining simplicity: * **OAuth 2.1**: Core authorization framework with enhanced security * **OAuth 2.0 Authorization Server Metadata (RFC8414)**: Standardized server discovery * **OAuth 2.0 Dynamic Client Registration Protocol (RFC7591)**: Automatic client registration * **OAuth 2.0 Protected Resource Metadata (RFC9728)**: Resource server discovery * **Client ID Metadata Document (CIMD)**: Lets authorization servers fetch client metadata directly from a client-hosted document for authorization ## The authorization flow in practice [Section titled “The authorization flow in practice”](#the-authorization-flow-in-practice) Now let’s zoom in and see how the MCP OAuth 2.1 flow unfolds step-by-step: ### Discovery phase [Section titled “Discovery phase”](#discovery-phase) 1. **MCP client** encounters a protected MCP server 2. **Server** responds with `401 Unauthorized` and `WWW-Authenticate` header pointing to Scalekit Auth Server 3. **Client** discovers Scalekit Auth Server capabilities through metadata endpoints ### Authorization phase [Section titled “Authorization phase”](#authorization-phase) 4. **Client** registers with Scalekit Auth Server (if using DCR) 5. **Scalekit Auth Server** issues client credentials (if using DCR) 6. **Client** initiates appropriate OAuth flow 7. **User** grants consent (for Authorization Code flow) 8. **Scalekit Auth Server** issues access token with appropriate scopes ### Client registration [Section titled “Client registration”](#client-registration) #### Dynamic client registration [Section titled “Dynamic client registration”](#dynamic-client-registration) MCP clients and authorization servers SHOULD support the OAuth 2.1 Dynamic Client Registration Protocol to allow MCP clients to obtain OAuth client IDs without user interaction. This enables seamless onboarding of new AI agents without manual configuration. #### Client ID Metadata Document (CIMD) [Section titled “Client ID Metadata Document (CIMD)”](#client-id-metadata-document-cimd) MCP clients SHOULD support the Client ID Metadata Document (CIMD) specification, which allows clients to publish their OAuth client metadata at a well-known URL under their control. This enables authorization servers to automatically retrieve and validate client metadata without requiring an explicit dynamic registration request, simplifying onboarding for new AI agents while maintaining secure, decentralized client configuration. ### Access phase [Section titled “Access phase”](#access-phase) 9. **Client** includes access token in requests to MCP server 10. **MCP server** validates token and enforces scope-based permissions 11. **Server** processes request and returns response 12. **All interactions** are logged for audit and compliance ## Key security enhancements in MCP OAuth 2.1 [Section titled “Key security enhancements in MCP OAuth 2.1”](#key-security-enhancements-in-mcp-oauth-21) MCP’s OAuth 2.1 profile reduces a few common risks in the authorization code flow. The key enhancements are: * **Mandatory PKCE**: Clients must use PKCE to help prevent authorization code interception. * **Strict redirect URI validation**: Servers must only allow pre-registered redirect URIs and enforce an exact match to reduce redirect attacks. * **Short-lived tokens**: Authorization servers should issue short-lived access tokens to limit impact if a token leaks. * **Granular scopes**: Use narrow scopes (for example, `todo:read`, `todo:write`) so apps request only what they need and users can understand what they’re granting.
---
# DOCUMENT BOUNDARY
---
# Add OAuth 2.1 authorization to MCP servers
> Secure your Model Context Protocol (MCP) servers with Scalekit's drop-in OAuth 2.1 authorization solution and protect your AI integrations
This guide shows you how to add production-ready OAuth 2.1 authorization to your Model Context Protocol (MCP) server using Scalekit. You’ll learn how to secure your MCP server so that only authenticated and authorized users can access your tools through AI hosts like Claude Desktop, Cursor, or VS Code. ### Build with a coding agent * Global install (recommended) Terminal
```bash
npm install -g @scalekit-inc/cli
scalekit setup
```
* npx (one-off) Terminal
```bash
npx @scalekit-inc/cli setup
```
MCP servers expose tools that AI hosts can discover and execute to interact with your resources. For example: * A sales team member could use Claude Desktop to view customer information, update records, or set follow-up reminders * A developer could use VS Code or Cursor with a GitHub MCP server to perform everyday GitHub actions through chat * An autonomous agent could use an MCP server to perform actions such as look up the account details in a CRM system When you build MCP servers, multiple AI hosts may need to discover and use your server to interact with your resources. Scalekit handles the complex authentication and authorization for you, so you can focus on building better tools and improving functionality. 1. ## Get Scalekit SDK [Section titled “Get Scalekit SDK”](#get-scalekit-sdk) To get started, make sure you have your Scalekit account and API credentials ready. If you haven’t created a Scalekit account yet, you can [sign up and get a free account](https://app.scalekit.com/ws/signup). Next, install the Scalekit SDK for your language: * Node.js
```bash
npm install @scalekit-sdk/node
```
* Python
```sh
pip install scalekit-sdk-python
```
Use the Scalekit dashboard to register your MCP server and configure MCP hosts (or AI agents following the MCP client protocol) to use Scalekit as the authorization server. The Scalekit SDK validates tokens after users have been authenticated and authorized to access your MCP server. 2. ## Add MCP server to get drop-in OAuth2.1 authorization server [Section titled “Add MCP server to get drop-in OAuth2.1 authorization server”](#add-mcp-server-to-get-drop-in-oauth21-authorization-server) In the Scalekit dashboard, go to **MCP servers** and select **Add MCP server**.  1. Provide a **name** for your MCP server to help you identify it easily. This name appears on the Consent page that MCP hosts display to users when authorizing access to your MCP server. 2. Enable **dynamic client registration** for MCP hosts. This allows MCP hosts to automatically register with Scalekit (and your authorization server), eliminating the need for manual registration and making it easier for users to adopt your MCP server secur. 3. Enable **Client ID Metadata Document (CIMD)** to allow your authorization server to fetch client metadata from MCP hosts and authorize them automatically. 4. Click **Save** to register the server. Note: If your MCP server is intended for use by public MCP clients such as Claude, Cursor, or VS Code, it is recommended to keep both DCR and CIMD enabled. Clients that support CIMD will use the CIMD flow, while clients that do not yet support CIMD can fall back to Dynamic Client Registration. This ensures your MCP server remains compatible with the widest range of MCP clients while preserving a smooth authorization experience. Toggling DCR or CIMD? If you enable or disable DCR or CIMD, be sure to restart your MCP server. Certain MCP frameworks, like FastMCP, cache authorization server details, and a restart ensures the updated configuration is correctly applied. 3. ## Let MCP clients discover your OAuth2.1 authorization server [Section titled “Let MCP clients discover your OAuth2.1 authorization server”](#let-mcp-clients-discover-your-oauth21-authorization-server) MCP protocol directs any MCP client to discover your OAuth2.1 authorization server by calling a public endpoint on your MCP server. This endpoint is called `.well-known/oauth-protected-resource` and your MCP server must host this endpoint.  Copy the resource metadata JSON from **Dashboard > MCP Servers > Your server > Metadata JSON** and implement it in your `.well-known/oauth-protected-resource` endpoint. The `authorization_servers` field contains your Scalekit resource identifier, which clients use to initiate the OAuth flow. * Node.js
```javascript
// MCP client discovery endpoint
// Use case: Allow MCP clients to discover OAuth authorization server configuration
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
// From Scalekit dashboard > MCP servers > Your server > Metadata JSON
"authorization_servers": [
"https:///resources/"
],
"bearer_methods_supported": [
"header" // Bearer token in Authorization header
],
"resource": "https://mcp.yourapp.com", // Your MCP server URL
"resource_documentation": "https://mcp.yourapp.com/docs", // A URL to the documentation of the resource server
"scopes_supported": ["todo:read", "todo:write"] // Dashboard-configured scopes
});
});
```
* Python
```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
# OAuth Protected Resource Metadata endpoint - Required for MCP client discovery
# Copy the actual authorization server URL and metadata from your Scalekit dashboard.
# The values shown here are examples - replace with your actual configuration.
@app.get("/.well-known/oauth-protected-resource")
async def get_oauth_protected_resource():
return JSONResponse({
"authorization_servers": [
"https:///resources/"
],
"bearer_methods_supported": [
"header"
],
"resource": "https://mcp.yourapp.com",
"resource_documentation": "https://mcp.yourapp.com/docs",
"scopes_supported": ["todo:read", "todo:write"]
})
```
4. ## Validate all MCP client requests have a valid access token [Section titled “Validate all MCP client requests have a valid access token”](#validate-all-mcp-client-requests-have-a-valid-access-token) Your MCP server should validate that all incoming requests contain a valid access token. Leverage Scalekit SDKs to validate tokens and verify essential claims such as `aud` (audience), `iss` (issuer), `exp` (expiration), `iat` (issued at), and `scope` (permissions). * Node.js auth-config.js
```javascript
1
import { Scalekit } from '@scalekit-sdk/node';
2
3
// Initialize Scalekit client with environment credentials
4
// Reference installation guide for client setup details
5
const scalekit = new Scalekit(
6
process.env.SCALEKIT_ENVIRONMENT_URL,
7
process.env.SCALEKIT_CLIENT_ID,
8
process.env.SCALEKIT_CLIENT_SECRET
9
);
10
11
// Resource configuration
12
// Get these values from Scalekit dashboard > MCP servers > Your server
13
// For FastMCP: Use base URL with trailing slash (e.g., https://mcp.example.com/)
14
const RESOURCE_ID = 'https://your-mcp-server.com'; // If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard.
15
const METADATA_ENDPOINT = 'https://your-mcp-server.com/.well-known/oauth-protected-resource';
16
17
// WWW-Authenticate header for unauthorized responses
18
// This helps clients understand how to authenticate properly
19
export const WWWHeader = {
20
HeaderKey: 'WWW-Authenticate',
21
HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"`
22
};
```
* Python auth\_config.py
```python
1
from scalekit import ScalekitClient
2
from scalekit.common.scalekit import TokenValidationOptions
3
import os
4
5
# Initialize Scalekit client with environment credentials
6
# Reference installation guide for client setup details
7
scalekit_client = ScalekitClient(
8
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
9
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
10
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
11
)
12
13
# Resource configuration
14
# Get these values from Scalekit dashboard > MCP servers > Your server
15
# For FastMCP: Use base URL with trailing slash (e.g., https://mcp.example.com/)
16
RESOURCE_ID = "https://your-mcp-server.com" # If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard.
17
METADATA_ENDPOINT = "https://your-mcp-server.com/.well-known/oauth-protected-resource"
18
19
# WWW-Authenticate header for unauthorized responses
20
# This helps clients understand how to authenticate properly
21
WWW_HEADER = {
22
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"'
23
}
```
Extract the Bearer token from incoming MCP client requests. MCP clients send tokens in the `Authorization: Bearer ` header format. * Node.js
```javascript
1
// Extract Bearer token from Authorization header
2
// Use case: Validate requests from AI hosts like Claude Desktop, Cursor, or VS Code
3
const authHeader = req.headers['authorization'];
4
const token = authHeader?.startsWith('Bearer ')
5
? authHeader.split('Bearer ')[1]?.trim()
6
: null;
7
8
if (!token) {
9
throw new Error('Missing or invalid Bearer token');
10
}
```
* Python
```python
1
# Extract Bearer token from Authorization header
2
# Use case: Validate requests from AI hosts like Claude Desktop, Cursor, or VS Code
3
auth_header = request.headers.get("Authorization", "")
4
token = None
5
if auth_header.startswith("Bearer "):
6
token = auth_header.split("Bearer ")[1].strip()
7
8
if not token:
9
raise ValueError("Missing or invalid Bearer token")
```
Validate the token against your configured resource audience to ensure it was issued for your specific MCP server. The resource identifier must match the Server URL you registered earlier. * Node.js Validate token
```javascript
1
// Security: Validate token against configured resource audience
2
// This ensures the token was issued for your specific MCP server
3
await scalekit.validateToken(token, {
4
issuer: ''
5
audience: [RESOURCE_ID]
6
});
```
* Python Validate token
```python
1
# Method 1: validate_access_token - Returns boolean (True/False)
2
# Use this method when you only need to verify token validity without detailed error information.
3
# This approach is suitable for simple authorization checks where you don't need token claims.
4
def validate_token_with_issuer_audience(token: str) -> bool:
5
"""
6
Validates a token and returns True if valid, False otherwise.
7
8
:param token: The token to validate
9
:return: True if token is valid, False otherwise
10
"""
11
options = TokenValidationOptions(
12
issuer="",
13
audience=[RESOURCE_ID]
14
)
15
16
try:
17
is_valid = scalekit_client.validate_access_token(token, options=options)
18
return is_valid
19
except Exception as ex:
20
print(f"Token validation failed: {ex}")
21
return False
22
23
# Method 2: validate_token - Returns token claims/payload
24
# Use this method when you need access to token claims (user info, scopes, etc.) or detailed error information.
25
# This approach is suitable for authorization that requires specific user context or scope validation.
26
def validate_token_and_get_claims(token: str) -> dict:
27
"""
28
Validates a token with specific audience and raises exception on failure.
29
30
:param token: The token to validate
31
:raises: ScalekitValidateTokenFailureException if validation fails
32
"""
33
options = TokenValidationOptions(
34
issuer="",
35
audience=[RESOURCE_ID],
36
required_scopes=["todo:read", "todo:write"] # Optional: validate specific scopes for finer access control
37
)
38
39
scalekit_client.validate_token(token, options=options)
```
* Go middleware/auth.go
```go
1
// Security: validate the token's issuer and audience — confirms it was issued by
2
// your Scalekit environment for your specific MCP server.
3
ok, err := scalekitClient.ValidateTokenWithOptions(ctx, token, &scalekit.ValidateTokenOptions{
4
Issuer: "",
5
Audience: []string{RESOURCE_ID},
6
})
7
if err != nil || !ok {
8
// Reject: return 401 with the WWW-Authenticate header and stop processing.
9
w.Header().Set("WWW-Authenticate", wwwAuthenticateHeader)
10
http.Error(w, "invalid token", http.StatusUnauthorized)
11
return
12
}
13
// Token is valid — continue handling the request.
14
15
// Prefer validating scopes per tool-call rather than in this global check, so each
16
// tool enforces only the scope it needs.
```
* Java middleware/AuthMiddleware.java
```java
1
// Security: validate the token's issuer and audience — confirms it was issued by
2
// your Scalekit environment for your specific MCP server.
3
boolean isValid = scalekitClient.authentication().validateAccessToken(
4
token,
5
TokenValidationOptions.builder()
6
.issuer("")
7
.audience(List.of(RESOURCE_ID))
8
.build()
9
);
10
if (!isValid) {
11
// Reject: return 401 with the WWW-Authenticate header and stop processing.
12
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
13
response.setHeader("WWW-Authenticate", wwwAuthenticateHeader);
14
return;
15
}
16
// Token is valid — continue handling the request.
17
18
// Prefer enforcing scopes per tool-call rather than in this global check: read the
19
// token's "scope" claim via validateAccessTokenAndGetClaims(token, options) inside
20
// each tool and verify it contains the scope that tool needs.
```
#### Complete middleware implementation [Section titled “Complete middleware implementation”](#complete-middleware-implementation) Combine token extraction and validation into a complete authentication middleware that protects all your MCP endpoints. * Node.js
```javascript
import { Scalekit } from '@scalekit-sdk/node';
import { NextFunction, Request, Response } from 'express';
const scalekit = new Scalekit(
process.env.SCALEKIT_ENVIRONMENT_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const RESOURCE_ID = 'https://your-mcp-server.com'; // If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard.
const METADATA_ENDPOINT = 'https://your-mcp-server.com/.well-known/oauth-protected-resource';
export const WWWHeader = {
HeaderKey: 'WWW-Authenticate',
HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"`
};
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
try {
// Security: Allow public access to well-known endpoints for metadata discovery
// This enables MCP clients to discover your OAuth configuration
if (req.path.includes('.well-known')) {
return next();
}
// Extract Bearer token from Authorization header
const authHeader = req.headers['authorization'];
const token = authHeader?.startsWith('Bearer ')
? authHeader.split('Bearer ')[1]?.trim()
: null;
if (!token) {
throw new Error('Missing or invalid Bearer token');
}
// Security: Validate token against configured resource audience
await scalekit.validateToken(token, {
audience: [RESOURCE_ID]
});
next();
} catch (err) {
// Return proper OAuth 2.0 error response with WWW-Authenticate header
return res
.status(401)
.set(WWWHeader.HeaderKey, WWWHeader.HeaderValue)
.end();
}
}
// Apply authentication middleware to all MCP endpoints
app.use('/', authMiddleware);
```
* Python
```python
from scalekit import ScalekitClient
from scalekit.common.scalekit import TokenValidationOptions
from fastapi import Request, HTTPException, status
from fastapi.responses import Response
import os
scalekit_client = ScalekitClient(
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
)
RESOURCE_ID = "https://your-mcp-server.com" # If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard.
METADATA_ENDPOINT = "https://your-mcp-server.com/.well-known/oauth-protected-resource"
# WWW-Authenticate header for unauthorized responses
WWW_HEADER = {
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"'
}
async def auth_middleware(request: Request, call_next):
# Security: Allow public access to well-known endpoints for metadata discovery
if request.url.path.startswith("/.well-known"):
return await call_next(request)
# Extract Bearer token from Authorization header
auth_header = request.headers.get("Authorization", "")
token = None
if auth_header.startswith("Bearer "):
token = auth_header.split("Bearer ")[1].strip()
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers=WWW_HEADER
)
# Security: Validate token against configured resource audience
try:
options = TokenValidationOptions(
issuer=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
audience=[RESOURCE_ID]
)
scalekit_client.validate_token(token, options=options)
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers=WWW_HEADER
)
return await call_next(request)
# Apply authentication middleware to all MCP endpoints
app.middleware("http")(auth_middleware)
```
5. ## Implement scope-based tool authorization Optional [Section titled “Implement scope-based tool authorization ”](#implement-scope-based-tool-authorization-) Add scope validation at the MCP tool execution level to ensure tools are only executed when the user has authorized the MCP client with the required permissions. This provides fine-grained access control and follows the principle of least privilege. * Node.js
```diff
1
// Security: Validate token has required scope for this specific tool execution
2
// Use case: Ensure users only have access to authorized MCP tools
3
try {
4
await scalekit.validateToken(
5
token, {
6
audience: [RESOURCE_ID],
7
requiredScopes: [scope]
8
}
9
);
10
} catch(error) {
11
// Return OAuth 2.0 compliant error for insufficient scope
12
return res.status(403).json({
13
error: 'insufficient_scope',
14
error_description: `Required scope: ${scope}`,
15
scope: scope
16
});
17
}
```
* Python
```diff
1
# Security: Validate token has required scope for this specific tool execution
2
# Use case: Ensure users only have access to authorized MCP tools
3
try:
4
scalekit_client.validate_access_token(
5
token,
6
options=TokenValidationOptions(
7
audience=[RESOURCE_ID],
8
+required_scopes=[scope]
9
)
10
)
11
except ScalekitValidateTokenFailureException as ex:
12
# Return OAuth 2.0 compliant error for insufficient scope
13
return {
14
"error": "insufficient_scope",
15
"error_description": f"Required scope: {scope}",
16
"scope": scope
17
}
```
6. ## Enable additional authentication methods [Section titled “Enable additional authentication methods”](#enable-additional-authentication-methods) Beyond the OAuth 2.1 authorization you’ve implemented, you can enable additional authentication methods that work seamlessly with your MCP server’s token validation: **[Enterprise SSO](/mcp/auth-methods/enterprise/)** Enable organizations to authenticate through their identity providers (Okta, Azure AD, Google Workspace). Your MCP server continues validating tokens the same way, while Scalekit handles: * Centralized access control through existing enterprise identity systems * Single sign-on experience for organization members * Compliance with corporate security policies **[Social logins](/mcp/auth-methods/social/)** Allow users to authenticate via Google, GitHub, Microsoft, and other social providers. Your existing token validation logic remains unchanged while providing: * Quick onboarding for individual users * Familiar authentication experience * Reduced friction for personal and small team use cases These authentication methods require no changes to your MCP server implementation—you continue validating tokens exactly as shown in the previous steps. **[Bring your own auth](/mcp/auth-methods/custom-auth/)** allows you to use your own authentication system to authenticate users to your MCP server. Your MCP server now has production-ready OAuth 2.1 authorization! You’ve successfully implemented a secure authorization flow that protects your MCP tools and ensures only authenticated users can access them through AI hosts. **Try the demo**: Download and run our [sample MCP server](https://github.com/scalekit-inc/mcp-auth-demos) with authentication already configured to see the complete integration in action. In summary, **Scalekit OAuth authorization server** Acts as the identity provider for your MCP server. * 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 business logic of executing the actual tool calls.
---
# DOCUMENT BOUNDARY
---
# MCP Servers - Additional Reading
> Explore advanced topics for MCP servers, including OAuth 2.1 flows, scope design, dynamic client registration, and security best practices.
MCP Clients that want to get authorized to access your MCP Server need to follow either of the below OAuth 2.1 Flows Supported by Scalekit. ## OAuth 2.1 Flows Supported [Section titled “OAuth 2.1 Flows Supported”](#oauth-21-flows-supported) ### Authorization Code Flow [Section titled “Authorization Code Flow”](#authorization-code-flow) Ideal when an AI agent or MCP Client acts on behalf of a human user:
```javascript
1
// Step 1: Redirect user to authorization server
2
const authURL = new URL('https://your-org.scalekit.com/oauth/authorize');
3
authURL.searchParams.set('response_type', 'code');
4
authURL.searchParams.set('client_id', 'your-client-id');
5
authURL.searchParams.set('redirect_uri', 'https://your-app.com/callback');
6
authURL.searchParams.set('scope', 'mcp:tools:calendar:read mcp:tools:email:send');
7
authURL.searchParams.set('state', generateSecureRandomString());
8
authURL.searchParams.set('code_challenge', generatePKCEChallenge());
9
authURL.searchParams.set('code_challenge_method', 'S256');
10
11
// Step 2: Handle callback and exchange code for token
12
app.get('/callback', async (req, res) => {
13
const { code, state } = req.query;
14
15
// Verify state parameter to prevent CSRF
16
if (!isValidState(state)) {
17
return res.status(400).json({ error: 'Invalid state parameter' });
18
}
19
20
const tokenResponse = await fetch('https://your-org.scalekit.com/oauth/token', {
21
method: 'POST',
22
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
23
body: new URLSearchParams({
24
grant_type: 'authorization_code',
25
code,
26
client_id: 'your-client-id',
27
redirect_uri: 'https://your-app.com/callback',
28
code_verifier: getPKCEVerifier() // From PKCE challenge generation
29
})
30
});
31
32
const tokens = await tokenResponse.json();
33
// Store tokens securely and proceed with MCP calls
34
});
```
### Client Credentials Flow [Section titled “Client Credentials Flow”](#client-credentials-flow) Perfect for automated agents that don’t represent a specific user but want to access your MCP Server on their own behalf. This is typically used for Machine-to-Machine (M2M) authentication.
```javascript
1
const getMachineToken = async () => {
2
const response = await fetch('https://your-org.scalekit.com/oauth/token', {
3
method: 'POST',
4
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
5
body: new URLSearchParams({
6
grant_type: 'client_credentials',
7
client_id: 'your-service-client-id',
8
client_secret: 'your-service-client-secret',
9
scope: 'mcp:tools:inventory:check mcp:resources:store-data',
10
audience: 'https://your-mcp-server.com',
11
})
12
});
13
14
return await response.json();
15
};
```
## Scope Design Best Practices [Section titled “Scope Design Best Practices”](#scope-design-best-practices) Design OAuth scopes that reflect your MCP server’s actual capabilities and security requirements: ### Hierarchical Scopes [Section titled “Hierarchical Scopes”](#hierarchical-scopes)
```javascript
1
// Resource-based scopes
2
'mcp:resources:customer-data:read' // Read customer data
3
'mcp:resources:customer-data:write' // Modify customer data
4
'mcp:resources:*' // All resources (admin-level)
5
6
// Tool-based scopes
7
'mcp:tools:weather' // Weather API access
8
'mcp:tools:calendar:read' // Read calendar events
9
'mcp:tools:calendar:write' // Create/modify calendar events
10
'mcp:tools:email:send' // Send emails
11
'mcp:tools:*' // All tools access
12
13
// Action-based scopes
14
'mcp:exec:workflows:risk-assessment' // Execute risk assessment workflow
15
'mcp:exec:functions:data-analysis' // Run data analysis functions
```
### Scope Validation Helpers [Section titled “Scope Validation Helpers”](#scope-validation-helpers)
```javascript
1
const ScopeValidator = {
2
hasScope: (userScopes, requiredScope) => {
3
return userScopes.includes(requiredScope) ||
4
userScopes.includes(requiredScope.split(':').slice(0, -1).join(':') + ':*');
5
},
6
7
hasAnyScope: (userScopes, allowedScopes) => {
8
return allowedScopes.some(scope => ScopeValidator.hasScope(userScopes, scope));
9
},
10
11
validateToolAccess: (userScopes, toolName) => {
12
const toolScope = `mcp:tools:${toolName}`;
13
const wildcardScope = 'mcp:tools:*';
14
return userScopes.includes(toolScope) || userScopes.includes(wildcardScope);
15
}
16
};
17
18
// Usage in MCP tool handlers
19
app.post('/mcp/tools/:toolName', (req, res) => {
20
const { toolName } = req.params;
21
const userScopes = req.auth.scopes;
22
23
if (!ScopeValidator.validateToolAccess(userScopes, toolName)) {
24
return res.status(403).json({
25
error: 'insufficient_scope',
26
error_description: `Access to tool '${toolName}' requires appropriate scope`
27
});
28
}
29
30
// Process tool request
31
});
```
## Dynamic Client Registration [Section titled “Dynamic Client Registration”](#dynamic-client-registration) Scalekit supports Dynamic Client Registration (DCR) to enable seamless integration for new MCP clients that want to connect to your MCP Server. MCP clients can auto-register using DCR:
```javascript
1
// MCP clients can auto-register using DCR
2
const registerClient = async (clientMetadata) => {
3
const response = await fetch('https://your-org.scalekit.com/resource-server/oauth/register', {
4
method: 'POST',
5
headers: { 'Content-Type': 'application/json' },
6
body: JSON.stringify({
7
client_name: 'AI Sales Assistant',
8
client_uri: 'https://sales-ai.company.com',
9
redirect_uris: ['https://sales-ai.company.com/oauth/callback'],
10
grant_types: ['authorization_code', 'refresh_token'],
11
response_types: ['code'],
12
scope: 'mcp:tools:crm:read mcp:tools:email:send',
13
audience: 'https://your-mcp-server.com',
14
token_endpoint_auth_method: 'client_secret_basic',
15
...clientMetadata
16
})
17
});
18
19
return await response.json();
20
// Returns: { client_id, client_secret, client_id_issued_at, ... }
21
};
```
## Security Implementation [Section titled “Security Implementation”](#security-implementation) ### Rate Limiting by Client [Section titled “Rate Limiting by Client”](#rate-limiting-by-client) Implement client-specific rate limits:
```javascript
1
import rateLimit from 'express-rate-limit';
2
3
const createClientRateLimit = () => {
4
return rateLimit({
5
windowMs: 15 * 60 * 1000, // 15 minutes
6
limit: (req) => {
7
// Different limits based on client type or scopes
8
const scopes = req.auth?.scopes || [];
9
if (scopes.includes('mcp:tools:*')) return 1000; // Premium client
10
if (scopes.includes('mcp:tools:basic')) return 100; // Basic client
11
return 50; // Default limit
12
},
13
keyGenerator: (req) => req.auth?.clientId || req.ip,
14
message: {
15
error: 'rate_limit_exceeded',
16
error_description: 'Too many requests from this client'
17
}
18
});
19
};
20
21
app.use('/mcp', createClientRateLimit());
```
### Comprehensive Logging [Section titled “Comprehensive Logging”](#comprehensive-logging) Track all OAuth and MCP interactions:
```javascript
1
const auditLogger = {
2
logTokenRequest: (clientId, grantType, scopes, success) => {
3
console.log(JSON.stringify({
4
event: 'oauth_token_request',
5
timestamp: new Date().toISOString(),
6
client_id: clientId,
7
grant_type: grantType,
8
requested_scopes: scopes,
9
success
10
}));
11
},
12
13
logMCPAccess: (req, toolName, success, error = null) => {
14
console.log(JSON.stringify({
15
event: 'mcp_tool_access',
16
timestamp: new Date().toISOString(),
17
user_id: req.auth?.userId,
18
client_id: req.auth?.clientId,
19
tool_name: toolName,
20
scopes: req.auth?.scopes,
21
success,
22
error: error?.message,
23
ip_address: req.ip,
24
user_agent: req.get('User-Agent')
25
}));
26
}
27
};
28
29
// Use in your MCP handlers
30
app.post('/mcp/tools/:toolName', async (req, res) => {
31
const { toolName } = req.params;
32
33
try {
34
// Process tool request
35
const result = await processToolRequest(toolName, req.body);
36
37
auditLogger.logMCPAccess(req, toolName, true);
38
res.json(result);
39
} catch (error) {
40
auditLogger.logMCPAccess(req, toolName, false, error);
41
res.status(500).json({ error: 'Tool execution failed' });
42
}
43
});
```
### Health Check Endpoints [Section titled “Health Check Endpoints”](#health-check-endpoints) Monitor your MCP server and authorization integration:
```javascript
1
app.get('/health', async (req, res) => {
2
const health = {
3
status: 'healthy',
4
timestamp: new Date().toISOString(),
5
services: {
6
mcp_server: 'healthy',
7
oauth_server: 'unknown'
8
}
9
};
10
11
try {
12
// Test OAuth server connectivity
13
const oauthTest = await fetch('https://your-org.scalekit.com/.well-known/oauth-authorization-server');
14
health.services.oauth_server = oauthTest.ok ? 'healthy' : 'degraded';
15
} catch (error) {
16
health.services.oauth_server = 'unhealthy';
17
health.status = 'degraded';
18
}
19
20
const statusCode = health.status === 'healthy' ? 200 : 503;
21
res.status(statusCode).json(health);
22
});
```
## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Common Issues and Solutions [Section titled “Common Issues and Solutions”](#common-issues-and-solutions) **Token Validation Failures**
```javascript
1
// Debug token validation issues
2
const debugTokenValidation = async (token) => {
3
try {
4
// Check token structure
5
const [header, payload, signature] = token.split('.');
6
console.log('Token Header:', JSON.parse(atob(header)));
7
console.log('Token Payload:', JSON.parse(atob(payload)));
8
9
// Validate with detailed error info
10
await jwtVerify(token, JWKS, {
11
issuer: 'https://your-org.scalekit.com',
12
audience: 'https://your-mcp-server.com'
13
});
14
} catch (error) {
15
console.error('Token validation error:', {
16
name: error.name,
17
message: error.message,
18
code: error.code
19
});
20
}
21
};
```
**CORS Issues with Authorization Server**
```javascript
1
// Configure CORS for OAuth endpoints
2
app.use('/oauth', cors({
3
origin: 'https://your-org.scalekit.com',
4
credentials: true,
5
methods: ['GET', 'POST', 'OPTIONS'],
6
allowedHeaders: ['Authorization', 'Content-Type', 'MCP-Protocol-Version']
7
}));
```
**Scope Permission Debugging**
```javascript
1
const debugScopes = (req, res, next) => {
2
console.log('Request Scopes:', {
3
user_scopes: req.auth?.scopes,
4
required_scope: req.requiredScope,
5
has_permission: req.auth?.scopes?.includes(req.requiredScope)
6
});
7
next();
8
};
```
### Error Response Standards [Section titled “Error Response Standards”](#error-response-standards) Follow OAuth 2.1 and MCP error response formats:
```javascript
1
const sendOAuthError = (res, error, description, statusCode = 400) => {
2
res.status(statusCode).json({
3
error,
4
error_description: description,
5
error_uri: 'https://your-mcp-server.com/docs/errors'
6
});
7
};
8
9
// Usage examples
10
app.use((error, req, res, next) => {
11
if (error.name === 'TokenExpiredError') {
12
return sendOAuthError(res, 'invalid_token', 'Access token has expired', 401);
13
}
14
15
if (error.name === 'InsufficientScopeError') {
16
return sendOAuthError(res, 'insufficient_scope', `Required scope: ${error.requiredScope}`, 403);
17
}
18
19
// Default error
20
sendOAuthError(res, 'server_error', 'An unexpected error occurred', 500);
21
});
```
## Advanced Configuration [Section titled “Advanced Configuration”](#advanced-configuration) ### Custom Scope Mapping [Section titled “Custom Scope Mapping”](#custom-scope-mapping) Map OAuth scopes to internal permissions:
```javascript
1
const scopePermissionMap = {
2
'mcp:tools:weather': ['weather:read'],
3
'mcp:tools:calendar:read': ['calendar:events:read'],
4
'mcp:tools:calendar:write': ['calendar:events:read', 'calendar:events:write'],
5
'mcp:tools:email:send': ['email:send', 'contacts:read'],
6
'mcp:resources:customer-data': ['customers:read', 'customers:write']
7
};
8
9
const getPermissionsFromScopes = (scopes) => {
10
const permissions = new Set();
11
scopes.forEach(scope => {
12
const scopePermissions = scopePermissionMap[scope] || [];
13
scopePermissions.forEach(permission => permissions.add(permission));
14
});
15
return Array.from(permissions);
16
};
```
### Refresh Token Management [Section titled “Refresh Token Management”](#refresh-token-management) Handle token refresh for long-running agents:
```javascript
1
const TokenManager = {
2
async refreshToken(refreshToken) {
3
const response = await fetch('https://your-org.scalekit.com/oauth2/token', {
4
method: 'POST',
5
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
6
body: new URLSearchParams({
7
grant_type: 'refresh_token',
8
refresh_token: refreshToken,
9
client_id: 'your-client-id',
10
client_secret: 'your-client-secret'
11
})
12
});
13
14
return await response.json();
15
},
16
17
async autoRefreshWrapper(tokenStore, makeRequest) {
18
try {
19
return await makeRequest(tokenStore.accessToken);
20
} catch (error) {
21
if (error.status === 401) {
22
// Token expired, try refresh
23
const newTokens = await this.refreshToken(tokenStore.refreshToken);
24
tokenStore.accessToken = newTokens.access_token;
25
tokenStore.refreshToken = newTokens.refresh_token;
26
27
// Retry original request
28
return await makeRequest(tokenStore.accessToken);
29
}
30
throw error;
31
}
32
}
33
};
```
---
# DOCUMENT BOUNDARY
---
# MCP authentication patterns
> Authentication patterns: Human users via OAuth Authorization Code flow, autonomous agents via Client Credentials flow, and downstream integrations using API keys, OAuth, or token cascading
Scalekit provides secure authentication for MCP servers across three distinct patterns, each corresponding to different interaction models and trust boundaries. Understanding which pattern applies to your use case ensures you implement the right security model for your MCP server architecture. This guide covers all three authentication patterns: human-to-MCP interactions, agent-to-MCP communication, and MCP-to-downstream integrations. Each pattern uses different OAuth 2.1 flows and has specific configuration requirements explained with sequence diagrams and practical guidance. ## Pattern comparison [Section titled “Pattern comparison”](#pattern-comparison) Understanding the differences between these patterns helps you choose the right approach for your architecture. Each pattern serves specific use cases and has different security characteristics. | Aspect | Human → MCP | Agent/Machine → MCP | MCP → Downstream | | -------------------- | ---------------------------------------------- | -------------------------------------- | -------------------------------------- | | **Actor** | Human using AI host (Claude, ChatGPT, VS Code) | Autonomous agent or service | MCP Server making backend calls | | **OAuth Flow** | Authorization Code | Client Credentials | Varies by sub-pattern | | **Initiator** | User interaction in MCP client | Programmatic request | MCP server implementation code | | **Token Lifetime** | Medium (typically hours) | Configurable (typically long-lived) | Depends on downstream system | | **User Consent** | Required during authorization flow | Not applicable (pre-configured) | Not applicable | | **Scope Assignment** | During consent prompt | At client registration | At implementation time | | **Best For** | Interactive human workflows | Scheduled tasks, autonomous operations | Backend integration with APIs/services | | **Complexity** | Medium (handles browser flow) | Low (direct token request) | Varies (simple to complex) | ## Pattern 1: Human interacting with MCP server [Section titled “Pattern 1: Human interacting with MCP server”](#pattern-1-human-interacting-with-mcp-server) When a human uses a compliant MCP host application, that host acts as the OAuth client. It initiates authorization with the Scalekit Authorization Server, obtains a scoped access token, and interacts securely with the MCP Server on behalf of the user. This pattern represents the most common interaction model for real-world MCP use cases - humans interacting with an MCP server through AI host applications like Claude Desktop, VS Code, Cursor, or Windsurf, while Scalekit ensures tokens are valid, scoped, and auditable. ### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence) ### How it works [Section titled “How it works”](#how-it-works) 1. **Initiation** – The human configures an MCP server in their MCP client application. 2. **Challenge** – The MCP Server responds with an HTTP `401` containing a `WWW-Authenticate` header that points to the Scalekit Authorization Server. 3. **Authorization Flow** – The MCP Client opens the user’s browser to initiate the OAuth 2.1 authorization flow. During this step, the Scalekit Authorization Server handles user authentication through Magic Link & OTP, Passkeys, Social login providers (like Google, GitHub, or LinkedIn), or Enterprise SSO integrations (such as Okta, Microsoft Entra ID, or ADFS). The user is then prompted to grant consent for the requested scopes. Once approved, Scalekit returns an authorization code, which the MCP Client exchanges for an access token. 4. **Token Issuance** – Scalekit issues an OAuth 2.1 access token containing claims and scopes (for example, `todo:read`, `calendar:write`) that represent the user’s permissions. 5. **Authorized Request** – The client calls the MCP Server again, now attaching the Bearer token in the `Authorization` header. 6. **Validation and Execution** – The MCP Server validates the token issued by Scalekit and executes the requested tool. ### Implementation [Section titled “Implementation”](#implementation) #### 1. Register your MCP server in the Scalekit Dashboard [Section titled “1. Register your MCP server in the Scalekit Dashboard”](#1-register-your-mcp-server-in-the-scalekit-dashboard) Create a new MCP server in the Scalekit Dashboard to obtain your server credentials and configure authentication settings. #### 2. Implement the protected resource metadata endpoint [Section titled “2. Implement the protected resource metadata endpoint”](#2-implement-the-protected-resource-metadata-endpoint) Add a `.well-known/oauth-protected-resource` endpoint that provides your MCP server’s authentication configuration to clients. #### 3. Configure scopes for your server capabilities [Section titled “3. Configure scopes for your server capabilities”](#3-configure-scopes-for-your-server-capabilities) Define OAuth scopes that correspond to the tools and permissions your MCP server exposes. #### 4. Set up token validation middleware [Section titled “4. Set up token validation middleware”](#4-set-up-token-validation-middleware) Implement middleware to validate incoming JWT tokens from Scalekit before processing MCP tool requests. #### 5. Test the complete authentication flow [Section titled “5. Test the complete authentication flow”](#5-test-the-complete-authentication-flow) Verify the end-to-end flow works with an MCP client to ensure secure authentication. For complete implementation guidance, see the [MCP OAuth 2.1 quickstart](/authenticate/mcp/quickstart/) or framework-specific guides for [FastMCP](/authenticate/mcp/fastmcp-quickstart/), [FastAPI + FastMCP](/authenticate/mcp/fastapi-fastmcp-quickstart/), and [Express.js](/authenticate/mcp/expressjs-quickstart/). ## Pattern 2: Agent / machine interacting with MCP server [Section titled “Pattern 2: Agent / machine interacting with MCP server”](#pattern-2-agent--machine-interacting-with-mcp-server) An autonomous agent or any machine-to-machine process can directly interact with an MCP Server secured by Scalekit. In this model, the agent acts as a confidential OAuth client, authenticated using a `client_id` and `client_secret` issued by Scalekit. This pattern uses the OAuth 2.1 Client Credentials flow, allowing the agent to obtain an access token without user interaction. Tokens are scoped and time-bound, ensuring secure and auditable automation between services. ### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-1) ### Client registration [Section titled “Client registration”](#client-registration) #### 1. Navigate to the MCP Server Clients tab [Section titled “1. Navigate to the MCP Server Clients tab”](#1-navigate-to-the-mcp-server-clients-tab) Go to **[Dashboard](https://app.scalekit.com) > MCP Servers** and select your MCP Server. Click on the **Clients** tab.  #### 2. Create a new M2M client [Section titled “2. Create a new M2M client”](#2-create-a-new-m2m-client) Click **Create Client** to start the client creation process.  #### 3. Copy your client credentials [Section titled “3. Copy your client credentials”](#3-copy-your-client-credentials) Copy the **client\_id** and **client\_secret** immediately - the secret will not be shown again for security reasons. Store these securely in your agent’s configuration.  #### 4. Configure client scopes [Section titled “4. Configure client scopes”](#4-configure-client-scopes) Optionally, set scopes (e.g., `todo:read`, `todo:write`) that correspond to the permissions configured for your MCP Server. Click **Save** to complete the setup. ### Requesting an access token [Section titled “Requesting an access token”](#requesting-an-access-token) Once you have the client credentials, the agent can request a token directly from the Scalekit Authorization Server: Request access token
```bash
1
curl --location '{{env_url}}/oauth/token' \
2
--header 'Content-Type: application/x-www-form-urlencoded' \
3
--data-urlencode 'grant_type=client_credentials' \
4
--data-urlencode 'client_id={{client_id}}' \
5
--data-urlencode 'client_secret={{secret_value}}' \
6
--data-urlencode 'scope=todo:read todo:write'
```
Scalekit responds with a JSON payload containing the access token: Token response
```json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "todo:read todo:write"
}
```
Use the `access_token` in the `Authorization` header when calling your MCP Server’s endpoints. ### Implementation [Section titled “Implementation”](#implementation-1) #### 1. Create an M2M client for your target MCP server [Section titled “1. Create an M2M client for your target MCP server”](#1-create-an-m2m-client-for-your-target-mcp-server) Use the Scalekit Dashboard to create a Machine-to-Machine client for the MCP server you want to authenticate with. #### 2. Store client credentials securely [Section titled “2. Store client credentials securely”](#2-store-client-credentials-securely) Store the `client_id` and `client_secret` using environment variables or a secrets manager. Never hardcode credentials in your agent code. #### 3. Implement token requests in your agent [Section titled “3. Implement token requests in your agent”](#3-implement-token-requests-in-your-agent) Before making MCP calls, request access tokens using the OAuth 2.1 Client Credentials flow from the Scalekit Authorization Server. #### 4. Add token caching and refresh logic [Section titled “4. Add token caching and refresh logic”](#4-add-token-caching-and-refresh-logic) Implement caching to store tokens until they expire, and refresh them automatically to maintain uninterrupted service. #### 5. Attach tokens to MCP tool requests [Section titled “5. Attach tokens to MCP tool requests”](#5-attach-tokens-to-mcp-tool-requests) Include the access token as a Bearer token in the `Authorization` header when calling MCP server tools. For hands-on experience, use the FastMCP Todo Server from the [FastMCP quickstart](/authenticate/mcp/fastmcp-quickstart/). Create an M2M client and run your token request programmatically within your agent code. ## Pattern 3: MCP server integrating with downstream systems [Section titled “Pattern 3: MCP server integrating with downstream systems”](#pattern-3-mcp-server-integrating-with-downstream-systems) In real-world scenarios, an MCP Server often needs to make backend calls - to your own APIs, to another MCP Server, or to external APIs such as CRM, ticketing, or SaaS tools. This section explains three secure ways to perform these downstream integrations, each corresponding to a different trust boundary and authorization pattern. ### Sub-pattern 3a: Using API keys or custom tokens [Section titled “Sub-pattern 3a: Using API keys or custom tokens”](#sub-pattern-3a-using-api-keys-or-custom-tokens) Your MCP Server can communicate with internal or external backend systems that have their own authorization servers or API key-based access. In this setup, the MCP Server manages its own credentials securely (for example, in environment variables, a vault, or secrets manager) and injects them when making downstream calls. #### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-2) #### When to use this pattern [Section titled “When to use this pattern”](#when-to-use-this-pattern) * External APIs have their own authentication (AWS, Stripe, Twilio, etc.) * Internal systems use proprietary authentication mechanisms * Legacy systems that don’t support OAuth 2.1 * You control credential management and rotation #### Example scenario [Section titled “Example scenario”](#example-scenario) * The MCP Server stores an API key as `EXTERNAL_API_KEY` in environment variables * When a tool (e.g., `get_weather_data`) is called, your MCP server attaches the key in the request headers * The backend API validates the key and responds with data * The MCP Server processes and returns the formatted response to the client ### Sub-pattern 3b: MCP-to-MCP communication [Section titled “Sub-pattern 3b: MCP-to-MCP communication”](#sub-pattern-3b-mcp-to-mcp-communication) If you have two MCP Servers that need to communicate - for example, `crm-mcp` calling tools from `tickets-mcp` - you can follow the same authentication pattern described in **Pattern 2** above. The calling MCP Server (in this case, `crm-mcp`) acts as an autonomous agent, authenticating with the receiving MCP Server via OAuth 2.1 Client Credentials Flow. Once the token is issued by Scalekit, the calling MCP uses it to call tools exposed by the second MCP Server. #### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-3) #### Implementation [Section titled “Implementation”](#implementation-2) The implementation follows Pattern 2 (Agent/Machine → MCP): 1. Create an M2M client for the receiving MCP server in Scalekit 2. Configure the calling MCP server with the client credentials 3. Request tokens using the Client Credentials flow 4. Call the receiving MCP’s tools with the Bearer token For detailed implementation guidance, refer to the [Pattern 2 section](#pattern-2-agent--machine-interacting-with-mcp-server) above. ### Sub-pattern 3c: Cascading the same token [Section titled “Sub-pattern 3c: Cascading the same token”](#sub-pattern-3c-cascading-the-same-token) In some cases, you may want your MCP Server to forward (or “cascade”) the same access token it received from the client - for example, when your backend system lies within the same trust boundary as the Scalekit Authorization Server and can validate the token based on its issuer, scopes, and expiry. #### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-4) #### When to use this pattern [Section titled “When to use this pattern”](#when-to-use-this-pattern-1) Use token cascading when: * Both systems (MCP Server and backend API) trust the same Authorization Server (Scalekit) * The backend API can validate JWTs using public keys or JWKS URL * Scopes and issuer claims (`iss`, `scope`, `exp`) are sufficient to determine access * You need to preserve the original user context across service boundaries Trust boundary consideration Only cascade tokens across services that share the same trust boundary. If your backend API does not validate Scalekit-issued tokens, use a separate service credential or the Client Credentials flow (sub-pattern 3b) instead. #### Implementation requirements [Section titled “Implementation requirements”](#implementation-requirements) For the backend API to validate cascaded tokens: 1. Configure the backend to validate JWT signatures using Scalekit’s public keys 2. Verify the token’s `iss` (issuer) claim matches your Scalekit environment 3. Check the `aud` (audience) claim includes the backend API’s identifier 4. Validate the `exp` (expiration) claim to reject expired tokens 5. Verify required scopes are present in the token’s `scope` claim ## Choosing the right pattern [Section titled “Choosing the right pattern”](#choosing-the-right-pattern) Use this decision guide to select the appropriate authentication pattern for your use case: **For human users accessing MCP tools:** → Use **Pattern 1: Human → MCP** (Authorization Code Flow) **For autonomous agents or scheduled tasks:** → Use **Pattern 2: Agent/Machine → MCP** (Client Credentials Flow) **For MCP server making backend calls:** * External APIs with their own auth → Use **Pattern 3a: API Keys** * Another MCP server you control → Use **Pattern 3b: MCP-to-MCP** (Client Credentials Flow) * Backend within same trust boundary → Use **Pattern 3c: Token Cascading** ## Next steps [Section titled “Next steps”](#next-steps) Now that you understand the authentication patterns, you can: * Follow the [MCP OAuth 2.1 quickstart](/authenticate/mcp/quickstart/) to implement Pattern 1 or Pattern 2 * Explore framework-specific implementations: * [FastMCP quickstart](/authenticate/mcp/fastmcp-quickstart/) for Python with built-in provider * [FastAPI + FastMCP quickstart](/authenticate/mcp/fastapi-fastmcp-quickstart/) for custom Python middleware * [Express.js quickstart](/authenticate/mcp/expressjs-quickstart/) for Node.js/TypeScript servers * Review the [MCP authentication demos](https://github.com/scalekit-inc/mcp-auth-demos) on GitHub for complete working examples
---
# DOCUMENT BOUNDARY
---
# MCP Auth code samples
> MCP Auth authentication examples and patterns
### [Add Auth to Node.js MCP Servers](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node) [Add Scalekit auth to a Node.js MCP server with minimal setup. Includes a working example with user greeting.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node) ### [Add Auth to Python MCP Servers](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python) [Add Scalekit auth to a Python MCP server in minutes. Includes a working example with user greeting.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python) ### [Secure FastMCP Apps with Auth](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp) [Build a secure FastMCP app with Scalekit. Features a complete todo list with protected endpoints and session management.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp)
---
# DOCUMENT BOUNDARY
---
# Bring your own auth into your MCP server
> Federated authentication system with Scalekit's OAuth 2.1 authorization layer for MCP servers
If you already have an authentication system in place, you can use Scalekit as a drop-in OAuth 2.1 authorization layer for your MCP servers. This federated approach allows you to maintain your existing auth infrastructure while adding standards-compliant OAuth 2.1 authorization for MCP clients. **Why use federated authentication?** * **Preserve existing auth**: Keep your current authentication system and user management * **Standards compliance**: Add OAuth 2.1 authorization without rebuilding your auth layer * **Seamless integration**: Users authenticate with your familiar login experience * **Centralized control**: Maintain full control over user authentication and policies When an MCP client initiates authentication, Scalekit acts as a bridge between the MCP client and your existing authentication system. The flow involves redirecting users to your login endpoint, validating their identity, and passing user information back to Scalekit to complete the OAuth 2.1 flow. 1. ## Initiate authentication flow [Section titled “Initiate authentication flow”](#initiate-authentication-flow) When the MCP client starts the authentication flow by calling `/oauth/authorize` on Scalekit, Scalekit redirects the user to your configured login endpoint with two critical parameters: * `login_request_id` string : Unique identifier for this login request * `state` string : OAuth state parameter to maintain security across requests **Example redirect URL:**
```sh
https:///login?login_request_id=&state=
```
2. ## Authenticate the user in your system [Section titled “Authenticate the user in your system”](#authenticate-the-user-in-your-system) When the user lands on your login page, process authentication using your existing logic?whether that’s username/password, SSO, biometric authentication, or any other method your system supports. After successful authentication, make a secure backend-to-backend POST request to Scalekit with the authenticated user’s information. Send user details to Scalekit
```bash
curl --location '/api/v1/connections//auth-requests//user' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ' \
--data-raw '{
"sub": "1234567890",
"email": "alice@example.com",
"given_name": "Alice",
"family_name": "Doe",
"email_verified": true,
"phone_number": "+1234567890",
"phone_number_verified": false,
"name": "Alice Doe",
"preferred_username": "alice.d",
"picture": "https://example.com/avatar.jpg",
"gender": "female",
"locale": "en-US"
}'
```
3. ## Redirect back to Scalekit [Section titled “Redirect back to Scalekit”](#redirect-back-to-scalekit) After receiving a successful response from Scalekit confirming the user details were accepted, redirect the user back to Scalekit’s callback endpoint with the `state` parameter. **Callback URL format:**
```sh
/sso/v1/connections//partner:callback?state=
```
The `state_value` must match the `state` parameter you received in step 1. This ensures the authentication flow’s integrity and prevents CSRF attacks. State validation Always verify that the `state` value you send back matches exactly what you received initially. Mismatched state values should be rejected. 4. ## Complete the OAuth flow [Section titled “Complete the OAuth flow”](#complete-the-oauth-flow) After processing the callback from your authentication system, Scalekit automatically handles the remaining OAuth 2.1 flow steps: * Displays the consent screen to the user (if required) * Generates the authorization code * Handles token exchange requests from the MCP client * Issues access tokens with appropriate scopes The MCP client receives valid OAuth 2.1 tokens and can now access your MCP server with the authenticated user’s identity. Your MCP server now supports federated authentication with your existing auth system
---
# DOCUMENT BOUNDARY
---
# Publish your MCP connector to Claude & ChatGPT
> Give MCP connector directory reviewers a test user and static code so they can authenticate and test your server end to end.
To list your MCP connector in the [Claude connector directory](https://claude.com/docs/connectors/building/submission) or the [ChatGPT app directory](https://developers.openai.com/apps-sdk/deploy/submission#review-and-approval-faqs), a reviewer must sign in to your server and run every tool end to end. Both submission forms have a testing-access step that expects every credential and step needed to reach a fully-populated account. Scalekit’s [Test Users](/authenticate/run-e2e-tests/) feature makes this possible without handing over a real inbox: allowlist a reviewer email and set a **static 6-digit code**, and the reviewer signs in through Scalekit’s hosted login using that code. This page shows how to prepare the credential and write the instructions to paste into the submission form. Works only with Scalekit-managed auth Test Users works only when **Scalekit manages your app’s login, sessions, and user management** (Full Stack Auth / SaaSKit), because it intercepts Scalekit’s own Magic Link/OTP delivery. It does **not** work when your MCP server runs in [Bring Your Own Auth](/mcp/auth-methods/custom-auth/) mode. In that mode Scalekit federates to your own identity provider and never owns the login screen or the OTP, so there is no static code to hand a reviewer. For Bring Your Own Auth, create a reviewer account directly in your identity provider and share those credentials instead. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * An MCP connector already protected with Scalekit MCP Auth using Scalekit-managed auth. See the [MCP Auth quickstart](/authenticate/mcp/quickstart/). * The **Magic Link & OTP** auth method enabled, with delivery set to `Verification Code` or `Magic Link + Verification Code`. `Magic Link`-only delivery is not supported. See the [Test Users prerequisite](/authenticate/run-e2e-tests/#prerequisites). ## How reviewer sign-in works [Section titled “How reviewer sign-in works”](#how-reviewer-sign-in-works) The reviewer follows the same OAuth 2.1 flow as any [human interacting with your MCP server](/authenticate/mcp/overview/#the-authorization-flow-in-practice): 1. The MCP client (MCP Inspector or Claude as a custom connector) calls your protected server and receives a `401` pointing to Scalekit. 2. The client redirects the reviewer to Scalekit’s hosted login. 3. The reviewer enters the **test email** and the **static code** you configured — no inbox required. 4. The reviewer approves the consent screen, Scalekit issues an access token, and the client runs your tools. ## Prepare the reviewer credential [Section titled “Prepare the reviewer credential”](#prepare-the-reviewer-credential) 1. ## Enable Test Users and add a reviewer email [Section titled “Enable Test Users and add a reviewer email”](#enable-test-users-and-add-a-reviewer-email) In **Dashboard > Environment settings > Test users**, toggle **Enable test users** and add a reviewer address. The email must contain `+sktest` in the local part — for example `reviewer+sktest@yourapp.com`. Set the **Static confirmation code** (the default is `424242`). For the full field-by-field reference, see [Configure test users](/authenticate/run-e2e-tests/#configure-test-users). 2. ## Create the reviewer inside a fully-populated organization [Section titled “Create the reviewer inside a fully-populated organization”](#create-the-reviewer-inside-a-fully-populated-organization) Reviewers expect your tools to return real data, so add the reviewer user to an organization that already has representative content. Create the user with `sendInvitationEmail: false` so no email goes out, then confirm the same email is on the Test Users allowlist. * Node.js Create the reviewer user
```ts
1
import { ScalekitClient } from '@scalekit-sdk/node'
2
3
const scalekit = new ScalekitClient(
4
process.env.SCALEKIT_ENVIRONMENT_URL!,
5
process.env.SCALEKIT_CLIENT_ID!,
6
process.env.SCALEKIT_CLIENT_SECRET!
7
)
8
9
try {
10
// Add the reviewer to a populated org so tools return real data during review.
11
const { user } = await scalekit.user.createUserAndMembership(orgId, {
12
email: 'reviewer+sktest@yourapp.com',
13
sendInvitationEmail: false,
14
})
15
} catch (error) {
16
console.error('Failed to create reviewer user:', error)
17
}
```
* Python Create the reviewer user
```python
1
import os
2
from scalekit import ScalekitClient
3
from scalekit.v1.users.users_pb2 import CreateUser
4
5
scalekit_client = ScalekitClient(
6
env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
7
client_id=os.environ["SCALEKIT_CLIENT_ID"],
8
client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
9
)
10
11
try:
12
# Add the reviewer to a populated org so tools return real data during review.
13
user_response = scalekit_client.users.create_user_and_membership(
14
org_id,
15
CreateUser(email="reviewer+sktest@yourapp.com"),
16
send_invitation_email=False,
17
)
18
except Exception as error:
19
print(f"Failed to create reviewer user: {error}")
```
* Go Create the reviewer user
```go
1
import usersv1 "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/users"
2
3
scalekitClient := scalekit.NewScalekitClient(
4
os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
5
os.Getenv("SCALEKIT_CLIENT_ID"),
6
os.Getenv("SCALEKIT_CLIENT_SECRET"),
7
)
8
9
// Add the reviewer to a populated org so tools return real data during review.
10
userResp, err := scalekitClient.User().CreateUserAndMembership(ctx, orgID,
11
&usersv1.CreateUser{Email: "reviewer+sktest@yourapp.com"},
12
false,
13
)
14
if err != nil {
15
log.Fatalf("failed to create reviewer user: %v", err)
16
}
```
* Java Create the reviewer user
```java
1
import com.scalekit.grpc.scalekit.v1.users.*;
2
3
ScalekitClient scalekitClient = new ScalekitClient(
4
System.getenv("SCALEKIT_ENVIRONMENT_URL"),
5
System.getenv("SCALEKIT_CLIENT_ID"),
6
System.getenv("SCALEKIT_CLIENT_SECRET")
7
);
8
9
try {
10
// Add the reviewer to a populated org so tools return real data during review.
11
CreateUserAndMembershipRequest request = CreateUserAndMembershipRequest.newBuilder()
12
.setUser(CreateUser.newBuilder().setEmail("reviewer+sktest@yourapp.com").build())
13
.setSendInvitationEmail(false)
14
.build();
15
16
CreateUserAndMembershipResponse userResp = scalekitClient.users()
17
.createUserAndMembership(orgId, request);
18
} catch (Exception error) {
19
System.err.println("Failed to create reviewer user: " + error.getMessage());
20
}
```
## Verify [Section titled “Verify”](#verify) Connect to your server the same way the reviewer will — via [MCP Inspector](https://github.com/modelcontextprotocol/inspector) or as a custom connector in Claude — and sign in with the reviewer email and static code. Verification succeeds when: * Sign-in completes with the static code and no real verification email is sent. * Every tool runs successfully and returns representative data from the populated organization. Fix any gaps before you submit to a directory. ## Write the test setup instructions [Section titled “Write the test setup instructions”](#write-the-test-setup-instructions) Paste instructions into the submission form’s testing-access field — the **Test setup instructions** box on Claude’s **Test & launch** step, or the equivalent testing-instructions field in the ChatGPT app submission. Give the reviewer everything needed to authenticate autonomously: Test setup instructions (paste-ready)
```text
Connector URL: https://mcp.yourapp.com/mcp
Authentication: OAuth 2.0 (hosted login). When prompted to connect, use the
test account below — no email inbox is required.
Email: reviewer+sktest@yourapp.com
Verification code: 424242
Steps:
1. Add the connector and start the OAuth sign-in.
2. On the login screen, enter the email above and continue.
3. At the verification-code prompt, enter 424242 and continue.
4. Approve the consent screen.
5. The account belongs to a fully-populated organization, so every tool
returns representative data.
```
Replace the URL, email, and code with your own values. Keep the code identical to the **Static confirmation code** configured in the dashboard. ## Next steps [Section titled “Next steps”](#next-steps) Once your connector is approved, remove the reviewer email from the allowlist or rotate the static code — see [Configure test users](/authenticate/run-e2e-tests/#configure-test-users) for how to manage the allowlist. ## Common scenarios [Section titled “Common scenarios”](#common-scenarios)
---
# DOCUMENT BOUNDARY
---
# Express.js quickstart
> Build a production-ready Express.js MCP server with TypeScript, custom middleware for OAuth token validation, and Scalekit authentication.
This guide shows you how to build a production-ready Express.js MCP server with TypeScript and Scalekit’s OAuth authentication. You’ll implement custom middleware for token validation, expose OAuth resource metadata for client discovery, and create MCP tools that enforce authorization using the MCP SDK. Use this quickstart when you’re building Node.js-based MCP servers and want fine-grained control over request handling. The Express integration gives you flexibility to add custom routes, middleware chains, integrate with existing Express applications, and handle complex authorization requirements. The full code is available on [GitHub](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node). **Prerequisites** * A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers * **Node.js 20+** installed locally * Familiarity with Express.js, TypeScript, and OAuth token validation * Basic understanding of MCP server architecture 1. ## Register your MCP server in Scalekit [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit) Create a protected resource entry so Scalekit can issue tokens that your custom Express middleware validates. 1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**. 2. Enter a descriptive name (for example, `Greeting MCP`). 3. Set **Server URL** to `http://localhost:3002/` (keep the trailing slash). 4. Click **Save** to create the server.  When you save, Scalekit displays the OAuth-protected resource metadata. Copy this JSON—you’ll use it in your `.env` file.  2. ## Create your project directory [Section titled “Create your project directory”](#create-your-project-directory) Set up a clean directory structure for your TypeScript Express project. Terminal
```bash
1
mkdir express-mcp-node
2
cd express-mcp-node
```
3. ## Add package dependencies [Section titled “Add package dependencies”](#add-package-dependencies) Create a `package.json` with scripts and all required dependencies for Express, TypeScript, and the MCP SDK. Terminal
```bash
1
cat <<'EOF' > package.json
2
{
3
"name": "express-mcp-node",
4
"version": "1.0.0",
5
"type": "module",
6
"scripts": {
7
"dev": "tsx src/server.ts",
8
"build": "tsc",
9
"start": "node dist/server.js"
10
},
11
"dependencies": {
12
"@modelcontextprotocol/sdk": "^1.13.0",
13
"@scalekit-sdk/node": "^2.0.1",
14
"cors": "^2.8.5",
15
"dotenv": "^16.4.5",
16
"express": "^5.1.0",
17
"zod": "^3.25.57"
18
},
19
"devDependencies": {
20
"@types/cors": "^2.8.19",
21
"@types/express": "^4.17.21",
22
"@types/node": "^20.11.19",
23
"tsx": "^4.7.0",
24
"typescript": "^5.4.5"
25
}
26
}
27
EOF
```
4. ## Configure TypeScript [Section titled “Configure TypeScript”](#configure-typescript) Add a TypeScript configuration file optimized for ES2022 modules and strict type checking. Terminal
```bash
1
cat <<'EOF' > tsconfig.json
2
{
3
"compilerOptions": {
4
"target": "ES2022",
5
"module": "ES2022",
6
"moduleResolution": "node",
7
"esModuleInterop": true,
8
"forceConsistentCasingInFileNames": true,
9
"strict": false,
10
"skipLibCheck": true,
11
"resolveJsonModule": true,
12
"outDir": "dist",
13
"rootDir": "src",
14
"types": ["node"]
15
},
16
"include": ["src/**/*"]
17
}
18
EOF
```
5. ## Install dependencies [Section titled “Install dependencies”](#install-dependencies) Install all packages declared in `package.json`. Terminal
```bash
1
npm install
```
6. ## Configure environment variables [Section titled “Configure environment variables”](#configure-environment-variables) Create a `.env` file with your Scalekit credentials and the protected resource metadata from step 1. Terminal
```bash
1
cat <<'EOF' > .env
2
PORT=3002
3
SK_ENV_URL=https://.scalekit.com
4
SK_CLIENT_ID=
5
SK_CLIENT_SECRET=
6
MCP_SERVER_ID=
7
PROTECTED_RESOURCE_METADATA=''
8
EXPECTED_AUDIENCE=http://localhost:3002/
9
EOF
10
11
open .env
```
| Variable | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `PORT` | Local port for the Express server. Must match the Server URL registered in Scalekit (defaults to `3002`). | | `SK_ENV_URL` | Your Scalekit environment URL from **Dashboard > Settings > API Credentials** | | `SK_CLIENT_ID` | Client ID from **Dashboard > Settings > API Credentials**. Used with `SK_CLIENT_SECRET` to initialize the SDK. | | `SK_CLIENT_SECRET` | Client secret from **Dashboard > Settings > API Credentials**. Keep this secret and rotate regularly. | | `MCP_SERVER_ID` | The MCP server ID from **Dashboard > MCP Servers**. Not directly used in this implementation but documented for reference. | | `PROTECTED_RESOURCE_METADATA` | The complete OAuth resource metadata JSON from step 1. Clients use this to discover authorization requirements. | | `EXPECTED_AUDIENCE` | The audience value that tokens must include. Should match your server’s public URL (e.g., `http://localhost:3002/`). | Protect your credentials Never commit `.env` to version control. Add it to `.gitignore` immediately and use a secret manager in production (e.g., AWS Secrets Manager, HashiCorp Vault, or your deployment platform’s secrets service). 7. ## Implement the Express MCP server [Section titled “Implement the Express MCP server”](#implement-the-express-mcp-server) Create `src/server.ts` with the complete server implementation. This includes the Scalekit client initialization, authentication middleware for token validation, CORS configuration, and the greeting MCP tool. src/server.ts
```typescript
1
import 'dotenv/config';
2
import cors from 'cors';
3
import express, { NextFunction, Request, Response } from 'express';
4
import { z } from 'zod';
5
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
import { Scalekit } from '@scalekit-sdk/node';
8
9
// Load environment variables
10
const PORT = Number(process.env.PORT ?? 3002);
11
const SK_ENV_URL = process.env.SK_ENV_URL ?? '';
12
const SK_CLIENT_ID = process.env.SK_CLIENT_ID ?? '';
13
const SK_CLIENT_SECRET = process.env.SK_CLIENT_SECRET ?? '';
14
const EXPECTED_AUDIENCE = process.env.EXPECTED_AUDIENCE ?? '';
15
const PROTECTED_RESOURCE_METADATA = process.env.PROTECTED_RESOURCE_METADATA ?? '';
16
17
// Use case: Configure OAuth resource metadata URL for MCP clients
18
// This allows MCP clients to discover authorization requirements via WWW-Authenticate header
19
// Security: The WWW-Authenticate header signals to clients where to obtain tokens
20
const RESOURCE_METADATA_URL = `http://localhost:${PORT}/.well-known/oauth-protected-resource`;
21
22
// WWW-Authenticate header for 401 responses
23
const WWW_HEADER_KEY = 'WWW-Authenticate';
24
const WWW_HEADER_VALUE = `Bearer realm="OAuth", resource_metadata="${RESOURCE_METADATA_URL}"`;
25
26
// Initialize Scalekit client for token validation
27
// Security: Use SDK to validate JWT signatures and claims
28
// This prevents accepting forged or tampered tokens
29
const scalekit = new Scalekit(SK_ENV_URL, SK_CLIENT_ID, SK_CLIENT_SECRET);
30
31
// Initialize MCP server with greeting tool
32
// Context: The McpServer handles MCP protocol details while Express handles HTTP routing
33
const server = new McpServer({ name: 'Greeting MCP', version: '1.0.0' });
34
35
// Use case: Simple greeting tool demonstrating OAuth-protected MCP operations
36
// Context: This tool is protected by the authentication middleware applied to all routes
37
server.tool(
38
'greet_user',
39
'Greets the user with a personalized message.',
40
{
41
name: z.string().min(1, 'Name is required'),
42
},
43
async ({ name }: { name: string }) => ({
44
content: [
45
{
46
type: 'text',
47
text: `Hi ${name}, welcome to Scalekit!`
48
}
49
]
50
})
51
);
52
53
// Initialize Express application
54
const app = express();
55
56
// Enable CORS for cross-origin MCP clients
57
// Use case: Allow MCP clients from different origins to connect
58
app.use(cors({ origin: true, credentials: false }));
59
60
// Parse JSON request bodies
61
// Context: MCP protocol uses JSON-RPC format
62
app.use(express.json());
63
64
// Use case: Expose OAuth resource metadata for MCP client discovery
65
// This endpoint allows clients to discover authorization requirements and server capabilities
66
// Context: MCP clients use this metadata to initiate the OAuth flow
67
app.get('/.well-known/oauth-protected-resource', (_req: Request, res: Response) => {
68
if (!PROTECTED_RESOURCE_METADATA) {
69
res.status(500).json({ error: 'PROTECTED_RESOURCE_METADATA config missing' });
70
return;
71
}
72
73
const metadata = JSON.parse(PROTECTED_RESOURCE_METADATA);
74
res.type('application/json').send(JSON.stringify(metadata, null, 2));
75
});
76
77
// Use case: Health check endpoint for monitoring and load balancers
78
// Context: Keep this separate from protected endpoints for deployment health checks
79
app.get('/health', (_req: Request, res: Response) => {
80
res.json({ status: 'healthy' });
81
});
82
83
// Security: Validate Bearer tokens on all protected endpoints
84
// Public endpoints (health, metadata) are exempt from authentication
85
// This prevents unauthorized access to MCP tools and operations
86
app.use(async (req: Request, res: Response, next: NextFunction) => {
87
// Allow public endpoints without authentication
88
// Use case: Health checks for monitoring; metadata for client discovery
89
if (req.path === '/.well-known/oauth-protected-resource' || req.path === '/health') {
90
next();
91
return;
92
}
93
94
// Extract Bearer token from Authorization header
95
// Use case: OAuth 2.1 Bearer token format (RFC 6750)
96
// Security: Reject requests without valid Bearer token prefix
97
const header = req.headers.authorization;
98
const token = header?.startsWith('Bearer ')
99
? header.slice('Bearer '.length).trim()
100
: undefined;
101
102
if (!token) {
103
res.status(401)
104
.set(WWW_HEADER_KEY, WWW_HEADER_VALUE)
105
.json({ error: 'Missing Bearer token' });
106
return;
107
}
108
109
try {
110
// Validate token using Scalekit SDK
111
// Security: Verifies signature, expiration, issuer, and audience claims
112
// Context: This critical step prevents accepting tokens from other issuers
113
await scalekit.validateToken(token, { audience: [EXPECTED_AUDIENCE] });
114
next();
115
} catch (error) {
116
res.status(401)
117
.set(WWW_HEADER_KEY, WWW_HEADER_VALUE)
118
.json({ error: 'Token validation failed' });
119
}
120
});
121
122
// Handle MCP protocol requests at root path
123
// Use case: Process authenticated MCP tool requests using StreamableHTTPServerTransport
124
// Context: The transport layer handles MCP JSON-RPC communication
125
app.post('/', async (req: Request, res: Response) => {
126
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
127
await server.connect(transport);
128
129
try {
130
await transport.handleRequest(req, res, req.body);
131
} catch (error) {
132
res.status(500).json({ error: 'MCP transport error' });
133
}
134
});
135
136
// Start the Express server
137
app.listen(PORT, () => {
138
console.log(`MCP server running on http://localhost:${PORT}`);
139
});
```
8. ## Start the Express server [Section titled “Start the Express server”](#start-the-express-server) Start the Express server in development mode with auto-reload enabled. The server will listen on `http://localhost:3002/` and display logs indicating Express is ready to receive authenticated MCP requests. Terminal
```bash
1
npm run dev
```
The server starts on `http://localhost:3002/` and logs indicate Express is ready. The MCP endpoint at `/` accepts authenticated POST requests, and the metadata endpoint is accessible at `/.well-known/oauth-protected-resource`. 9. ## Connect with MCP Inspector [Section titled “Connect with MCP Inspector”](#connect-with-mcp-inspector) Test your server end-to-end using the MCP Inspector to verify the OAuth flow works correctly. This allows you to see the authentication handshake and test calling your MCP tools with validated tokens. Terminal
```bash
1
npx @modelcontextprotocol/inspector@latest
```
In the Inspector UI: 1. Enter your MCP Server URL: `http://localhost:3002/` 2. Click **Connect** to initiate the OAuth flow 3. Authenticate with Scalekit when prompted 4. Run the `greet_user` tool with any name  You now have a working Express.js MCP server with Scalekit-protected OAuth authentication. Extend this implementation by adding more MCP tools using `server.tool()` with Zod schema validation, implementing scope-based authorization using custom middleware, integrating with your existing Express application, or adding features like rate limiting and request logging using Express’s middleware ecosystem.
---
# DOCUMENT BOUNDARY
---
# FastAPI + FastMCP quickstart
> Build a production-ready MCP server with FastAPI custom middleware for OAuth token validation and Scalekit authentication.
This guide shows you how to build a production-ready FastAPI + FastMCP server with Scalekit’s OAuth authentication. You’ll implement custom middleware for token validation, expose OAuth resource metadata for client discovery, and create MCP tools that enforce authorization. Use this quickstart when you need more control over your server’s behavior than FastMCP’s built-in provider offers. The FastAPI integration gives you flexibility to add custom middleware, implement additional endpoints, integrate with existing FastAPI applications, and handle complex authorization requirements. The full code is available on [GitHub](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python). **Prerequisites** * A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers * **Python 3.11+** installed locally * Familiarity with FastAPI and OAuth token validation * Basic understanding of MCP server architecture 1. ## Register your MCP server in Scalekit [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit) Create a protected resource entry so Scalekit can issue tokens that your custom FastAPI middleware validates. 1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**. 2. Enter a descriptive name (for example, `Greeting MCP`). 3. Set **Server URL** to `http://localhost:3002/` (keep the trailing slash). 4. Click **Save** to create the server.  When you save, Scalekit displays the OAuth-protected resource metadata. Copy this JSON—you’ll use it in your `.env` file.  2. ## Create your project directory [Section titled “Create your project directory”](#create-your-project-directory) Set up a clean directory structure with a Python virtual environment to isolate FastAPI and FastMCP dependencies. Terminal
```bash
1
mkdir fastapi-mcp-python
2
cd fastapi-mcp-python
3
python3 -m venv .venv
4
source .venv/bin/activate
```
3. ## Add dependencies [Section titled “Add dependencies”](#add-dependencies) Create a `requirements.txt` file with all required packages and install them. Terminal
```bash
1
cat <<'EOF' > requirements.txt
2
mcp>=1.0.0
3
fastapi>=0.104.0
4
fastmcp>=0.8.0
5
uvicorn>=0.24.0
6
pydantic>=2.5.0
7
python-dotenv>=1.0.0
8
httpx>=0.25.0
9
python-jose[cryptography]>=3.3.0
10
cryptography>=41.0.0
11
scalekit-sdk-python>=2.4.0
12
starlette>=0.27.0
13
EOF
14
15
pip install -r requirements.txt
```
4. ## Configure environment variables [Section titled “Configure environment variables”](#configure-environment-variables) Create a `.env` file with your Scalekit credentials and the protected resource metadata from step 1. Terminal
```bash
1
cat <<'EOF' > .env
2
PORT=3002
3
SK_ENV_URL=https://.scalekit.com
4
SK_CLIENT_ID=
5
SK_CLIENT_SECRET=
6
MCP_SERVER_ID=
7
PROTECTED_RESOURCE_METADATA=''
8
EXPECTED_AUDIENCE=http://localhost:3002/
9
EOF
10
11
open .env
```
| Variable | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `PORT` | Local port for the FastAPI server. Must match the Server URL registered in Scalekit (defaults to `3002`). | | `SK_ENV_URL` | Your Scalekit environment URL from **Dashboard > Settings > API Credentials** | | `SK_CLIENT_ID` | Client ID from **Dashboard > Settings > API Credentials**. Used with `SK_CLIENT_SECRET` to initialize the SDK. | | `SK_CLIENT_SECRET` | Client secret from **Dashboard > Settings > API Credentials**. Keep this secret and rotate regularly. | | `MCP_SERVER_ID` | The MCP server ID from **Dashboard > MCP Servers**. Not directly used in this implementation but documented for reference. | | `PROTECTED_RESOURCE_METADATA` | The complete OAuth resource metadata JSON from step 1. Clients use this to discover authorization requirements. | | `EXPECTED_AUDIENCE` | The audience value that tokens must include. Should match your server’s public URL (e.g., `http://localhost:3002/`). | Protect your credentials Never commit `.env` to version control. Add it to `.gitignore` immediately and use a secret manager in production (e.g., AWS Secrets Manager, HashiCorp Vault, or your deployment platform’s secrets service). 5. ## Implement the FastAPI + FastMCP server [Section titled “Implement the FastAPI + FastMCP server”](#implement-the-fastapi--fastmcp-server) Create `main.py` with the complete server implementation. This includes the Scalekit client initialization, authentication middleware for token validation, CORS configuration, and the greeting MCP tool. main.py
```python
1
import json
2
import os
3
from fastapi import FastAPI, Request, Response
4
from fastmcp import FastMCP, Context
5
from scalekit import ScalekitClient
6
from scalekit.common.scalekit import TokenValidationOptions
7
from starlette.middleware.cors import CORSMiddleware
8
from dotenv import load_dotenv
9
10
load_dotenv()
11
12
# Load environment variables
13
PORT = int(os.getenv("PORT", "3002"))
14
SK_ENV_URL = os.getenv("SK_ENV_URL", "")
15
SK_CLIENT_ID = os.getenv("SK_CLIENT_ID", "")
16
SK_CLIENT_SECRET = os.getenv("SK_CLIENT_SECRET", "")
17
EXPECTED_AUDIENCE = os.getenv("EXPECTED_AUDIENCE", "")
18
PROTECTED_RESOURCE_METADATA = os.getenv("PROTECTED_RESOURCE_METADATA", "")
19
20
# Use case: Configure OAuth resource metadata URL for MCP clients
21
# This allows MCP clients to discover authorization requirements via WWW-Authenticate header
22
# Security: The WWW-Authenticate header signals to clients where to obtain tokens
23
RESOURCE_METADATA_URL = f"http://localhost:{PORT}/.well-known/oauth-protected-resource"
24
WWW_HEADER = {
25
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{RESOURCE_METADATA_URL}"'
26
}
27
28
# Initialize Scalekit client for token validation
29
# Security: Use SDK to validate JWT signatures and claims
30
# This prevents accepting forged or tampered tokens
31
scalekit_client = ScalekitClient(
32
env_url=SK_ENV_URL,
33
client_id=SK_CLIENT_ID,
34
client_secret=SK_CLIENT_SECRET,
35
)
36
37
# Initialize FastMCP with stateless HTTP transport
38
# HTTP transport allows MCP clients to connect via standard OAuth flows
39
mcp = FastMCP("Greeting MCP", stateless_http=True)
40
41
42
@mcp.tool(
43
name="greet_user",
44
description="Greets the user with a personalized message."
45
)
46
async def greet_user(name: str, ctx: Context | None = None) -> dict:
47
"""
48
Use case: Simple greeting tool demonstrating OAuth-protected MCP operations
49
Context: This tool is protected by the authentication middleware
50
"""
51
return {
52
"content": [
53
{
54
"type": "text",
55
"text": f"Hi {name}, welcome to Scalekit!"
56
}
57
]
58
}
59
60
61
# Mount FastMCP as a FastAPI application
62
# Context: This allows us to layer FastAPI middleware on top of FastMCP
63
mcp_app = mcp.http_app(path="/")
64
app = FastAPI(lifespan=mcp_app.lifespan)
65
66
# Enable CORS for cross-origin MCP clients
67
# Use case: Allow MCP clients from different origins to connect
68
app.add_middleware(
69
CORSMiddleware,
70
allow_origins=["*"],
71
allow_credentials=True,
72
allow_methods=["GET", "POST", "OPTIONS"],
73
allow_headers=["*"]
74
)
75
76
77
@app.middleware("http")
78
async def auth_middleware(request: Request, call_next):
79
"""
80
Security: Validate Bearer tokens on all protected endpoints.
81
Public endpoints (health, metadata) are exempt from authentication.
82
This prevents unauthorized access to MCP tools and operations.
83
"""
84
# Allow public endpoints without authentication
85
# Use case: Health checks for monitoring; metadata for client discovery
86
if request.url.path in {"/health", "/.well-known/oauth-protected-resource"}:
87
return await call_next(request)
88
89
# Extract Bearer token from Authorization header
90
# Use case: OAuth 2.1 Bearer token format (RFC 6750)
91
# Security: Reject requests without valid Bearer token prefix
92
auth_header = request.headers.get("authorization")
93
if not auth_header or not auth_header.startswith("Bearer "):
94
return Response(
95
'{"error": "Missing Bearer token"}',
96
status_code=401,
97
headers=WWW_HEADER,
98
media_type="application/json"
99
)
100
101
token = auth_header.split("Bearer ", 1)[1].strip()
102
103
# Validate token using Scalekit SDK
104
# Security: Verifies signature, expiration, issuer, and audience claims
105
# Context: This critical step prevents accepting tokens from other issuers
106
options = TokenValidationOptions(
107
issuer=SK_ENV_URL,
108
audience=[EXPECTED_AUDIENCE]
109
)
110
111
try:
112
is_valid = scalekit_client.validate_access_token(token, options=options)
113
if not is_valid:
114
raise ValueError("Invalid token")
115
except Exception:
116
return Response(
117
'{"error": "Token validation failed"}',
118
status_code=401,
119
headers=WWW_HEADER,
120
media_type="application/json"
121
)
122
123
# Token is valid, proceed with request
124
# This allows MCP clients to call tools with authenticated context
125
return await call_next(request)
126
127
128
@app.get("/.well-known/oauth-protected-resource")
129
async def oauth_metadata():
130
"""
131
Use case: Expose OAuth resource metadata for MCP client discovery
132
This endpoint allows clients to discover authorization requirements and server capabilities
133
Context: MCP clients use this metadata to initiate the OAuth flow
134
"""
135
if not PROTECTED_RESOURCE_METADATA:
136
return Response(
137
'{"error": "PROTECTED_RESOURCE_METADATA config missing"}',
138
status_code=500,
139
media_type="application/json"
140
)
141
142
metadata = json.loads(PROTECTED_RESOURCE_METADATA)
143
return Response(
144
json.dumps(metadata, indent=2),
145
media_type="application/json"
146
)
147
148
149
@app.get("/health")
150
async def health_check():
151
"""
152
Use case: Health check endpoint for monitoring and load balancers
153
Context: Keep this separate from protected endpoints for deployment health checks
154
"""
155
return {"status": "healthy"}
156
157
158
# Mount the FastMCP application at root path
159
app.mount("/", mcp_app)
160
161
162
if __name__ == "__main__":
163
import uvicorn
164
# Start server with auto-reload for development
165
# Production: Use 'uvicorn main:app --host 0.0.0.0 --port 3002 --workers 4' behind a reverse proxy
166
uvicorn.run(app, host="0.0.0.0", port=PORT)
```
6. ## Start the FastAPI server [Section titled “Start the FastAPI server”](#start-the-fastapi-server) Start the FastAPI server in development mode with auto-reload enabled. The server will listen on `http://localhost:3002/` and display logs indicating FastAPI is ready to receive authenticated MCP requests. Terminal
```bash
1
python main.py
```
The server starts on `http://localhost:3002/` and logs indicate FastAPI is ready. The MCP endpoint accepts authenticated requests, and the metadata endpoint is accessible at `/.well-known/oauth-protected-resource`. 7. ## Connect with MCP Inspector [Section titled “Connect with MCP Inspector”](#connect-with-mcp-inspector) Test your server end-to-end using the MCP Inspector to verify the OAuth flow works correctly. This allows you to see the authentication handshake and test calling your MCP tools with validated tokens. Terminal
```bash
1
npx @modelcontextprotocol/inspector@latest
```
In the Inspector UI: 1. Enter your MCP Server URL: `http://localhost:3002/` 2. Click **Connect** to initiate the OAuth flow 3. Authenticate with Scalekit when prompted 4. Run the `greet_user` tool with any name  You now have a working FastAPI + FastMCP server with Scalekit-protected OAuth authentication. Extend this implementation by adding more MCP tools with the `@mcp.tool` decorator, implementing scope-based authorization using custom middleware, integrating with your existing FastAPI application, or adding features like rate limiting and request logging using FastAPI’s middleware pipeline.
---
# DOCUMENT BOUNDARY
---
# FastMCP quickstart
> FastMCP todo server with OAuth scope validation and CRUD operations.
This guide shows you how to build a production-ready FastMCP server protected by Scalekit’s OAuth authentication. You’ll register your server as a protected resource, implement scope-based authorization for CRUD operations, and validate tokens on every request. Use this quickstart to experience a working reference implementation with a simple todo application. The todo app demonstrates how to enforce `todo:read` and `todo:write` scopes across multiple tools. After completing this guide, you can apply the same authentication pattern to secure your own FastMCP tools. The full code is available on [GitHub](https://github.com/scalekit-inc/mcp-demo/tree/main/todo-fastmcp). **Prerequisites** * A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers * **Python 3.11+** installed locally * Familiarity with OAuth scopes and basic terminal commands 1. ## Register your MCP server in Scalekit [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit) Create a protected resource entry so Scalekit can issue scoped tokens that FastMCP validates on every request. 1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**. 2. Enter a descriptive name (for example, `FastMCP Todo Server`). 3. Set **Server URL** to `http://localhost:3002/` (keep the trailing slash). This field is a required.\ For a server running at `http://localhost:3002/mcp`, register `http://localhost:3002/`. FastMCP appends `/mcp` automatically, so always provide the base URL with a trailing slash. 4. Create or link the scopes below, then click **Save**.  | Scope | Description | Required | | ------------ | -------------------------------------------- | -------- | | `todo:read` | Grants read access to todo tasks | Yes | | `todo:write` | Allows creating, updating, or deleting tasks | Yes | 2. ## Create your FastMCP todo server [Section titled “Create your FastMCP todo server”](#create-your-fastmcp-todo-server) Prepare a fresh directory and virtual environment to keep FastMCP dependencies isolated. Terminal
```bash
1
mkdir -p fastmcp-todo
2
cd fastmcp-todo
3
python3 -m venv venv
4
source venv/bin/activate
```
3. ## Add dependencies and configuration templates [Section titled “Add dependencies and configuration templates”](#add-dependencies-and-configuration-templates) Create the support files that FastMCP and Scalekit expect, then install the required libraries. Terminal
```bash
1
cat <<'EOF' > requirements.txt
2
fastmcp>=2.13.0.2
3
python-dotenv>=1.0.0
4
EOF
5
6
pip install -r requirements.txt
7
8
cat <<'EOF' > env.example
9
PORT=3002
10
SCALEKIT_ENVIRONMENT_URL=https://your-environment-url.scalekit.com
11
SCALEKIT_CLIENT_ID=your_client_id
12
SCALEKIT_RESOURCE_ID=mcp_server_id
13
MCP_URL=http://localhost:3002/
14
EOF
```
4. ## Implement the FastMCP todo server [Section titled “Implement the FastMCP todo server”](#implement-the-fastmcp-todo-server) Copy the following code into `server.py`. It registers the Scalekit provider, defines an in-memory todo store, and exposes CRUD tools guarded by OAuth scopes. server.py
```python
1
"""Scalekit-authenticated FastMCP server providing in-memory CRUD tools for todos.
2
3
This example demonstrates how to protect FastMCP tools with OAuth scopes.
4
Each tool validates the required scope before executing operations.
5
"""
6
7
import os
8
import uuid
9
from dataclasses import dataclass, asdict
10
from typing import Optional
11
12
from dotenv import load_dotenv
13
from fastmcp import FastMCP
14
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
15
from fastmcp.server.dependencies import AccessToken, get_access_token
16
17
load_dotenv()
18
19
# Use case: Configure FastMCP server with OAuth protection
20
# Security: Scalekit provider validates every request's Bearer token
21
mcp = FastMCP(
22
"Todo Server",
23
stateless_http=True,
24
auth=ScalekitProvider(
25
environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
26
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
27
resource_id=os.getenv("SCALEKIT_RESOURCE_ID"),
28
# FastMCP appends /mcp automatically; keep base URL with trailing slash only
29
mcp_url=os.getenv("MCP_URL"),
30
),
31
)
32
33
34
@dataclass
35
class TodoItem:
36
id: str
37
title: str
38
description: Optional[str]
39
completed: bool = False
40
41
def to_dict(self) -> dict:
42
return asdict(self)
43
44
45
# Use case: In-memory storage for demo purposes
46
# Production: Replace with your database or persistent storage
47
_TODO_STORE: dict[str, TodoItem] = {}
48
49
50
def _require_scope(scope: str) -> Optional[str]:
51
"""
52
Security: Validate that the current request's token includes the required scope.
53
This prevents unauthorized access to protected operations.
54
"""
55
token: AccessToken = get_access_token()
56
if scope not in token.scopes:
57
return f"Insufficient permissions: `{scope}` scope required."
58
return None
59
60
61
@mcp.tool
62
def create_todo(title: str, description: Optional[str] = None) -> dict:
63
"""
64
Use case: Create a new todo item for task tracking
65
Requires: todo:write scope
66
"""
67
error = _require_scope("todo:write")
68
if error:
69
return {"error": error}
70
71
todo = TodoItem(id=str(uuid.uuid4()), title=title, description=description)
72
_TODO_STORE[todo.id] = todo
73
return {"todo": todo.to_dict()}
74
75
76
@mcp.tool
77
def list_todos(completed: Optional[bool] = None) -> dict:
78
"""
79
Use case: Retrieve all todos, optionally filtered by completion status
80
Requires: todo:read scope
81
"""
82
error = _require_scope("todo:read")
83
if error:
84
return {"error": error}
85
86
todos = [
87
todo.to_dict()
88
for todo in _TODO_STORE.values()
89
if completed is None or todo.completed == completed
90
]
91
return {"todos": todos}
92
93
94
@mcp.tool
95
def get_todo(todo_id: str) -> dict:
96
"""
97
Use case: Retrieve a specific todo by ID
98
Requires: todo:read scope
99
"""
100
error = _require_scope("todo:read")
101
if error:
102
return {"error": error}
103
104
todo = _TODO_STORE.get(todo_id)
105
if todo is None:
106
return {"error": f"Todo `{todo_id}` not found."}
107
108
return {"todo": todo.to_dict()}
109
110
111
@mcp.tool
112
def update_todo(
113
todo_id: str,
114
title: Optional[str] = None,
115
description: Optional[str] = None,
116
completed: Optional[bool] = None,
117
) -> dict:
118
"""
119
Use case: Update existing todo properties or mark as complete
120
Requires: todo:write scope
121
"""
122
error = _require_scope("todo:write")
123
if error:
124
return {"error": error}
125
126
todo = _TODO_STORE.get(todo_id)
127
if todo is None:
128
return {"error": f"Todo `{todo_id}` not found."}
129
130
if title is not None:
131
todo.title = title
132
if description is not None:
133
todo.description = description
134
if completed is not None:
135
todo.completed = completed
136
137
return {"todo": todo.to_dict()}
138
139
140
@mcp.tool
141
def delete_todo(todo_id: str) -> dict:
142
"""
143
Use case: Remove a todo from the system
144
Requires: todo:write scope
145
"""
146
error = _require_scope("todo:write")
147
if error:
148
return {"error": error}
149
150
todo = _TODO_STORE.pop(todo_id, None)
151
if todo is None:
152
return {"error": f"Todo `{todo_id}` not found."}
153
154
return {"deleted": todo_id}
155
156
157
if __name__ == "__main__":
158
# Start HTTP transport server
159
mcp.run(transport="http", port=int(os.getenv("PORT", "3002")))
```
5. ## Provide runtime secrets [Section titled “Provide runtime secrets”](#provide-runtime-secrets) Copy the environment template and populate the values from your Scalekit dashboard. Terminal
```bash
1
cp env.example .env
2
open .env
```
| Variable | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL, labeled **Environment URL** in **Dashboard > Settings > API Credentials** | | `SCALEKIT_CLIENT_ID` | Client ID from **Dashboard > Settings > API Credentials** | | `SCALEKIT_RESOURCE_ID` | The resource identifier assigned to your MCP server (starts with `res_`). Find it on your MCP server’s detail page in the dashboard. | | `MCP_URL` | The base public URL you registered (keep trailing slash, e.g., `http://localhost:3002/`) | | `PORT` | Local port for FastMCP HTTP transport (defaults to `3002`) | Store secrets securely Avoid committing `.env` to source control. Use your team’s secret manager in production and rotate credentials if they appear in logs or terminal history. 6. ## Run the FastMCP server locally [Section titled “Run the FastMCP server locally”](#run-the-fastmcp-server-locally) Start the server so it can accept authenticated MCP requests at `/mcp`. Terminal
```bash
1
source venv/bin/activate
2
python server.py
```
When the server boots successfully, you’ll see FastMCP announce the HTTP transport and listen on `http://localhost:3002/`, ready to enforce Scalekit-issued tokens.  7. ## Connect with an MCP client [Section titled “Connect with an MCP client”](#connect-with-an-mcp-client) Use any MCP-compatible client to exercise the todo tools with scoped tokens. During development, the MCP Inspector demonstrates how the Scalekit provider enforces scopes end-to-end. Terminal
```bash
1
npx @modelcontextprotocol/inspector@latest
```
In the Inspector UI, point the client to `http://localhost:3002/mcp` and click **Connect**. The client initiates OAuth authentication with Scalekit. After successful authentication, run any tool—the server exposes `create_todo`, `list_todos`, `get_todo`, `update_todo`, and `delete_todo`.  Once you’re satisfied with the quickstart example, extend `server.py` with your own FastMCP tools or replace the in-memory store with your production data source. Scalekit’s provider handles authentication for any toolset you add.
---
# DOCUMENT BOUNDARY
---
# New to MCP?
> Lock down MCP connections with OAuth 2.1 so agents get only the access they need
AI systems are moving beyond chatbots to agents that act in the real world. They handle sensitive data and run complex workflows. As they grow, they need a secure, standard way to connect. The Model Context Protocol (MCP) provides that standard. It defines how AI applications safely discover and use external tools and data. MCP incorporates OAuth 2.1 authorization mechanisms at the transport level. This enables MCP clients to make secure requests to restricted MCP servers on behalf of resource owners. | Features | Benefit | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | Industry standard | Well-established authorization framework with extensive tooling and ecosystem support | | Security best practices | Incorporates improvements over OAuth 2.0, removing deprecated flows and enforcing security measures like PKCE | | Multiple grant types | Supports different use cases: **Authorization code** for human user scenarios and **Client credentials** for machine-to-machine integrations | | Ecosystem compatibility | Integrates with existing identity providers and authorization servers | ## Complete MCP OAuth 2.1 flow [Section titled “Complete MCP OAuth 2.1 flow”](#complete-mcp-oauth-21-flow) Here’s the complete end-to-end authorization flow showing all phases from discovery to token refresh in a single sequence diagram: ### Understanding the MCP authorization flow [Section titled “Understanding the MCP authorization flow”](#understanding-the-mcp-authorization-flow) MCP OAuth 2.1 provides secure, standardized authorization for AI agents accessing protected resources. The flow establishes trust, authenticates users, authorizes access, and maintains security throughout the session lifecycle by building each phase on the previous one.
---
# DOCUMENT BOUNDARY
---
# Production readiness checklist
> A focused checklist for launching your MCP server authentication, with emphasis on custom domain and branding for a production-grade OAuth consent experience.
As you prepare to launch MCP server authentication to production, verify these items before going live. ## Production environment [Section titled “Production environment”](#production-environment) **Verify production environment configuration** Confirm that your environment URL (`SCALEKIT_ENVIRONMENT_URL`), client ID (`SCALEKIT_CLIENT_ID`), and client secret (`SCALEKIT_CLIENT_SECRET`) are correctly set for production, not dev or staging. ## Custom domain (CNAME) [Section titled “Custom domain (CNAME)”](#custom-domain-cname) Configuring a custom domain is the highest-impact step for MCP auth. Without it, the OAuth consent screen your users see during authorization displays `yourapp.scalekit.com` (Scalekit’s default domain) instead of your own brand. Complete this section before you test any end-to-end auth flows in production. **Configure a custom domain in the Scalekit dashboard** Go to **Dashboard > Auth for SaaS > Customization > Custom Domain** and follow the CNAME setup instructions. CNAME configuration is available only in production environments. See [Branded custom domains](/guides/custom-domain/) for step-by-step instructions. **Verify SSL certificate provisioning** After CNAME verification, Scalekit automatically provisions an SSL certificate. Click **Check** in **Dashboard > Auth for SaaS > Customization > Custom Domain** to confirm status. Provisioning can take up to 24 hours. Contact if it takes longer. **Update customer-facing authorization URLs to use your custom domain** Backend API calls work with either URL, but the authorization URL and any other endpoints your users interact with must use your custom domain (for example, `mcp.yourapp.com`) for branding to take effect. Update those URLs in your application code after CNAME setup is complete. **Confirm the consent screen shows your branded domain** Open an incognito window, trigger an authorization flow, and verify the OAuth consent screen shows your custom domain (for example, `mcp.yourapp.com`) and not the default Scalekit URL. ## Consent screen branding [Section titled “Consent screen branding”](#consent-screen-branding) **Upload your logo and set brand colors** Go to **Dashboard > Auth for SaaS > Customization > Branding** to upload your logo, set a favicon, and configure background colors. This is what users see during the OAuth authorization flow. **Set a recognizable MCP server name** When registering your MCP server in **Dashboard > MCP Servers**, provide a name that users will recognize (for example, `Acme Calendar`, not `mcp-prod-v2`). This name appears on the consent page that MCP hosts display to users when they authorize access. ## Bring your own auth (if applicable) [Section titled “Bring your own auth (if applicable)”](#bring-your-own-auth-if-applicable) If you are using a federated authentication setup where Scalekit acts as the OAuth 2.1 layer but delegates user authentication to your existing system, verify these additional items. **Configure your login endpoint URL in the Scalekit dashboard** Scalekit redirects users to your login endpoint with a `login_request_id` and `state` parameter. Confirm the endpoint URL is correctly set for your production environment. **Verify your login endpoint handles the `login_request_id` and `state` parameters** Your login page must capture both parameters from the redirect and pass them through the authentication flow. Missing either parameter breaks the callback handshake. **Test the backend-to-backend user handoff** After authenticating a user, your system POSTs user attributes to Scalekit at `/api/v1/connections//auth-requests//user`. Verify this call succeeds with production credentials and that the `connection_id` (starts with `conn_`) is correct for your production MCP server. **Verify the state parameter in your callback redirect** After the user handoff, your system redirects back to Scalekit’s callback URL with the `state` value from step 1. Confirm the values match exactly. Mismatched state is silently rejected and breaks the flow. **Test the complete federated flow end-to-end** Trigger a full authorization flow from an MCP client through your login page, the user handoff API call, and the callback redirect, and verify the MCP client receives a valid access token at the end. See [Bring your own auth](/authenticate/mcp/custom-auth/) for implementation details. ## Core auth flows [Section titled “Core auth flows”](#core-auth-flows) **Test the human → MCP authorization flow end-to-end** After CNAME and branding are configured, trigger a complete authorization code flow: from the initial tool call through the `401` challenge, consent screen, token exchange, and authorized response. Run this in production with your live credentials. **Test the agent → MCP client credentials flow** Verify that API clients can obtain tokens and call MCP tools successfully. Confirm that client credentials are stored in environment variables or a secrets manager, not hardcoded. ## Monitoring [Section titled “Monitoring”](#monitoring) **Enable auth logs and set up alerts** Monitor **Dashboard > Auth Logs** for repeated `401` errors, invalid scope requests, and token failures after launch. Set up alerts for unusual activity patterns. After launch, use [Auth logs](/guides/dashboard/auth-logs/) to debug authentication issues and review the [MCP auth troubleshooting guide](/authenticate/mcp/troubleshooting/) for common problems.
---
# DOCUMENT BOUNDARY
---
# Managing MCP Clients
> Manage MCP clients by viewing registered MCP clients, tracking user consent, and revoking access to your MCP servers.
To maintain security and control over your MCP Server, you need to manage which client applications can access it. Scalekit provides several ways for clients to connect, including automatic registration for modern apps and manual pre-registration for custom or trusted clients. This guide covers the different types of MCP clients and shows you how to: * View all registered clients * See which users have granted consent to a client * Revoke user access for any client There are three main categories of MCP Clients that can interact with your MCP Server: ## 1. Automatic registration with DCR [Section titled “1. Automatic registration with DCR”](#1-automatic-registration-with-dcr) These are MCP Clients that automatically register themselves as OAuth clients. Most modern MCP clients, such as Claude Desktop, OpenAI, VS Code, and Cursor, support Dynamic Client Registration (DCR). They initiate the registration process and start the OAuth Authorization flow with the Scalekit server to obtain an access token without requiring manual configuration. During the consent flow, users see your **environment domain** as the requesting identifier — not “Scalekit” and not your application name. This is by design: the domain identifies the authorization server handling the request, similar to how Google OAuth shows the requesting domain. To display a custom branded domain, configure a [custom domain](/agentkit/advanced/custom-domain) for your Scalekit environment. ## 2. Manual client pre-registration [Section titled “2. Manual client pre-registration”](#2-manual-client-pre-registration) These are MCP Clients that you manually register in the Scalekit Dashboard. This is useful when you want to restrict access to specific, pre-approved clients or when you are building a custom client that requires fixed credentials. You can create OAuth clients that can either act as themselves or on behalf of the user. ### How to pre-register a client [Section titled “How to pre-register a client”](#how-to-pre-register-a-client) If you need to manually register an MCP Client, you can do so in the Scalekit Dashboard. 1. Navigate to the **Clients** section of your MCP Server. 2. Click the **Create Client** button.  **Configuration:** * **Client name**: A display name (e.g., “My Custom Client”). * **Redirect URI**: The URL where the client will redirect users after authorization. 3. **Choosing the right OAuth flow:** * **For Client Credentials Flow**: Leave the Redirect URI field empty. Your application will authenticate using only the `client_id` and `client_secret`. This is suitable for server-to-server communication. * **For Authorization Code Grant Flow**: Provide one or more Redirect URIs where users will be redirected after granting consent. This is required for user-facing applications that need to act on behalf of users. **Cross-App Access (XAA):** XAA doesn’t require any explicit setting - your static clients by default support cross-app access under enterprise plan. Once the client is created, you will receive a `client_id` and `client_secret` to configure in your application.  ### 2.1 OAuth client credential flow [Section titled “2.1 OAuth client credential flow”](#21-oauth-client-credential-flow) Use this flow when your MCP Client needs to act on its own behalf rather than on behalf of a specific user. This is ideal for machine-to-machine communication scenarios. **When to use:** * Backend services or server-side applications * Automated scripts or batch processes * System integrations that don’t require user interaction * Applications that need to access resources without user context **Characteristics:** * No user interaction required * No redirect URI needed * Client authenticates using `client_id` and `client_secret` * Access token represents the client itself ### 2.2 OAuth authorization code grant flow [Section titled “2.2 OAuth authorization code grant flow”](#22-oauth-authorization-code-grant-flow) Use this flow when your MCP Client needs to act on behalf of a user. This is the standard OAuth flow that requires user consent. **When to use:** * User-facing applications (web, desktop, or mobile) * Applications that need to access user-specific resources * Scenarios requiring explicit user consent * Applications where actions should be attributed to specific users **Characteristics:** * Requires user authentication and consent * Redirect URI is mandatory * Client receives authorization code, exchanges it for access token * Access token represents the user’s authorization ## 3. Registration via metadata URL (CIMD) [Section titled “3. Registration via metadata URL (CIMD)”](#3-registration-via-metadata-url-cimd) These are MCP Clients that support Client ID Metadata Document (CIMD), an OAuth 2.0 mechanism that allows clients to use a URL as their client identifier. When a CIMD-compatible client initiates the OAuth flow, Scalekit fetches the client’s metadata (such as name, redirect URIs, and other registration information) from the provided URL. This provides an alternative registration method without requiring manual pre-registration or Dynamic Client Registration, making it easier for clients to authenticate across different authorization servers. ## Manage registered clients [Section titled “Manage registered clients”](#manage-registered-clients) ### View all registered clients [Section titled “View all registered clients”](#view-all-registered-clients) You can view a list of all MCP Clients that have been registered with your MCP Server (both DCR and pre-registered) in the Scalekit Dashboard. 1. Go to your MCP Server in the dashboard. 2. Click on the **Clients** tab.  ### View consented users [Section titled “View consented users”](#view-consented-users) For each registered MCP Client that uses the OAuth Authorization Code Grant Flow, you can view all users who have granted consent. 1. From the **Clients** list, click on a specific client. 2. Navigate to the **Consents** tab to see the list of users who have authorized this client.  ### Revoke user access [Section titled “Revoke user access”](#revoke-user-access) As an administrator, you can revoke a user’s consent for a specific MCP Client at any time. This is useful when: * A user requests to revoke access * You need to remove access for security reasons * An employee leaves the organization * You want to force re-authentication **To revoke access:** 1. Navigate to the specific MCP Client from the **Clients** list. 2. Go to the **Consents** tab. 3. Find the user whose access you want to revoke. 4. Click the **Revoke** or **Delete** action for that user. Once revoked, the user will need to go through the authorization flow again to grant consent if they want to use the MCP Client. ### Revoke access from a public MCP client [Section titled “Revoke access from a public MCP client”](#revoke-access-from-a-public-mcp-client) On Scalekit, MCP clients registered through Dynamic Client Registration (`DCR`) or a Client ID Metadata Document (`CIMD`) are public clients: they have no client secret. Scalekit’s OAuth token revocation endpoint requires client authentication, so a direct revoke call from these clients returns `invalid_client`. That response is expected, not a misconfiguration. To revoke access, use one of the supported paths instead: * **Revoke consent from the dashboard**: revoke the user’s consent from the **Consents** tab, as described in [Revoke user access](/authenticate/mcp/managing-mcp-clients/#revoke-user-access). * **Disconnect in the client**: most MCP clients expose a **Disconnect** action that clears tokens stored on the client. This is local sign-out only; it does not revoke the grant on Scalekit. * **Revoke the session in your own system**: when you use [Bring your own auth](/authenticate/mcp/custom-auth/), revoke the user’s session in your auth system. After your MCP server rejects that session, the next request returns `401 Unauthorized` and the client must authenticate again.
---
# DOCUMENT BOUNDARY
---
# Agent / Machine interacting with MCP Server
> Learn how an autonomous agent or machine securely authenticates with an MCP Server using OAuth 2.1 Client Credentials flow in Scalekit.
An **autonomous agent** or any **machine-to-machine process** can directly interact with an **MCP Server** secured by Scalekit. In this model, the agent acts as a **confidential OAuth client**, authenticated using a `client_id` and `client_secret` issued by Scalekit. This topology uses the **OAuth 2.1 Client Credentials flow**, allowing the agent to obtain an access token without user interaction. Tokens are scoped and time-bound, ensuring secure and auditable automation between services. *** ## Authorization Sequence [Section titled “Authorization Sequence”](#authorization-sequence) *** ## How It Works [Section titled “How It Works”](#how-it-works) **Client Registration** Before an agent can request tokens, you must create a **Machine-to-Machine (M2M) client** for your MCP Server in Scalekit. Steps to create a client: 1. Navigate to **Dashboard ? MCP Servers** and select your MCP Server. Go to the **Clients** tab.  2. Click **Create Client**.  3. Copy the **client\_id** and **client\_secret** immediately - the secret will not be shown again.  4. Optionally, set scopes (e.g., `todo:read`, `todo:write`) that correspond to the permissions configured for your MCP Server. Hit **Save** *** ## Requesting an Access Token [Section titled “Requesting an Access Token”](#requesting-an-access-token) Once you have the client credentials, the agent can request a token directly from the Scalekit Authorization Server: Terminal
```bash
1
curl --location '{{env_url}}/oauth/token' \
2
--header 'Content-Type: application/x-www-form-urlencoded' \
3
--data-urlencode 'grant_type=client_credentials' \
4
--data-urlencode 'client_id={{client_id}}' \
5
--data-urlencode 'client_secret={{secret_value}}' \
6
--data-urlencode 'scope=todo:read todo:write'
```
Scalekit responds with a JSON payload similar to:
```json
1
{
2
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...",
3
"token_type": "Bearer",
4
"expires_in": 3600,
5
"scope": "todo:read todo:write"
6
}
```
Use the `access_token` in the `Authorization` header when calling your MCP Server’s endpoint. *** ## Try It Yourself [Section titled “Try It Yourself”](#try-it-yourself) If you’d like to simulate this flow, use the same **FastMCP Todo Server** from the [FastMCP Example](/authenticate/mcp/fastmcp-quickstart). Create an **M2M client** in the Scalekit Dashboard and run your token request using `curl` or programmatically within your agent. Once the token is obtained, attach it as a Bearer token in the `Authorization` header when calling your MCP Server’s tools.
---
# DOCUMENT BOUNDARY
---
# Human interacting with MCP Server
> Learn how a human authenticates with an MCP Server via OAuth 2.1 when using MCP-compliant hosts such as ChatGPT, Claude, VSCode, or Windsurf.
When a human uses a compliant MCP host, that host acts as the OAuth client. It initiates authorization with the Scalekit Authorization Server, obtains a scoped access token, and interacts securely with the MCP Server on behalf of the user. This topology represents the most common interaction model for real-world MCP usecases - **humans interacting with an MCP**, while Scalekit ensures tokens are valid, scoped, and auditable. *** ## Authorization Sequence [Section titled “Authorization Sequence”](#authorization-sequence) *** ## How It Works [Section titled “How It Works”](#how-it-works) 1. **Initiation** ? The human configures an MCP server in their MCP client. 2. **Challenge** ? The MCP Server responds with an HTTP `401` containing a `WWW-Authenticate` header that points to the Scalekit Authorization Server. 3. **Authorization Flow** ? The MCP Client opens the user’s browser to initiate the OAuth 2.1 authorization flow. During this step, the Scalekit Authorization Server handles user authentication through Passwordless, Passkeys, Social login providers (like Google, GitHub, or LinkedIn), or Enterprise SSO integrations (such as Okta, Microsoft Entra ID, or ADFS). The user is then prompted to grant consent for the requested scopes. Once approved, Scalekit returns an authorization code, which the MCP Client exchanges for an access token. 4. **Token Issuance** ? Scalekit issues an OAuth 2.1 access token containing claims and scopes (for example, `todo:read`, `calendar:write`) that represent the user’s permissions. 5. **Authorized Request** ? The client calls the MCP Server again, now attaching the Bearer token in the `Authorization` header. 6. **Validation and Execution** ? The MCP Server validates the token issued by scalekit and executes the requested tool. *** ## Try It Yourself [Section titled “Try It Yourself”](#try-it-yourself) Head to the **[FastMCP Examples section](/authenticate/mcp/fastmcp-quickstart)** to experience this topology in action. There you’ll register a FastMCP server, configure Scalekit Auth, and observe token issuance and validation end-to-end.
---
# DOCUMENT BOUNDARY
---
# MCP Server interacting with MCPs / APIs
> Understand how an MCP Server integrates with internal systems, other MCP servers, or external APIs using secure tokens or API keys.
In real-world scenarios, an **MCP Server** often needs to make backend calls - to your **own APIs**, to **another MCP Server**, or to **external APIs** such as CRM, ticketing, or SaaS tools. This page explains three secure ways to perform these downstream integrations, each corresponding to a different trust boundary and authorization pattern. ## 1. Using API Keys or Custom Tokens [Section titled “1. Using API Keys or Custom Tokens”](#1-using-api-keys-or-custom-tokens) Your MCP Server can communicate with internal or external backend systems that have their own authorization servers or API key?based access. In this setup, the MCP Server manages its own credentials securely (for example, an environment variable, vault, or secrets manager) and injects them when making downstream calls. ### Example [Section titled “Example”](#example) * The MCP Server stores an API key as `EXTERNAL_API_KEY` in environment variables. * When a tool (e.g., `get_weather_data`) is called, your MCP server attaches the key in the request. * The backend API validates the key and responds with data. *** ## 2. Interacting with Another MCP Server autonomously [Section titled “2. Interacting with Another MCP Server autonomously”](#2-interacting-with-another-mcp-server-autonomously) If you have two MCP Servers that need to communicate - for example, `crm-mcp` calling tools from `tickets-mcp` - you can follow the same authentication pattern described in the [Agent ? MCP](/authenticate/mcp/topologies/agent-mcp/) topology. The calling MCP Server (in this case, `crm-mcp`) acts as an **autonomous agent**, authenticating with the receiving MCP Server via **OAuth 2.1 Client Credentials Flow**. Once the token is issued by Scalekit, the calling MCP uses it to call tools exposed by the second MCP Server. You can find a detailed explanation of this topology in [this section](/authenticate/mcp/topologies/agent-mcp). *** ## 3. Cascading the Same Token to Downstream Systems [Section titled “3. Cascading the Same Token to Downstream Systems”](#3-cascading-the-same-token-to-downstream-systems) In some cases, you may want your MCP Server to forward (or “cascade”) the **same access token** it received from the client - for example, when your backend system lies within the same trust boundary as the Scalekit Authorization Server and can validate the token based on its issuer, scopes, and expiry. ### When to Use This Pattern [Section titled “When to Use This Pattern”](#when-to-use-this-pattern) * Both systems (MCP Server and backend MCP/API) trust **the same Authorization Server** (Scalekit). * The backend API can validate JWTs using public keys or JWKS URL. * Scopes and issuer claims (`iss`, `scope`, `exp`) are sufficient to determine access.
---
# DOCUMENT BOUNDARY
---
# Troubleshooting MCP auth
> Troubleshooting guide for common errors while adding auth for MCP Servers
Use this guide when you use Scalekit as the authentication server for your MCP servers and a client fails to connect, a request stalls, or the authentication handshake breaks. Start with the category that matches your symptom, then open the matching scenario. Most issues fall into configuration and setup, CORS and network access, or client-specific behavior. Each scenario explains the cause and the fix. ## Configuration and setup [Section titled “Configuration and setup”](#configuration-and-setup) ## CORS and network access [Section titled “CORS and network access”](#cors-and-network-access) ## Client-specific issues [Section titled “Client-specific issues”](#client-specific-issues) ## Best practices [Section titled “Best practices”](#best-practices) Follow these best practices to avoid common issues and maintain a robust MCP authentication setup: 1. **Use separate Scalekit environments** for development and production to prevent configuration conflicts 2. **Register MCP servers with environment-specific domains:** * Development: `https://mcp-dev.yourdomain.com/` * Production: `https://mcp.yourdomain.com/` 3. **Update your MCP client configuration** to point to the correct Scalekit environment for each deployment 4. **Test authentication independently** in each environment before you deploy to production 5. **Monitor authentication logs** in **Dashboard > Authentication > Logs** to identify and resolve issues quickly 6. **Keep callback URLs updated** whenever you change domains or ports ## Get help [Section titled “Get help”](#get-help) If none of the scenarios resolve the issue, open **Dashboard > Authentication > Logs** and review the authorization and token requests for the failing attempt. When you contact [support](mailto:support@scalekit.com), include: * The failing `client_id` and the MCP server `resource_id` * The MCP client and transport (for example, a Claude Desktop custom connector or `mcp-remote`) * The full error text and the approximate timestamp of the attempt * The steps that reproduce the failure
---
# DOCUMENT BOUNDARY
---
# Add auth to xmcp server
> Build an MCP server with xmcp framework and Scalekit OAuth 2.1 authentication. xmcp handles transport, routing, and bundling so you focus on tools.
This guide shows you how to build an MCP server with [xmcp](https://xmcp.dev) and secure it with Scalekit OAuth 2.1. xmcp is a TypeScript MCP framework that handles transport setup, bundling, and hot reload — you write tools as plain functions and add auth via a middleware file. Use this quickstart when you want a framework that manages MCP protocol details for you. xmcp gives you file-based routing for tools, built-in Streamable HTTP transport, and a middleware convention that keeps auth separate from business logic. The full code is available on [GitHub](https://github.com/scalekit-developers/xmcp-scalekit-example). **Prerequisites** * A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers * **Node.js 18+** installed locally * Basic understanding of TypeScript and OAuth 1. ## Register your MCP server in Scalekit [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit) Create a protected resource entry so Scalekit can issue and validate tokens for your server. 1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**. 2. Enter a descriptive name (for example, `xmcp Demo`). 3. Set **Server URL** to `http://localhost:3001`. 4. Ensure **Allow dynamic client registration** is checked — this is required for MCP clients like Claude Desktop and Cursor to connect automatically. 5. Click **Save** to create the server. After saving, note the **Resource ID** shown below the server name (for example, `res_...`). You’ll need it in the next step. 2. ## Create your project [Section titled “Create your project”](#create-your-project) Scaffold a new xmcp project and add the dependencies for Scalekit authentication. Terminal
```bash
1
mkdir xmcp-scalekit-example
2
cd xmcp-scalekit-example
```
Create `package.json`: package.json
```json
1
{
2
"name": "xmcp-scalekit-example",
3
"private": true,
4
"scripts": {
5
"dev": "xmcp dev",
6
"build": "xmcp build",
7
"start": "node dist/http.js"
8
},
9
"dependencies": {
10
"xmcp": "^0.6.10",
11
"@scalekit-sdk/node": "^2.6.2",
12
"jose": "^5.2.0",
13
"express": "^4.22.1",
14
"zod": "^4.0.10"
15
},
16
"devDependencies": {
17
"@types/express": "^4.17.25",
18
"@types/node": "^22.19.2",
19
"typescript": "^5.9.3"
20
}
21
}
```
Install dependencies: Terminal
```bash
1
npm install
```
3. ## Configure environment variables [Section titled “Configure environment variables”](#configure-environment-variables) Create a `.env` file with your Scalekit credentials from step 1. Terminal
```bash
1
cat <<'EOF' > .env
2
SCALEKIT_ENVIRONMENT_URL=https://.scalekit.com
3
SCALEKIT_CLIENT_ID=
4
SCALEKIT_CLIENT_SECRET=
5
SCALEKIT_RESOURCE_ID=
6
BASE_URL=http://localhost:3001
7
PORT=3001
8
EOF
9
10
open .env
```
| Variable | Description | | -------------------------- | ---------------------------------------------------------------------------------------------- | | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL from **Dashboard > Settings > API Credentials** | | `SCALEKIT_CLIENT_ID` | Client ID from **Dashboard > Settings > API Credentials** | | `SCALEKIT_CLIENT_SECRET` | Client secret from **Dashboard > Settings > API Credentials** | | `SCALEKIT_RESOURCE_ID` | Resource ID from **Dashboard > MCP Servers** (the `res_...` value shown below the server name) | | `BASE_URL` | Public URL where your server is reachable. Must match the Server URL registered in Scalekit | | `PORT` | Local port for the server | Resource ID is required Without `SCALEKIT_RESOURCE_ID`, the server cannot serve the resource-specific authorization server metadata that includes the `registration_endpoint`. MCP clients that rely on Dynamic Client Registration (DCR) — including Claude Desktop and Cursor — will fail to connect. 4. ## Enable Streamable HTTP transport [Section titled “Enable Streamable HTTP transport”](#enable-streamable-http-transport) Create `xmcp.config.ts` at the project root to enable Streamable HTTP mode. By default xmcp uses stdio; setting `http: true` starts an Express-based HTTP server. xmcp.config.ts
```typescript
1
import { XmcpConfig } from "xmcp";
2
3
const config: XmcpConfig = {
4
http: true,
5
paths: {
6
prompts: false,
7
resources: false,
8
},
9
};
10
11
export default config;
```
5. ## Add the Scalekit auth provider [Section titled “Add the Scalekit auth provider”](#add-the-scalekit-auth-provider) Create `src/lib/scalekit-auth.ts` — this is the auth provider that handles JWT verification and OAuth discovery endpoints. The provider returns an xmcp `Middleware` object with a `router` (for discovery endpoints) and a `middleware` (for token validation on `/mcp`). src/lib/scalekit-auth.ts
```typescript
1
import {
2
Router,
3
Request,
4
Response,
5
NextFunction,
6
type RequestHandler,
7
} from "express";
8
import { createContext, type Middleware } from "xmcp";
9
import { createRemoteJWKSet, jwtVerify, errors } from "jose";
10
import { Scalekit } from "@scalekit-sdk/node";
11
12
// --- Types ---
13
14
export interface ScalekitConfig {
15
readonly environmentUrl: string;
16
readonly clientId: string;
17
readonly clientSecret: string;
18
readonly baseURL: string;
19
readonly resourceId?: string;
20
readonly docsURL?: string;
21
readonly scopes?: readonly string[];
22
}
23
24
export interface JWTClaims {
25
readonly sub: string;
26
readonly iss: string;
27
readonly aud?: string | readonly string[];
28
readonly exp: number;
29
readonly iat: number;
30
readonly scope?: string;
31
readonly sid?: string;
32
readonly org_id?: string;
33
}
34
35
export interface Session {
36
readonly userId: string;
37
readonly scopes: readonly string[];
38
readonly organizationId?: string;
39
readonly expiresAt: Date;
40
readonly issuedAt: Date;
41
readonly claims: JWTClaims;
42
}
43
44
interface SessionContext {
45
session: Session | null;
46
}
47
48
interface ClientContext {
49
client: Scalekit;
50
}
51
52
const sessionContext = createContext({
53
name: "scalekit-context-session",
54
});
55
56
const clientContext = createContext({
57
name: "scalekit-context-client",
58
});
59
60
// --- Public accessors (call from tools) ---
61
62
export function getSession(): Session {
63
const ctx = sessionContext.getContext();
64
if (!ctx.session) {
65
throw new Error(
66
"[Scalekit] No session. Is the request authenticated?"
67
);
68
}
69
return ctx.session;
70
}
71
72
export function getClient(): Scalekit {
73
const { client } = clientContext.getContext();
74
if (!client) {
75
throw new Error(
76
"[Scalekit] Client not initialized."
77
);
78
}
79
return client;
80
}
81
82
// --- JWT helpers ---
83
84
async function verifyScalekitToken(
85
token: string,
86
jwksUrl: URL,
87
issuer: string
88
) {
89
const JWKS = createRemoteJWKSet(jwksUrl);
90
const { payload } = await jwtVerify(token, JWKS, {
91
issuer,
92
clockTolerance: 30,
93
});
94
if (!payload.sub) throw new Error("Missing sub claim");
95
return payload as unknown as JWTClaims;
96
}
97
98
function claimsToSession(claims: JWTClaims): Session {
99
return {
100
userId: claims.sub,
101
scopes: claims.scope ? claims.scope.split(" ") : [],
102
organizationId: claims.org_id,
103
expiresAt: new Date(claims.exp * 1000),
104
issuedAt: new Date(claims.iat * 1000),
105
claims,
106
};
107
}
108
109
function extractBearerToken(
110
header: string | undefined
111
): string | null {
112
if (!header) return null;
113
const parts = header.split(" ");
114
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer")
115
return null;
116
return parts[1];
117
}
118
119
// --- Provider factory ---
120
121
export function scalekitProvider(
122
config: ScalekitConfig
123
): Middleware {
124
const client = new Scalekit(
125
config.environmentUrl,
126
config.clientId,
127
config.clientSecret
128
);
129
130
clientContext.provider({ client }, () => {});
131
sessionContext.provider({ session: null }, () => {});
132
133
const envUrl = config.environmentUrl.replace(/\/$/, "");
134
const authServerBase = config.resourceId
135
? `${envUrl}/resources/${config.resourceId}`
136
: envUrl;
137
138
// Pre-fetch JWKS URI — try OAuth AS metadata first.
139
// Resource-specific paths serve /.well-known/oauth-authorization-server
140
// but NOT /.well-known/openid-configuration.
141
let resolvedJwksUri: URL | null = null;
142
(async () => {
143
try {
144
const urls = [
145
`${authServerBase}/.well-known/oauth-authorization-server`,
146
`${authServerBase}/.well-known/openid-configuration`,
147
];
148
for (const url of urls) {
149
const response = await fetch(url);
150
if (response.ok) {
151
const meta = (await response.json()) as {
152
jwks_uri?: string;
153
};
154
if (meta.jwks_uri) {
155
resolvedJwksUri = new URL(meta.jwks_uri);
156
console.log(
157
"[Scalekit] Resolved JWKS URI:",
158
resolvedJwksUri.toString()
159
);
160
return;
161
}
162
}
163
}
164
} catch (e) {
165
console.warn("[Scalekit] Could not pre-fetch JWKS URI:", e);
166
}
167
})();
168
169
return {
170
middleware: buildMiddleware(
171
config,
172
authServerBase,
173
() => resolvedJwksUri
174
),
175
router: buildRouter(config, authServerBase),
176
};
177
}
178
179
// --- Router (discovery endpoints) ---
180
181
function buildRouter(
182
config: ScalekitConfig,
183
authServerBase: string
184
): Router {
185
const router = Router();
186
const baseUrl = config.baseURL.replace(/\/$/, "");
187
188
// RFC 9728: Protected Resource Metadata
189
router.get(
190
"/.well-known/oauth-protected-resource",
191
(_req: Request, res: Response) => {
192
res.json({
193
resource: baseUrl,
194
authorization_servers: [authServerBase],
195
bearer_methods_supported: ["header"],
196
...(config.scopes &&
197
config.scopes.length > 0 && {
198
scopes_supported: config.scopes,
199
}),
200
});
201
}
202
);
203
204
// RFC 8414: Authorization Server Metadata (proxied from Scalekit)
205
// Tries /.well-known/oauth-authorization-server first because
206
// resource-specific paths include registration_endpoint for DCR.
207
router.get(
208
"/.well-known/oauth-authorization-server",
209
async (_req: Request, res: Response) => {
210
try {
211
const asUrl = `${authServerBase}/.well-known/oauth-authorization-server`;
212
const asRes = await fetch(asUrl);
213
if (asRes.ok) {
214
res.json(await asRes.json());
215
return;
216
}
217
const oidcUrl = `${authServerBase}/.well-known/openid-configuration`;
218
const oidcRes = await fetch(oidcUrl);
219
if (oidcRes.ok) {
220
res.json(await oidcRes.json());
221
return;
222
}
223
res.status(502).json({
224
error: "Failed to fetch authorization server metadata",
225
});
226
} catch {
227
res.status(502).json({
228
error: "Failed to fetch authorization server metadata",
229
});
230
}
231
}
232
);
233
234
return router;
235
}
236
237
// --- Middleware (token validation) ---
238
239
function buildMiddleware(
240
config: ScalekitConfig,
241
authServerBase: string,
242
getJwksUri: () => URL | null
243
): RequestHandler {
244
const wwwAuth =
245
'Bearer resource_metadata="/.well-known/oauth-protected-resource"';
246
247
return async (
248
req: Request,
249
res: Response,
250
next: NextFunction
251
) => {
252
if (!req.path.startsWith("/mcp")) {
253
next();
254
return;
255
}
256
257
const token = extractBearerToken(req.headers.authorization);
258
if (!token) {
259
res.setHeader("WWW-Authenticate", wwwAuth);
260
res.status(401).json({
261
error: "unauthorized",
262
error_description: "Missing or invalid bearer token",
263
});
264
return;
265
}
266
267
try {
268
const jwksUrl =
269
getJwksUri() ||
270
new URL(`${authServerBase}/.well-known/jwks`);
271
272
// Validate against environmentUrl — Scalekit always sets
273
// the environment URL as the JWT issuer, not the
274
// resource-specific path.
275
const claims = await verifyScalekitToken(
276
token,
277
jwksUrl,
278
config.environmentUrl.replace(/\/$/, "")
279
);
280
281
const session = claimsToSession(claims);
282
sessionContext.provider({ session }, () => next());
283
} catch {
284
res.setHeader(
285
"WWW-Authenticate",
286
`${wwwAuth}, error="invalid_token"`
287
);
288
res.status(401).json({
289
error: "invalid_token",
290
error_description: "Token verification failed",
291
});
292
}
293
};
294
}
```
Three details that are easy to get wrong: * **JWKS resolution:** Resource-specific paths on Scalekit serve `/.well-known/oauth-authorization-server` but return 404 for `/.well-known/openid-configuration`. The provider tries both in order. * **AS metadata proxy:** The resource-specific `oauth-authorization-server` response includes the `registration_endpoint` that DCR clients need. The environment-level `openid-configuration` does not. * **Issuer validation:** Scalekit JWTs always have the environment URL as `iss`, not the resource-specific path. The middleware validates against `config.environmentUrl`, not `authServerBase`. 6. ## Wire the middleware [Section titled “Wire the middleware”](#wire-the-middleware) Create `src/middleware.ts` — this is the file xmcp looks for to apply auth middleware to all requests. src/middleware.ts
```typescript
1
import { scalekitProvider } from "./lib/scalekit-auth";
2
3
export default scalekitProvider({
4
environmentUrl: process.env.SCALEKIT_ENVIRONMENT_URL!,
5
clientId: process.env.SCALEKIT_CLIENT_ID!,
6
clientSecret: process.env.SCALEKIT_CLIENT_SECRET!,
7
baseURL: process.env.BASE_URL || "http://localhost:3001",
8
resourceId: process.env.SCALEKIT_RESOURCE_ID,
9
scopes: ["openid", "profile", "email"],
10
});
```
7. ## Add tools [Section titled “Add tools”](#add-tools) xmcp uses file-based routing — each file in `src/tools/` becomes an MCP tool. Create two tools that use the authenticated session. src/tools/whoami.ts
```typescript
1
import { type ToolMetadata } from "xmcp";
2
import { getSession } from "../lib/scalekit-auth";
3
4
export const metadata: ToolMetadata = {
5
name: "whoami",
6
description:
7
"Returns the authenticated user's session information",
8
};
9
10
export default function whoami(): string {
11
const session = getSession();
12
return JSON.stringify(
13
{
14
userId: session.userId,
15
scopes: session.scopes,
16
organizationId: session.organizationId || "N/A",
17
expiresAt: session.expiresAt.toISOString(),
18
issuedAt: session.issuedAt.toISOString(),
19
},
20
null,
21
2
22
);
23
}
```
src/tools/greet.ts
```typescript
1
import { z } from "zod";
2
import { type InferSchema, type ToolMetadata } from "xmcp";
3
import { getSession } from "../lib/scalekit-auth";
4
5
export const schema = {
6
name: z.string().describe("The name to greet"),
7
};
8
9
export const metadata: ToolMetadata = {
10
name: "greet",
11
description: "Greet the user with their Scalekit identity",
12
};
13
14
export default function greet({
15
name,
16
}: InferSchema): string {
17
const session = getSession();
18
return `Hello, ${name}! Your user ID is ${session.userId}`;
19
}
```
8. ## Start the server [Section titled “Start the server”](#start-the-server) Terminal
```bash
1
npm run dev
```
You should see:
```plaintext
1
✔ MCP Server running on http://127.0.0.1:3001/mcp
2
[Scalekit] Resolved JWKS URI: https://.scalekit.com/keys
```
The JWKS log confirms the server successfully connected to Scalekit and can validate tokens. 9. ## Test with MCP Inspector [Section titled “Test with MCP Inspector”](#test-with-mcp-inspector) Verify the full flow using MCP Inspector. Terminal
```bash
1
npx @modelcontextprotocol/inspector@latest
```
In the Inspector UI: 1. Set transport to **Streamable HTTP**, URL to `http://localhost:3001/mcp`, connection to **Direct** 2. Get an access token using client credentials: Terminal
```bash
1
curl -s -X POST "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
2
-H "Content-Type: application/x-www-form-urlencoded" \
3
-d "grant_type=client_credentials\
4
&client_id=$SCALEKIT_CLIENT_ID\
5
&client_secret=$SCALEKIT_CLIENT_SECRET" | jq .access_token -r
```
3. Enable the **Authorization** custom header and set its value to `Bearer ` 4. Click **Connect** — you should see `whoami` and `greet` in the Tools tab You now have a working xmcp MCP server with Scalekit OAuth 2.1 authentication. Add more tools by creating files in `src/tools/`, and access the authenticated user from any tool via `getSession()`. For production deployment, build with `npm run build` and run the compiled output with `npm start`.