FastMCP OAuth with Scalekit Provider
Secure your FastMCP server with OAuth 2.1 in just 5 lines of code using Scalekit's built-in provider. This approach handles token validation, scope enforcement, and authentication flows automatically.
FastMCP advantage
Standard MCP OAuth: ~30 lines of middleware code, manual token validation FastMCP with Scalekit provider: ~5 lines of configuration, automatic token handling
Setup workflow
Copy this checklist and track progress:
FastMCP OAuth Setup:
- [ ] Step 1: Register MCP server in Scalekit
- [ ] Step 2: Install FastMCP and dependencies
- [ ] Step 3: Configure Scalekit provider
- [ ] Step 4: Add scope validation to tools
- [ ] Step 5: Test with MCP Inspector
Step 1: Register MCP server
In Scalekit dashboard:
- Navigate to Dashboard > MCP Servers > Add MCP Server
- Enter server name (e.g.,
FastMCP Todo Server) - Set Server URL to
http://localhost:3002/(include trailing slash) - Define scopes for your tools (e.g.,
todo:read,todo:write) - Click Save and note the
resource_id
Critical: Use base URL with trailing slash. FastMCP appends /mcp automatically.
- ✓ Correct:
http://localhost:3002/ - ✗ Wrong:
http://localhost:3002/mcp
Step 2: Install dependencies
Create project structure:
mkdir fastmcp-server
cd fastmcp-server
python3 -m venv venv
source venv/bin/activate
Create requirements.txt:
fastmcp>=2.13.0.2
python-dotenv>=1.0.0
Install:
pip install -r requirements.txt
Step 3: Configure Scalekit provider
Create .env file with Scalekit credentials:
PORT=3002
SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
SCALEKIT_CLIENT_ID=your_client_id
SCALEKIT_RESOURCE_ID=res_your_resource_id
MCP_URL=http://localhost:3002/
Get these values from Scalekit Dashboard > Settings and your MCP server configuration.
Initialize FastMCP server with Scalekit provider (server.py):
import os
from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
load_dotenv()
# 5-line OAuth setup
mcp = FastMCP(
"Your Server Name",
stateless_http=True,
auth=ScalekitProvider(
environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
resource_id=os.getenv("SCALEKIT_RESOURCE_ID"),
mcp_url=os.getenv("MCP_URL"),
),
)
if __name__ == "__main__":
mcp.run(transport="http", port=int(os.getenv("PORT", "3002")))
That's it! The Scalekit provider handles:
- Token validation on every request
- OAuth flow with MCP clients
- WWW-Authenticate header responses
- Discovery endpoint (
/.well-known/oauth-protected-resource)
Step 4: Add scope validation to tools
Use the built-in get_access_token() dependency to validate scopes:
from fastmcp.server.dependencies import AccessToken, get_access_token
def _require_scope(scope: str) -> str | None:
"""Validate request token has required scope."""
token: AccessToken = get_access_token()
if scope not in token.scopes:
return f"Insufficient permissions: `{scope}` scope required."
return None
@mcp.tool
def create_todo(title: str, description: str = None) -> dict:
"""Create a new todo item. Requires todo:write scope."""
error = _require_scope("todo:write")
if error:
return {"error": error}
# Your tool implementation
todo_id = str(uuid.uuid4())
return {"id": todo_id, "title": title, "description": description}
@mcp.tool
def list_todos() -> dict:
"""List all todos. Requires todo:read scope."""
error = _require_scope("todo:read")
if error:
return {"error": error}
# Your tool implementation
return {"todos": [...]}
Pattern: Every tool that requires authorization should call _require_scope() first.
Step 5: Test with MCP Inspector
Run your server:
source venv/bin/activate
python server.py
Launch MCP Inspector:
npx @modelcontextprotocol/inspector@latest
In Inspector:
- Enter URL:
http://localhost:3002/mcp - Leave authentication fields empty (uses dynamic client registration)
- Click Connect
- Complete OAuth flow when prompted
- Test tools with scoped tokens
Testing scope enforcement:
- Call
create_todowith token that only hastodo:read→ should fail - Call
list_todoswithtodo:readscope → should succeed
Complete example: Todo server
import os
import uuid
from dataclasses import dataclass, asdict
from typing import Optional
from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
from fastmcp.server.dependencies import AccessToken, get_access_token
load_dotenv()
mcp = FastMCP(
"Todo Server",
stateless_http=True,
auth=ScalekitProvider(
environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
resource_id=os.getenv("SCALEKIT_RESOURCE_ID"),
mcp_url=os.getenv("MCP_URL"),
),
)
@dataclass
class TodoItem:
id: str
title: str
description: Optional[str]
completed: bool = False
def to_dict(self) -> dict:
return asdict(self)
_TODO_STORE: dict[str, TodoItem] = {}
def _require_scope(scope: str) -> Optional[str]:
token: AccessToken = get_access_token()
if scope not in token.scopes:
return f"Insufficient permissions: `{scope}` scope required."
return None
@mcp.tool
def create_todo(title: str, description: Optional[str] = None) -> dict:
error = _require_scope("todo:write")
if error:
return {"error": error}
todo = TodoItem(id=str(uuid.uuid4()), title=title, description=description)
_TODO_STORE[todo.id] = todo
return {"todo": todo.to_dict()}
@mcp.tool
def list_todos(completed: Optional[bool] = None) -> dict:
error = _require_scope("todo:read")
if error:
return {"error": error}
todos = [
todo.to_dict()
for todo in _TODO_STORE.values()
if completed is None or todo.completed == completed
]
return {"todos": todos}
@mcp.tool
def get_todo(todo_id: str) -> dict:
error = _require_scope("todo:read")
if error:
return {"error": error}
todo = _TODO_STORE.get(todo_id)
if todo is None:
return {"error": f"Todo `{todo_id}` not found."}
return {"todo": todo.to_dict()}
@mcp.tool
def update_todo(
todo_id: str,
title: Optional[str] = None,
description: Optional[str] = None,
completed: Optional[bool] = None,
) -> dict:
error = _require_scope("todo:write")
if error:
return {"error": error}
todo = _TODO_STORE.get(todo_id)
if todo is None:
return {"error": f"Todo `{todo_id}` not found."}
if title is not None:
todo.title = title
if description is not None:
todo.description = description
if completed is not None:
todo.completed = completed
return {"todo": todo.to_dict()}
@mcp.tool
def delete_todo(todo_id: str) -> dict:
error = _require_scope("todo:write")
if error:
return {"error": error}
todo = _TODO_STORE.pop(todo_id, None)
if todo is None:
return {"error": f"Todo `{todo_id}` not found."}
return {"deleted": todo_id}
if __name__ == "__main__":
mcp.run(transport="http", port=int(os.getenv("PORT", "3002")))
Environment variable reference
| Variable | Description | Example |
|---|---|---|
SCALEKIT_ENVIRONMENT_URL |
Your Scalekit environment URL | https://yourenv.scalekit.com |
SCALEKIT_CLIENT_ID |
Client ID from Scalekit dashboard | skc_... |
SCALEKIT_RESOURCE_ID |
MCP server resource ID | res_... |
MCP_URL |
Base URL with trailing slash | http://localhost:3002/ |
PORT |
HTTP server port | 3002 |
Scope design patterns
Read-only operations: Use *:read scope
todo:read,data:read,user:read
Write operations: Use *:write scope
todo:write,data:write,user:write
Admin operations: Use *:admin scope
system:admin,user:admin
Multiple scopes per tool: Return error if ANY required scope is missing
def _require_scopes(scopes: list[str]) -> str | None:
token: AccessToken = get_access_token()
missing = [s for s in scopes if s not in token.scopes]
if missing:
return f"Missing scopes: {', '.join(missing)}"
return None
@mcp.tool
def admin_action() -> dict:
error = _require_scopes(["todo:write", "admin:access"])
if error:
return {"error": error}
# Implementation
Production deployment
Security checklist
- Store
.envin secret manager (AWS Secrets Manager, Vault) - Use HTTPS for all public endpoints
- Rotate
SCALEKIT_CLIENT_SECRETregularly - Enable rate limiting on MCP endpoints
- Log all authentication failures
- Monitor token validation errors
Environment-specific configuration
Development:
MCP_URL=http://localhost:3002/
Production:
MCP_URL=https://mcp.yourapp.com/
Update Scalekit dashboard with production URL before deploying.
Common issues
Token validation fails:
- Verify
SCALEKIT_RESOURCE_IDmatches dashboard - Check
MCP_URLhas trailing slash - Ensure environment variables are loaded (
load_dotenv())
Discovery endpoint not found:
- Confirm server is running on correct port
- Verify MCP client uses base URL +
/mcppath - Check firewall/network allows connections
Scope errors persist:
- Verify scopes are defined in Scalekit dashboard
- Check scope strings match exactly (case-sensitive)
- Ensure token was issued with required scopes
MCP Inspector connection fails:
- Leave auth fields empty (uses DCR)
- Check browser console for OAuth errors
- Verify server logs show authentication attempt
Extending your server
Adding new tools with scopes
@mcp.tool
def new_operation(param: str) -> dict:
"""Your tool description."""
error = _require_scope("your:scope")
if error:
return {"error": error}
# Your implementation
return {"result": "success"}
Multiple scope requirements
@mcp.tool
def sensitive_operation() -> dict:
"""Requires multiple scopes."""
error = _require_scopes(["data:read", "data:write", "admin:access"])
if error:
return {"error": error}
# Your implementation
return {"result": "success"}
Optional scope enhancement
@mcp.tool
def flexible_operation() -> dict:
"""Returns different data based on scopes."""
token: AccessToken = get_access_token()
# Basic data for all authenticated users
result = {"basic": "data"}
# Enhanced data if user has admin scope
if "admin:access" in token.scopes:
result["admin"] = "enhanced_data"
return result
Complete Working Example
The complete FastMCP todo server shown above is available in the Scalekit MCP Auth Demos repository:
GitHub Repository: scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp
This example demonstrates:
- Full CRUD operations with scope-based authorization
- In-memory todo storage for testing
- OAuth 2.1 integration via FastMCP ScalekitProvider
- Production-ready error handling and logging
Resources
- Full example: GitHub - todo-fastmcp
- Scalekit MCP Auth Demos
- FastMCP docs: fastmcp.dev
- Scalekit docs: docs.scalekit.com/authenticate/mcp/fastmcp-quickstart
- MCP Inspector:
npx @modelcontextprotocol/inspector@latest