plugins/mcp-auth/skills/fastapi-fastmcp/SKILL.md
# FastAPI + FastMCP OAuth Authentication with Scalekit ## Overview This skill documents the pattern for building production-ready MCP (Model Context Protocol) servers using FastAPI and FastMCP with OAuth 2.1 Bearer token authentication via Scalekit. This approach provides fine-grained control over authentication middleware, token validation, and server behavior compared to using FastMCP's built-in OAuth provider. ## When to Use This Pattern Use this FastAPI + FastMCP integration when you nee
npx skillsauth add scalekit-inc/claude-code-authstack plugins/mcp-auth/skills/fastapi-fastmcpInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill documents the pattern for building production-ready MCP (Model Context Protocol) servers using FastAPI and FastMCP with OAuth 2.1 Bearer token authentication via Scalekit. This approach provides fine-grained control over authentication middleware, token validation, and server behavior compared to using FastMCP's built-in OAuth provider.
Use this FastAPI + FastMCP integration when you need:
Don't use this pattern if FastMCP's built-in OAuth provider meets your needs—the additional FastAPI layer adds complexity.
MCP Client → FastAPI Server (401 + WWW-Authenticate)
MCP Client → Scalekit (Exchange code for token)
Scalekit → MCP Client (Bearer token)
MCP Client → FastAPI Server (Request + Bearer token)
FastAPI Middleware → Scalekit SDK (Validate token)
FastAPI → MCP Tool Handler → Response
/.well-known/oauth-protected-resource) for client discovery@mcp.tool decoratorRequired variables:
SK_ENV_URL: Scalekit environment URL (issuer)SK_CLIENT_ID + SK_CLIENT_SECRET: SDK authentication credentialsEXPECTED_AUDIENCE: The resource identifier that tokens must targetPROTECTED_RESOURCE_METADATA: Complete OAuth discovery metadata JSONPORT: Server listening port (must match registered server URL)Security:
.env files to version controlSK_CLIENT_SECRET regularlyEXPECTED_AUDIENCE matches your server's public URL exactly@app.middleware("http")
async def auth_middleware(request: Request, call_next):
# Exempt public endpoints
if request.url.path in {"/health", "/.well-known/oauth-protected-resource"}:
return await call_next(request)
# Extract Bearer token
auth_header = request.headers.get("authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return Response(
'{"error": "Missing Bearer token"}',
status_code=401,
headers={"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{RESOURCE_METADATA_URL}"'},
media_type="application/json"
)
token = auth_header.split("Bearer ", 1)[1].strip()
# Validate with Scalekit SDK
options = TokenValidationOptions(
issuer=SK_ENV_URL,
audience=[EXPECTED_AUDIENCE]
)
try:
is_valid = scalekit_client.validate_access_token(token, options=options)
if not is_valid:
raise ValueError("Invalid token")
except Exception:
return Response(
'{"error": "Token validation failed"}',
status_code=401,
headers=WWW_HEADER,
media_type="application/json"
)
return await call_next(request)
Key principles:
@app.get("/.well-known/oauth-protected-resource")
async def oauth_metadata():
if not PROTECTED_RESOURCE_METADATA:
return Response(
'{"error": "PROTECTED_RESOURCE_METADATA config missing"}',
status_code=500,
media_type="application/json"
)
metadata = json.loads(PROTECTED_RESOURCE_METADATA)
return Response(
json.dumps(metadata, indent=2),
media_type="application/json"
)
Purpose:
@mcp.tool(
name="greet_user",
description="Greets the user with a personalized message."
)
async def greet_user(name: str, ctx: Context | None = None) -> dict:
return {
"content": [
{
"type": "text",
"text": f"Hi {name}, welcome to Scalekit!"
}
]
}
Tool design:
ctx: Context | None to access authenticated user context in futurecontent arraymcp_app = mcp.http_app(path="/")
app = FastAPI(lifespan=mcp_app.lifespan)
# Add middleware (CORS, auth, etc.)
app.add_middleware(CORSMiddleware, ...)
# Add custom endpoints
@app.get("/health")
async def health_check():
return {"status": "healthy"}
# Mount MCP at root
app.mount("/", mcp_app)
Layering order:
exp claimuvicorn main:app --workers 4npx @modelcontextprotocol/inspector@latest
http://localhost:3002/# Get token from Scalekit (via OAuth flow or test endpoint)
export TOKEN="<your-access-token>"
# Test authenticated request
curl -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"method":"tools/call","params":{"name":"greet_user","arguments":{"name":"Saif"}}}' \
http://localhost:3002/
# Test missing token (should return 401)
curl -v http://localhost:3002/
import pytest
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_missing_token():
response = client.post("/")
assert response.status_code == 401
assert "WWW-Authenticate" in response.headers
def test_invalid_token():
response = client.post(
"/",
headers={"Authorization": "Bearer invalid-token"}
)
assert response.status_code == 401
def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
Symptom: Tokens fail validation with "invalid audience" error
Cause: EXPECTED_AUDIENCE doesn't match the Server URL registered in Scalekit
Fix: Ensure both have identical values including trailing slashes
Symptom: CORS errors or authentication bypassed Cause: Mounting order affects execution sequence Fix: Add middleware before mounting MCP app; mount MCP last
Symptom: Clients can't discover how to authenticate
Cause: PROTECTED_RESOURCE_METADATA not set or endpoint not working
Fix: Verify metadata JSON is copied correctly from Scalekit dashboard
Symptom: Works locally but fails in production
Cause: Hardcoded localhost URLs in configuration
Fix: Use environment-specific values for EXPECTED_AUDIENCE and RESOURCE_METADATA_URL
Symptom: Tests pass initially then fail after 1 hour Cause: Access tokens expire (default 3600 seconds) Fix: Refresh tokens before each test run or implement automatic refresh
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
# ... existing token validation ...
# Decode token to access claims (after validation)
import jwt
decoded = jwt.decode(token, options={"verify_signature": False})
scopes = decoded.get("scope", "").split()
# Attach to request state
request.state.scopes = scopes
request.state.user_id = decoded.get("sub")
return await call_next(request)
@mcp.tool()
async def admin_tool(ctx: Context) -> dict:
# Access request state in tool
if "admin" not in ctx.request_context.state.scopes:
raise PermissionError("Requires admin scope")
# ... tool logic ...
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
# ... validate token ...
decoded = jwt.decode(token, options={"verify_signature": False})
org_id = decoded.get("org_id")
request.state.org_id = org_id
return await call_next(request)
@mcp.tool()
async def get_org_data(ctx: Context) -> dict:
org_id = ctx.request_context.state.org_id
# Fetch data scoped to org_id
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/")
@limiter.limit("10/minute")
async def mcp_endpoint(request: Request):
# Rate-limited MCP calls
pass
mcp>=1.0.0 # MCP protocol implementation
fastapi>=0.104.0 # Web framework
fastmcp>=0.8.0 # FastMCP integration
uvicorn>=0.24.0 # ASGI server
pydantic>=2.5.0 # Data validation
python-dotenv>=1.0.0 # Environment variables
httpx>=0.25.0 # HTTP client
python-jose[cryptography]>=3.3.0 # JWT handling
cryptography>=41.0.0 # Cryptographic operations
scalekit-sdk-python>=2.4.0 # Scalekit authentication
starlette>=0.27.0 # ASGI toolkit
requirements.txt>= for development flexibilityA full production-ready FastAPI + FastMCP server is available in the Scalekit MCP Auth Demos repository:
GitHub Repository: scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python
This example includes:
main.py - Main server entry point with FastAPI appsrc/config/config.py - Environment configurationsrc/lib/auth.py - OAuth discovery endpoint handlersrc/lib/middleware.py - Token validation middlewaresrc/lib/transport.py - MCP transport layer setupcd greeting-mcp-python
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python main.py
See README.md for complete setup instructions.
development
Walks through a structured production readiness checklist for Scalekit SSO implementations. Use when the user says they are going live, launching to production, doing a pre-launch review, hardening their SSO setup, or wants to verify their Scalekit implementation is production-ready.
data-ai
Implements complete SSO and authentication flows using Scalekit. Handles modular SSO, IdP-initiated login, user session management, and enterprise customer onboarding. Use when adding authentication, SSO, SAML, OIDC, or user login to applications.
testing
Implements Scalekit's admin portal for customer self-serve SSO and SCIM configuration. Generates portal links server-side and embeds the portal as an iframe in the app's settings UI. Use when the user asks to add an admin portal, customer self-serve SSO setup, iframe embed for SSO config, shareable setup link, or let customers configure their own SSO or SCIM connection.
development
Walks through a structured production readiness checklist for Scalekit SCIM provisioning implementations. Use when the user says they are going live, launching to production, doing a pre-launch review, or wants to verify their SCIM directory sync implementation is production-ready.