Mcp auth fastapi fastmcp scalekit
Skill scalekit-inc/skills/skills/mcp-auth-fastapi-fastmcp-scalekit
Add Scalekit OAuth authentication to a FastAPI + FastMCP server (Python). Use when you need FastAPI-level middleware control over token validation alongside FastMCP tools. Implements /.well-known/oauth-protected-resource, a Starlette middleware that validates Authorization Bearer tokens via Scalekit SDK (issuer + audience), and mounts FastMCP via app.mount.From its SKILL.md
npx -y skills add scalekit-inc/skills --skill mcp-auth-fastapi-fastmcp-scalekitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.4 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it
Add OAuth auth to FastAPI + FastMCP (Scalekit)
When to use this skill vs the FastMCP skill
Use THIS skill when:
- You need custom FastAPI middleware, routes, or dependency injection alongside MCP tools
- You're adding MCP to an existing FastAPI application
- You need more control over request handling than FastMCP's built-in ScalekitProvider offers
Use the mcp-auth-fastmcp-scalekit skill instead when:
- You're building a standalone MCP server and don't need FastAPI-specific features
- You want the simplest possible setup (5-line auth config)
Critical wiring: how FastMCP mounts into FastAPI
FastMCP is NOT run standalone. It is mounted inside FastAPI:
mcp_app = mcp.http_app(path="/")
app = FastAPI(lifespan=mcp_app.lifespan) # lifespan MUST come from mcp_app
# ... add middleware and routes BEFORE mounting ...
app.mount("/", mcp_app) # MUST be last
Order matters:
- Create mcp_app from FastMCP
- Create FastAPI with lifespan=mcp_app.lifespan
- Add CORSMiddleware
- Add @app.middleware("http") auth middleware
- Add GET /.well-known/oauth-protected-resource (public)
- Add GET /health (public)
- app.mount("/", mcp_app) — ALWAYS last
Inputs to collect (ask if missing)
- PORT (default 3002)
- SK_ENV_URL, SK_CLIENT_ID, SK_CLIENT_SECRET
- EXPECTED_AUDIENCE — must match the Server URL registered in Scalekit (with trailing slash, e.g. http://localhost:3002/)
- PROTECTED_RESOURCE_METADATA JSON — copied from Scalekit dashboard MCP server page
Required outcomes
- GET /.well-known/oauth-protected-resource → returns PROTECTED_RESOURCE_METADATA JSON (no auth required)
- GET /health → {"status": "healthy"} (no auth required)
- @app.middleware("http") auth_middleware:
- Exempts /.well-known/oauth-protected-resource and /health
- Extracts Authorization: Bearer <token>
- On missing token → 401 + WWW-Authenticate header
- Validates via scalekit_client.validate_access_token(token, options=TokenValidationOptions(issuer=SK_ENV_URL, audience=[EXPECTED_AUDIENCE]))
- On invalid → 401 + WWW-Authenticate
- FastMCP mounted at / with lifespan=mcp_app.lifespan
- At least one @mcp.tool registered
Mode A — New project scaffold
Steps
-
Create directory and virtual environment:
mkdir -p <project-name> cd <project-name> python3 -m venv .venv source .venv/bin/activate -
Install dependencies:
pip install -r assets/requirements.txt -
Create .env from assets/env.example (fill real values from Scalekit dashboard).
-
Use assets/main-minimal.py as starting point. Add your tools using assets/tool-template.py.
-
Run:
python main.py -
Test with MCP Inspector (point to http://localhost:3002/ — NOT /mcp):
npx @modelcontextprotocol/inspector@latest
Mode B — Retrofit an existing FastAPI app
Identify insertion points
Ask for the existing server entrypoint. Look for:
- Where
app = FastAPI(...)is instantiated - Whether
app.mount(...)is already used - Existing middleware definitions and their order
- Whether POST / is already taken (if so, mount MCP at /mcp instead and update RESOURCE_METADATA_URL)
Patch plan (minimal diffs)
1. Add env vars
SK_ENV_URL=...
SK_CLIENT_ID=...
SK_CLIENT_SECRET=...
EXPECTED_AUDIENCE=http://localhost:3002/
PROTECTED_RESOURCE_METADATA='...'
2. Add imports
from fastmcp import FastMCP, Context
from scalekit import ScalekitClient
from scalekit.common.scalekit import TokenValidationOptions
from starlette.middleware.cors import CORSMiddleware
from fastapi import Request, Response
3. Add Scalekit client + constants
RESOURCE_METADATA_URL = f"http://localhost:{PORT}/.well-known/oauth-protected-resource"
WWW_HEADER = {
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{RESOURCE_METADATA_URL}"'
}
scalekit_client = ScalekitClient(
env_url=SK_ENV_URL,
client_id=SK_CLIENT_ID,
client_secret=SK_CLIENT_SECRET,
)
4. Create FastMCP instance + tools BEFORE patching FastAPI
mcp = FastMCP("Your Server Name", stateless_http=True)
@mcp.tool(name="your_tool", description="...")
async def your_tool(...) -> dict:
...
5. Patch FastAPI instantiation
Before:
app = FastAPI()
After:
mcp_app = mcp.http_app(path="/")
app = FastAPI(lifespan=mcp_app.lifespan)
WARNING: If the existing app already sets lifespan, merge the lifespans using an async context manager.
6. Add CORSMiddleware (before auth middleware)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"]
)
7. Add auth middleware
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
PUBLIC_PATHS = {"/health", "/.well-known/oauth-protected-resource"}
if request.url.path in PUBLIC_PATHS:
return await call_next(request)
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_HEADER,
media_type="application/json"
)
token = auth_header.split("Bearer ", 1)[0].strip()
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)
8. Add public routes
@app.get("/.well-known/oauth-protected-resource")
async def oauth_metadata():
if not PROTECTED_RESOURCE_METADATA:
return Response('{"error": "config missing"}', status_code=500, media_type="application/json")
return Response(json.dumps(json.loads(PROTECTED_RESOURCE_METADATA), indent=2), media_type="application/json")
@app.get("/health")
async def health_check():
return {"status": "healthy"}
9. Mount FastMCP LAST
app.mount("/", mcp_app)
Verification checklist
- GET /.well-known/oauth-protected-resource returns JSON without auth
- GET /health returns {"status": "healthy"}
- POST to MCP endpoint without token → 401 + WWW-Authenticate
- POST to MCP endpoint with valid token → tool response
- Wrong-audience token → 401
- Tool call works end-to-end in MCP Inspector (connect to http://localhost:PORT/)
See references/TROUBLESHOOTING.md for common issues.
What ships with it: 7 files
25.0 KB alongside SKILL.md, 3 of them executable
assets/
- env.example241 B
- main-minimal.pyruns2.6 KB
- main.pyruns4.1 KB
- requirements.txt209 B
- tool-template.pyruns527 B
references/
- REFERENCE.md7.7 KB
- TROUBLESHOOTING.md9.6 KB