⏳
Loading cheatsheet...
Build production MCP servers with FastMCP, typed tools, auth, transports, composition, and deployment patterns.
| Concept | Details |
|---|---|
| MCP | Model Context Protocol — open standard by Anthropic for AI-tool communication |
| Purpose | Let LLMs call tools, read resources, and use prompts from external servers |
| Transport | stdio (local) or SSE/HTTP (remote) |
| Architecture | Client (Claude, IDE…) ↔ Server (your tools) via JSON-RPC 2.0 |
| Spec | github.com/modelcontextprotocol/specification |
| Raw MCP SDK | FastMCP |
|---|---|
| Manual JSON schema definitions | Auto-generated from Python types |
| No built-in auth | Bearer auth out of the box |
| No server composition | Mount multiple servers |
| Verbose error handling | Pydantic validation + ToolError |
| No CLI tooling | fastmcp dev/run/deploy |
| Steeper learning curve | Familiar Flask/FastAPI-style API |
@mcp.tool() functions. Schema generation, validation, error handling, and transport are all handled automatically.# Install FastMCP
pip install fastmcp
# With server extras
pip install "fastmcp[server]"
# With CLI extras
pip install "fastmcp[cli]"
# Install everything
pip install "fastmcp[server,cli]"
# Verify installation
python -c "import fastmcp; print(fastmcp.__version__)"from fastmcp import FastMCP
# Create a server instance
mcp = FastMCP("My Server")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@mcp.tool()
def get_weather(city: str) -> str:
"""Get weather for a city"""
return f"Weather in {city}: 72°F Sunny"
if __name__ == "__main__":
mcp.run()@mcp.tool() function is sent to the LLM as the tool description. Write clear, specific docstrings — they directly impact how well the AI understands and uses your tools.| Mode | Command | Features |
|---|---|---|
| Development | fastmcp dev server.py | Auto-reload on file changes, verbose logging |
| Production | fastmcp run server.py | Optimized, no reload, stdio transport |
| As module | python -m fastmcp run server.py | Same as fastmcp run |
| HTTP/SSE | fastmcp run server.py --transport sse | Serve over HTTP with Server-Sent Events |
| Transport | Flag | Use Case |
|---|---|---|
| stdio (default) | — (default) | Local Claude Desktop, IDE integration |
| SSE | --transport sse | Remote access, web-based clients |
| Streamable HTTP | --transport streamable-http | Newer HTTP streaming transport |
# Development with auto-reload
fastmcp dev server.py
# Production run
fastmcp run server.py
# Run as module
python -m fastmcp run server.py
# Run with SSE transport
fastmcp run server.py --transport sse --host 0.0.0.0 --port 8000
# Run with custom host/port
fastmcp run server.py --host 127.0.0.1 --port 9000fastmcp dev during development. It watches your Python files for changes and automatically restarts the server. The --transport sse flag is essential when you need to expose your server over the network (not just stdio).from fastmcp import FastMCP
mcp = FastMCP("Advanced Server")
# ── Simple tool with type hints ──
@mcp.tool()
def search(query: str, limit: int = 10) -> list[str]:
"""Search for documents matching a query"""
return [f"Result {i}: {query}" for i in range(limit)]
# ── Async tool ──
@mcp.tool()
async def fetch_data(url: str) -> dict:
"""Fetch data from a URL asynchronously"""
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get(url)
return {"status": resp.status_code, "data": resp.text[:500]}
# ── Tool with Pydantic model ──
from pydantic import BaseModel, Field
class SearchParams(BaseModel):
query: str = Field(..., description="Search query string")
limit: int = Field(10, ge=1, le=100, description="Max results")
category: str | None = Field(None, description="Filter by category")
@mcp.tool()
def advanced_search(params: SearchParams) -> dict:
"""Advanced search with structured validation"""
return {"results": [], "query": params.query, "limit": params.limit}| Method | Syntax | Use Case |
|---|---|---|
| Decorator | @mcp.tool() | Most common, function-level |
| Manual | mcp.add_tool(fn) | Dynamic registration at runtime |
| Named | @mcp.tool(name="custom") | Override the auto-generated name |
| Tagged | @mcp.tool(tags=["db"]) | Group tools by category |
| Python Type | JSON Schema | Notes |
|---|---|---|
| int | {"type": "integer"} | Integer numbers |
| float | {"type": "number"} | Floating point |
| str | {"type": "string"} | Strings |
| bool | {"type": "boolean"} | True / False |
| list[str] | {"type": "array", "items": {"type": "string"}} | String array |
| dict | {"type": "object"} | Free-form object |
| BaseModel | Full schema from model | Nested validation |
| str | None | {"anyOf": [{"type":"string"}, {"type":"null"}]} | Optional param |
None as default and initialize inside the function body. FastMCP uses Pydantic under the hood, so Field(default_factory=list) works correctly in Pydantic models.from fastmcp import FastMCP
mcp = FastMCP("Resource Server")
# ── Static resource ──
@mcp.resource("config://app")
def get_config() -> str:
"""Application configuration"""
return '{"name": "my-app", "version": "1.0"}'
# ── Dynamic resource (template with URI params) ──
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""Get user profile by ID"""
return f"Profile for user {user_id}"
# ── Resource template (listable) ──
@mcp.resource("files://{path}")
def read_file(path: str) -> str:
"""Read file contents from a given path"""
with open(path) as f:
return f.read()
# ── Async resource ──
@mcp.resource("api://status")
async def get_status() -> str:
"""Get live API status"""
return '{"status": "healthy", "uptime": "99.9%"}'| Type | Pattern | Behavior |
|---|---|---|
| Static | config://app | Fixed URI, returns same data each time |
| Dynamic | users://{user_id}/profile | URI template, params extracted at call time |
| Listable | files://{path} | Client can enumerate available resources |
| Async | api://status | Supports async/await for I/O-bound ops |
| Scheme | Convention | Example |
|---|---|---|
| config:// | Configuration data | config://database |
| users:// | User-related data | users://123/profile |
| files:// | File system access | files:///tmp/data.csv |
| api:// | External API data | api://weather/forecast |
| db:// | Database queries | db://orders/pending |
from fastmcp import FastMCP
mcp = FastMCP("Prompt Server")
# ── Simple prompt ──
@mcp.prompt()
def code_review(code: str) -> str:
"""Generate a code review prompt"""
return (
f"Review this code:\n\n```\n{code}\n```\n\n"
"Provide feedback on: correctness, performance, readability, security."
)
# ── Prompt with default arguments ──
@mcp.prompt()
def debug_help(error: str, language: str = "python") -> str:
"""Debug assistance prompt for any language"""
return f"Error in {language}: {error}\n\nHow to fix?"
# ── Complex multi-argument prompt ──
@mcp.prompt()
def explain_concept(
topic: str,
audience: str = "beginner",
depth: str = "detailed"
) -> str:
"""Generate an explanation prompt"""
return (
f"Explain '{topic}' to a {audience} audience.\n"
f"Level of detail: {depth}.\n"
"Use analogies and examples."
)| Feature | Details |
|---|---|
| Parameters | Prompt functions accept args that become template variables |
| Defaults | Optional parameters with Python default values |
| Descriptions | Docstrings become prompt descriptions for discovery |
| Multi-line | Return multi-line strings, Markdown supported |
| Dynamic | Prompts can pull data from DB, APIs, or files at call time |
| Use Prompts When | Use Tools When |
|---|---|
| You want to guide the LLM's reasoning | You need to perform an action |
| The input is natural language | The input is structured data |
| You're shaping a conversation flow | You're returning computed results |
| Output is instructions, not data | Output is data the LLM should analyze |
from fastmcp import FastMCP, Context
mcp = FastMCP("Context Server")
# ── Access request context ──
@mcp.tool()
async def authenticated_tool(ctx: Context) -> str:
"""Tool that uses request context for metadata"""
request_id = ctx.request_id
await ctx.info(f"Processing request {request_id}")
return f"Processed with context: {request_id}"
# ── Report progress ──
@mcp.tool()
async def long_running_task(ctx: Context) -> str:
"""Tool with progress reporting"""
for i in range(1, 101):
await ctx.report_progress(i, 100)
if i % 25 == 0:
await ctx.info(f"Progress: {i}%")
return "Task complete!"
# ── Read request metadata ──
@mcp.tool()
async def inspect_request(ctx: Context) -> dict:
"""Inspect the incoming MCP request"""
return {
"request_id": ctx.request_id,
"client_id": getattr(ctx, "client_id", "unknown"),
}| Method | Description | Async |
|---|---|---|
| ctx.request_id | Unique identifier for the current request | No |
| ctx.info(msg) | Send info-level log to client | Yes |
| ctx.report_progress(current, total) | Report task progress (0-100) | Yes |
| ctx.read_resource(uri) | Read a server resource | Yes |
| ctx.report_error(msg) | Report a non-fatal error | Yes |
| Pattern | How | Use Case |
|---|---|---|
| Context param | Add ctx: Context to tool signature | Request-scoped metadata |
| Pydantic model | Accept a BaseModel as a parameter | Complex input validation |
| Server state | Set attributes on mcp instance | Shared configuration |
| FastMCP.depends() | Dependency injection helper | DB sessions, API clients |
ctx: Context to your tool function signature — FastMCP detects it and injects the current request context. This is similar to FastAPI's Request dependency injection.from fastmcp import FastMCP
from my_servers.weather import weather_mcp
from my_servers.database import db_mcp
from my_servers.filesystem import fs_mcp
# ── Compose servers into one ──
app = FastMCP("Super Server")
app.mount("weather", weather_mcp)
app.mount("db", db_mcp)
app.mount("fs", fs_mcp)
# Tools are now namespaced:
# weather.get_forecast
# db.query
# fs.read_file
if __name__ == "__main__":
app.run()# ── Individual server (weather_server.py) ──
from fastmcp import FastMCP
weather_mcp = FastMCP("Weather")
@weather_mcp.tool()
def get_forecast(city: str) -> str:
"""Get weather forecast for a city"""
return f"Sunny, 72°F in {city}"
@weather_mcp.resource("weather://config")
def weather_config() -> str:
return '{"units": "fahrenheit", "source": "openweather"}'
# This server can run standalone OR be mounted| Benefit | Details |
|---|---|
| Modularity | Each server is independently developed and tested |
| Reusability | Same server used standalone or composed |
| Namespacing | Mounted servers get prefixed tool/resource names |
| Scalability | Split large servers into domain-specific modules |
| Team collaboration | Different teams own different servers |
| Pattern | Example | When to Use |
|---|---|---|
| Flat mount | app.mount("db", db_mcp) | Simple tool grouping |
| Nested mount | app.mount("org/tools", tools_mcp) | Multi-level namespacing |
| Conditional | if env == "prod": app.mount("admin", admin_mcp) | Environment-specific tools |
| Dynamic | for plugin in plugins: app.mount(plugin.name, plugin.mcp) | Plugin architecture |
from fastmcp import FastMCP
from fastmcp.auth.providers import BearerAuthProvider
mcp = FastMCP("Secure Server")
# ── Simple bearer token auth ──
mcp.auth = BearerAuthProvider(token="my-secret-token")
# ── Custom token verification ──
async def verify_token(token: str) -> bool:
"""Verify token against database or JWT"""
valid_tokens = {"admin-token", "user-token"}
return token in valid_tokens
mcp.auth = BearerAuthProvider(token_verify_func=verify_token)
# ── Auth with user context ──
async def get_user(token: str):
"""Extract user info from token"""
if token == "admin-token":
return {"role": "admin", "name": "Alice"}
return None| Provider | Setup | Use Case |
|---|---|---|
| BearerAuthProvider | Single token or verify function | Simple API key auth |
| Custom AuthProvider | Subclass AuthProvider | OAuth2, JWT, custom flows |
| No auth (default) | No mcp.auth set | Local development only |
| Practice | Why |
|---|---|
| Never hardcode tokens | Use env vars: os.environ["API_KEY"] |
| Use HTTPS/SSE for remote | Prevents token interception on the wire |
| Validate all inputs | Pydantic handles this automatically |
| Rate limit tools | Prevent abuse, especially for expensive operations |
| Sanitize file paths | Prevent directory traversal in resource handlers |
| Audit logging | Log who called what and when |
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from pydantic import BaseModel, Field, ValidationError
mcp = FastMCP("Robust Server")
# ── Raise ToolError for user-facing errors ──
@mcp.tool()
def risky_operation(data: str) -> str:
"""Process data with validation"""
if not data:
raise ToolError("No data provided", code="NO_DATA")
if len(data) > 1000:
raise ToolError("Data too long (max 1000 chars)", code="INVALID_INPUT")
return f"Processed: {data}"
# ── Pydantic validation (automatic) ──
class CreateItem(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
quantity: int = Field(1, ge=1, le=1000)
@mcp.tool()
def create_item(params: CreateItem) -> dict:
"""Create an item with automatic validation"""
# If validation fails, FastMCP returns a clear error to the LLM
return {"id": "abc123", **params.model_dump()}
# ── Custom error codes ──
@mcp.tool()
def divide(a: float, b: float) -> float:
"""Divide two numbers"""
if b == 0:
raise ToolError("Division by zero is not allowed", code="DIV_ZERO")
return a / b| Error Type | How | When to Use |
|---|---|---|
| ToolError | raise ToolError(msg, code) | User-facing, recoverable errors |
| Pydantic ValidationError | Automatic from Field constraints | Input validation failures |
| Python exceptions | Let them propagate | Unexpected bugs (logged by FastMCP) |
| Custom exceptions | Subclass ToolError | Domain-specific error handling |
| Validator | Meaning | Example |
|---|---|---|
| Field(...) | Required field | name: str = Field(...) |
| Field(min_length=1) | Minimum string length | password: str = Field(min_length=8) |
| Field(ge=0) | Greater than or equal | age: int = Field(ge=0) |
| Field(gt=0) | Strictly greater than | price: float = Field(gt=0) |
| Field(le=100) | Less than or equal | score: int = Field(le=100) |
| Field(pattern=r"\d+") | Regex pattern | zip_code: str = Field(pattern=r"\d{5}") |
| Field(default=...) | Default value | role: str = Field(default="user") |
ToolError returns a structured error to the LLM with a message and code, helping it understand what went wrong and potentially retry. Raw Python exceptions are logged but may not be as helpful to the AI client.| Client | Transport | Notes |
|---|---|---|
| Claude Desktop | stdio | Primary target, JSON config file |
| Claude for VS Code | stdio | Same config pattern |
| Cursor IDE | stdio | Add to .cursor/mcp.json |
| Windsurf | stdio | Built-in MCP support |
| Continue.dev | stdio | Open-source AI code assistant |
| Any MCP client | stdio or SSE | Spec-compliant clients |
{
"mcpServers": {
"my-fastmcp": {
"command": "fastmcp",
"args": ["run", "/path/to/server.py"]
},
"weather-server": {
"command": "fastmcp",
"args": ["run", "/path/to/weather.py"],
"env": {
"API_KEY": "your-api-key-here"
}
},
"remote-sse-server": {
"url": "http://localhost:8000/sse"
}
}
}# Install a FastMCP server globally (adds to Claude Desktop config)
fastmcp install server.py --name my-server
# List installed servers
fastmcp list
# Uninstall a server
fastmcp uninstall my-serverfastmcp install for zero-config setup. This CLI command automatically adds your server to Claude Desktop's config file and detects the correct Python environment. Pass --env KEY=value to inject environment variables.| Option | Command | Best For |
|---|---|---|
| Prefect Horizon | fastmcp deploy server.py | Managed, scalable, zero infra |
| Docker | Dockerfile + docker run | Self-hosted, reproducible |
| Railway / Fly.io | Git push deploy | Quick PaaS deployment |
| AWS / GCP | Container on ECS/Cloud Run | Enterprise, full control |
| Bare metal | systemd + fastmcp run | Simple VPS hosting |
| Feature | Details |
|---|---|
| Managed endpoints | Auto-provisioned HTTPS URL |
| Auto-scaling | Scales with traffic automatically |
| Auth built-in | Token management included |
| Monitoring | Logs, metrics, request tracking |
| Versioning | Deploy multiple versions, rollback |
| Zero config | No Dockerfile or infra management needed |
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 8000
CMD ["fastmcp", "run", "server.py", "--transport", "sse", "--host", "0.0.0.0", "--port", "8000"]| Command | Description |
|---|---|
| fastmcp dev server.py | Run server in development mode (auto-reload) |
| fastmcp run server.py | Run server in production mode |
| fastmcp deploy server.py | Deploy to Prefect Horizon |
| fastmcp install server.py | Install server in Claude Desktop config |
| fastmcp uninstall name | Remove installed server from config |
| fastmcp list | List all installed MCP servers |
| fastmcp inspect server.py | Inspect server tools, resources, prompts |
| Flag | Default | Description |
|---|---|---|
| --transport | stdio | Transport: stdio, sse, streamable-http |
| --host | 127.0.0.1 | Bind address (SSE/HTTP transports) |
| --port | 8000 | Bind port (SSE/HTTP transports) |
| --name | filename | Custom server name |
| --env KEY=VAL | — | Set environment variables |
| -v / --verbose | false | Enable verbose logging |
| --help | — | Show help and all available options |
# Full workflow example
fastmcp dev server.py # Develop with auto-reload
fastmcp inspect server.py # Check tools/resources
fastmcp install server.py -n my-api --env API_KEY=sk-xxx
fastmcp deploy server.py # Deploy to Prefect Horizon
# Run with SSE on custom port
fastmcp run server.py --transport sse --host 0.0.0.0 --port 3000
# Verbose mode for debugging
fastmcp dev server.py -v| Feature | Raw MCP SDK | FastMCP |
|---|---|---|
| Schema generation | Manual JSON Schema | Auto from Python type hints |
| Input validation | Manual validation code | Pydantic-based, automatic |
| Tool registration | Implement Server interface | @mcp.tool() decorator |
| Error handling | Manual JSON-RPC errors | ToolError with error codes |
| Auth | Custom implementation | Built-in BearerAuthProvider |
| Composition | Manual proxy/wrapper | app.mount() native support |
| CLI tooling | None | dev / run / deploy / install |
| Dev experience | Raw protocol handling | Flask/FastAPI-like ergonomics |
| Documentation | Anthropic MCP spec | gofastmcp.com + examples |
| Learning curve | Steeper (understand JSON-RPC) | Gentle (Pythonic API) |
| Flexibility | Full control over protocol | Conventions over configuration |
| Lines of code | ~50-100 for basic server | ~10-15 for basic server |
| Scenario | Recommendation | Why |
|---|---|---|
| Building MCP servers quickly | FastMCP | 10x less boilerplate |
| Need custom auth flows | FastMCP | Extensible auth providers |
| Multi-server composition | FastMCP | Native mount() support |
| Full protocol control | Raw MCP SDK | Direct access to JSON-RPC |
| Non-Python implementations | Raw MCP SDK | TypeScript SDK available |
| Research / learning MCP | Raw MCP SDK | Understand the protocol |
| Production microservices | FastMCP | Deploy to Prefect Horizon |
| Existing Python project | FastMCP | Drop-in with minimal changes |
| Practice | Details |
|---|---|
| Descriptive docstrings | LLM uses docstrings to decide when to call — be specific |
| Single responsibility | One tool = one action. Combine in prompts if needed |
| Type hints everywhere | Enables auto schema + validation with zero effort |
| Use Pydantic for complex inputs | Nested objects, constraints, enums |
| Return structured data | JSON/serializable dicts, not raw strings |
| Name tools clearly | get_user, search_documents — verb + noun |
| Practice | Details |
|---|---|
| Composition over monolith | Split into domain servers, mount together |
| Environment variables | Never hardcode secrets or URLs |
| Async by default | Use async def for I/O-bound tools (HTTP, DB, files) |
| Timeout long operations | Set timeouts for external API calls |
| Graceful shutdown | Clean up connections, temp files on exit |
| Version your server | Include version in server name / metadata |
| Technique | How |
|---|---|
| fastmcp inspect | List all tools, resources, and their schemas |
| fastmcp dev -v | Verbose logging to see every request/response |
| Claude Desktop logs | Check Claude Desktop developer console for MCP errors |
| Unit test tools | Import and call tool functions directly in pytest |
| Integration test | Use fastmcp.Client to test the full MCP protocol |
| Tip | Impact |
|---|---|
| Use async for I/O | High — non-blocking HTTP/DB/file calls |
| Cache expensive results | High — avoid repeated API calls |
| Limit resource sizes | Medium — truncate large file reads |
| Batch operations | Medium — one tool call vs many |
| Lazy imports | Low — faster server startup time |
from fastmcp import FastMCP
from pydantic import BaseModel, Field
import httpx
import os
mcp = FastMCP("Production Server", version="1.2.0")
class Query(BaseModel):
text: str = Field(..., min_length=1, max_length=500)
limit: int = Field(10, ge=1, le=100)
@mcp.tool()
async def search_documents(query: Query) -> list[dict]:
"""Search documents in the knowledge base.
Returns relevant documents matching the query text.
Use this when the user asks about specific topics in
your documentation or knowledge base.
"""
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(
"https://api.example.com/search",
params={"q": query.text, "limit": query.limit},
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}
)
resp.raise_for_status()
return resp.json()["results"]
if __name__ == "__main__":
mcp.run()search with description "Search" will be called too often. A tool named search_internal_docswith "Search the company's internal documentation for policies, procedures, and FAQs" will be called precisely when needed.