Guardrails
- MUST use Streamable HTTP transport — stdio does not support OAuth.
- MUST validate the bearer token's audience (and required scopes, if enforcing scope-based authorization) before executing any tool logic.
- MUST NOT protect the
/.well-known/oauth-protected-resourcediscovery endpoint with auth middleware — it must remain publicly accessible. - MUST return 401 with a
WWW-Authenticateheader on missing or invalid tokens; a bare 401 causes AI hosts to fail silently.
Prerequisite: HTTP transport
MCP OAuth requires Streamable HTTP transport. Stdio does not support OAuth.
Node.js: Use StreamableHTTPServerTransport from @modelcontextprotocol/sdk/server/streamableHttp.js
Python: Use mcp.streamable_http_app(path="/mcp") and run with uvicorn module:app
If currently using stdio, migrate to HTTP first. See MCP Transport Docs.
Setup workflow
Copy this checklist and track progress:
MCP OAuth Setup:
- [ ] Step 1: Install Scalekit SDK
- [ ] Step 2: Register MCP server in Scalekit dashboard
- [ ] Step 3: Implement discovery endpoint
- [ ] Step 4: Add token validation middleware
- [ ] Step 5: (Optional) Add scope-based authorization
- [ ] Step 6: Test with AI hosts
Step 1: Install Scalekit SDK
Node.js:
npm install @scalekit-sdk/node
Python:
pip install scalekit-sdk-python
Get credentials from Scalekit dashboard after creating an account.
Step 2: Register MCP server
User action (dashboard) — tell the user to complete this, then wait for confirmation. Do not attempt to automate the Scalekit UI.
- Open app.scalekit.com → MCP servers → Add MCP server
- Provide a descriptive name (appears on consent page)
- Enable dynamic client registration (automatic MCP host registration)
- Enable Client ID Metadata Document (CIMD)
- Save, then restart the MCP server process after DCR/CIMD toggles
Advanced settings (optional):
- Server URL: public MCP identifier — use one consistent base URL everywhere (example throughout this skill:
https://mcp.yourapp.com) - Access token lifetime: 300-3600 seconds recommended
- Scopes: e.g.
todo:read,todo:write
After the user confirms registration, continue with code in Steps 3–5.
Step 3: Implement discovery endpoint
Create /.well-known/oauth-protected-resource endpoint. Copy metadata JSON from Dashboard > MCP Servers > Your server > Metadata JSON.
Node.js (Express):
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
"authorization_servers": [
"https://<SCALEKIT_ENVIRONMENT_URL>/resources/<YOUR_RESOURCE_ID>"
],
"bearer_methods_supported": ["header"],
"resource": "https://mcp.yourapp.com",
"resource_documentation": "https://mcp.yourapp.com/docs",
"scopes_supported": ["todo:read", "todo:write"]
});
});
Python (FastAPI):
@app.get("/.well-known/oauth-protected-resource")
async def get_oauth_protected_resource():
return {
"authorization_servers": [
"https://<SCALEKIT_ENVIRONMENT_URL>/resources/<YOUR_RESOURCE_ID>"
],
"bearer_methods_supported": ["header"],
"resource": "https://mcp.yourapp.com",
"resource_documentation": "https://mcp.yourapp.com/docs",
"scopes_supported": ["todo:read", "todo:write"]
}
Replace placeholders with actual values from Scalekit dashboard.
Step 4: Add token validation middleware
Initialize Scalekit client
Node.js:
import { ScalekitClient } from '@scalekit-sdk/node';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const RESOURCE_ID = 'https://mcp.yourapp.com'; // Or autogenerated ID from dashboard
const METADATA_ENDPOINT = 'https://mcp.yourapp.com/.well-known/oauth-protected-resource';
export const WWWHeader = {
HeaderKey: 'WWW-Authenticate',
HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"`
};
Python:
from scalekit import ScalekitClient
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://mcp.yourapp.com"
METADATA_ENDPOINT = "https://mcp.yourapp.com/.well-known/oauth-protected-resource"
WWW_HEADER = {
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"'
}
Implement authentication middleware
Node.js:
export async function authMiddleware(req, res, next) {
try {
// Allow public access to well-known endpoints
if (req.path.includes('.well-known')) {
return next();
}
// Extract Bearer token
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');
}
// Validate token against resource audience
await scalekit.validateToken(token, {
audience: [RESOURCE_ID]
});
next();
} catch (err) {
return res
.status(401)
.set(WWWHeader.HeaderKey, WWWHeader.HeaderValue)
.end();
}
}
// Apply to all MCP endpoints
app.use('/', authMiddleware);
Python:
from scalekit.common.scalekit import TokenValidationOptions
from fastapi import Request, HTTPException, status
async def auth_middleware(request: Request, call_next):
# Allow public access to well-known endpoints
if request.url.path.startswith("/.well-known"):
return await call_next(request)
# Extract Bearer token
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
)
# Validate token
try:
options = TokenValidationOptions(
issuer=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
audience=[RESOURCE_ID]
)
scalekit_client.validate_access_token_and_get_claims(token, options=options)
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers=WWW_HEADER
)
return await call_next(request)
# Apply to all MCP endpoints
app.middleware("http")(auth_middleware)
Step 5: Scope-based tool authorization (Optional)
Add fine-grained access control at the tool execution level:
Node.js:
try {
await scalekit.validateToken(token, {
audience: [RESOURCE_ID],
requiredScopes: [scope] // e.g., 'todo:write'
});
} catch(error) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scope: ${scope}`,
scope: scope
});
}
Python:
try:
scalekit_client.validate_access_token(
token,
options=TokenValidationOptions(
audience=[RESOURCE_ID],
required_scopes=[scope]
)
)
except Exception:
return {
"error": "insufficient_scope",
"error_description": f"Required scope: {scope}",
"scope": scope
}
Step 6: Verify and deploy
Verify your integration
Before testing with AI hosts, the coding agent will scan your project to determine the right URL to verify against. It will look for:
RESOURCE_IDorresourcevalues in your code or.env- The host/domain used in
/.well-known/oauth-protected-resource - Any deployed base URL in environment config (
SERVER_URL,PUBLIC_URL, etc.)
If no URL is found, you'll be asked:
"What is your MCP server base URL? (e.g.,
https://mcp.yourapp.comorhttps://mcp.yourapp.com/mcp)"
Once the URL is known, run these three checks:
Check 1 – Confirm 401 without token:
curl -i <your-mcp-url>
Expected: HTTP/1.1 401 Unauthorized
Check 2 – Confirm WWW-Authenticate header: The response must include:
WWW-Authenticate: Bearer realm="OAuth", resource_metadata="https://<your-domain>/.well-known/oauth-protected-resource"
This is what triggers the MCP client's OAuth flow. A plain 401 without this header will cause AI hosts (Claude Desktop, Cursor, VS Code) to fail silently.
Check 3 – Confirm metadata endpoint is reachable:
curl https://<your-domain>/.well-known/oauth-protected-resource
Expected: JSON with resource, authorization_servers, and scopes_supported.
After verification passes, test with Claude Desktop, Cursor, and VS Code. Ensure invalid tokens get 401, and scope-based authorization (if implemented) rejects insufficient scopes.
Framework-specific references
- FastMCP (Python, simplest): fastmcp-reference.md
- Express.js (Node.js): express-reference.md
- FastAPI + FastMCP (Python, custom middleware): fastapi-reference.md
Common issues
Token validation fails:
- Verify RESOURCE_ID matches Server URL in dashboard
- Check environment variables are set correctly
- Ensure token hasn't expired
Discovery endpoint not found:
- Verify endpoint path is exactly
/.well-known/oauth-protected-resource - Check endpoint is publicly accessible (not protected by auth middleware)
Scope validation errors:
- Verify scopes in dashboard match those in code
- Check token includes required scopes
- Ensure scope strings match exactly (case-sensitive)