| name | mcp-oauth-fastmcp |
| description | Add OAuth 2.1 authorization to FastMCP servers using Scalekit provider plugin. Use when building FastMCP servers, when users mention FastMCP authentication, Python MCP servers with Scalekit, or need rapid OAuth integration with minimal code. |
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()
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}
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}
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_todo with token that only has todo:read → should fail
- Call
list_todos with todo:read scope → 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
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}
Production deployment
Security checklist
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_ID matches dashboard
- Check
MCP_URL has 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 +
/mcp path
- 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}
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}
return {"result": "success"}
Optional scope enhancement
@mcp.tool
def flexible_operation() -> dict:
"""Returns different data based on scopes."""
token: AccessToken = get_access_token()
result = {"basic": "data"}
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