Installation & Quickstart
Install the package using your preferred Python package manager, or run it instantaneously using uvx without prior installation.
from fastapi import FastAPI, Depends, Header, HTTPException
from pydantic import BaseModel
from fast_mcp import FastMCP
app = FastAPI(title="Store API", version="1.0.0")
mcp = FastMCP(app=app, name="store-mcp")
# 1. Existing FastAPI route automatically reflected as an MCP tool
class Product(BaseModel):
id: int
name: str
price: float
@app.get("/products/{product_id}", tags=["mcp"])
async def get_product(product_id: int) -> Product:
"""Fetch product details by ID."""
if product_id == 404:
raise HTTPException(status_code=404, detail="Product not found")
return Product(id=product_id, name="Smart Widget", price=29.99)
# 2. Custom composite AI tool with native FastAPI dependency injection
def verify_token(authorization: str = Header(...)) -> str:
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid token")
return authorization.split(" ")[1]
@mcp.tool(name="order_status", description="Check customer order status")
def check_order(order_id: str, user: str = Depends(verify_token)) -> dict:
return {"order_id": order_id, "customer": user, "status": "Shipped"}
# 3. Mount MCP endpoints (/mcp/sse, /mcp/messages, /mcp/docs)
mcp.mount()
Run with standard ASGI servers (uvicorn main:app --reload) or launch over stdio for desktop AI clients with fast-mcp stdio main:app.
The Seven Core Architectural Capabilities
Click through each capability card below to explore working code examples, protocol behavior, and technical rationale.
1. Automatic Route Reflection (tags=["mcp"])
Expose existing endpoints without rewriting handlers into standalone functions. Tagging a FastAPI route with tags=["mcp"] automatically inspects Pydantic schemas, path/query variables, and docstrings (Google, Sphinx, NumPy format) into full MCP JSON tool definitions.
class InventoryQuery(BaseModel):
category: str
max_price: float = 100.0
@app.post("/inventory/search", tags=["mcp"])
async def search_inventory(query: InventoryQuery, limit: int = 10) -> list[dict]:
"""Search catalog items by category and maximum price.
Args:
query: Filter specification with category and price limit.
limit: Maximum number of records to return.
"""
return [{"item": "Keyboard", "price": 49.99}]
2. Custom AI Tools (@mcp.tool)
Define AI-specialized composite tools alongside standard REST endpoints. Supports bare and parameterized decorators, custom names, docstring extraction, and metadata tagging.
# Bare decorator
@mcp.tool
def calculate_quote(quantity: int, discount: float = 0.0) -> float:
"""Calculate quotation with volume discount."""
return quantity * 100.0 * (1.0 - discount)
# Parameterized decorator with custom name & tags
@mcp.tool(name="stock_check", description="Lookup stock quantity", tags=["inventory"])
async def check_inventory(sku: str) -> dict:
return {"sku": sku, "in_stock": True, "count": 42}
3. ASGI Scope Bridging & Native Auth
Client authorization headers (Authorization: Bearer <token>, cookies, API keys) passed during SSE connection or message post are preserved and bridged into an in-memory ASGI Request. FastAPI's Depends() and Security() resolve seamlessly.
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fast_mcp import FastMCP, get_current_request
security = HTTPBearer()
def get_current_user(creds: HTTPAuthorizationCredentials = Depends(security)) -> str:
token = creds.credentials
if token != "secret-ai-token":
raise HTTPException(status_code=401, detail="Invalid token")
return "authorized_agent"
@mcp.tool
async def sensitive_action(user: str = Depends(get_current_user)) -> dict:
# Access low-level request if needed
request = get_current_request()
return {"status": "ok", "caller": user, "ip": request.client.host}
4. Dynamic Progressive Tool Discovery
Avoid overwhelming LLM context windows when hosting hundreds of API routes. When enabled, tools/list only advertises baseline tools and the search_tools(query: str) meta-tool. FastMCP ranks candidates using the zero-dependency KeywordTagRouter.
mcp = FastMCP(
app=app,
dynamic_discovery=True, # Or set dynamic_discovery_threshold=20
baseline_tools=["search_tools", "get_health_status"],
baseline_tag="core",
)
# When LLM asks to create an invoice, it first calls:
# search_tools(query="invoice")
# -> Returns matching tools: [create_invoice, get_invoice, cancel_invoice]
5. Resilient Error Interception & Custom Serializers
Traps route HTTPException (400, 404, 422) and Pydantic validation errors into clean CallToolResult(is_error=True) payloads. The LLM receives actionable error text to self-correct instead of crashing the transport. Serializers format responses into token-saving minified JSON or customized markdown.
class Report(BaseModel):
title: str
metrics: dict[str, int]
@mcp.serializer(Report)
def format_report(report: Report) -> str:
"""Format report into concise markdown table for LLM consumption."""
md = f"### {report.title}\n"
for k, v in report.metrics.items():
md += f"- **{k}**: {v}\n"
return md
6. Embedded /mcp/docs & In-Chat MCP Apps (SEP-1865)
Features an embedded browser inspector at /mcp/docs for human developers, plus native support for in-chat interactive iframes in desktop AI clients (Claude Desktop, Cursor, VS Code) via _meta.ui.resourceUri, the built-in inspect() tool, and @mcp.app().
@mcp.app(
name="order_dashboard",
resource_uri="ui://store/dashboard",
html="""
<div style="font-family: sans-serif; padding: 1.5rem; background: #f8fafc; border: 1px solid #e2e8f0;">
<h2>Live Store Dashboard</h2>
<p>Active Users: <strong>1,420</strong></p>
</div>
"""
)
def live_dashboard() -> str:
"""Launch interactive store dashboard widget."""
return '<iframe src="ui://store/dashboard" width="100%" height="400"></iframe>'
7. Local stdio CLI Runner & Desktop AI Bridge
Connect desktop AI clients (Claude Desktop, Cursor) directly to your FastAPI backend over standard input/output pipes with zero network setup, port conflicts, or external proxies.
# Shell Command:
fast-mcp stdio main:app
# Or Programmatically in Python:
import asyncio
from fast_mcp import FastMCP, run_stdio
mcp = FastMCP(name="my-stdio-server")
@mcp.tool()
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
asyncio.run(run_stdio(mcp))
MCP Client Configuration (Claude Desktop & Cursor)
Configure desktop AI assistants to connect directly to your FastMCP instance. Use uvx for zero-install instant execution, or connect via local CLI or SSE transport.
Add to claude_desktop_config.json. Uses uvx to automatically pull and execute mcp-fastapi in an isolated environment without manual installation:
{
"mcpServers": {
"my-fastapi-backend": {
"command": "uvx",
"args": [
"--from",
"mcp-fastapi",
"fast-mcp",
"stdio",
"main:app"
]
}
}
}
For environments where mcp-fastapi is already installed in your virtual environment:
{
"mcpServers": {
"my-fastapi-backend": {
"command": "fast-mcp",
"args": [
"stdio",
"main:app"
]
}
}
}
In Cursor Settings > Features > MCP Servers, configure an stdio server:
{
"mcpServers": {
"fastapi-cursor-tools": {
"command": "uvx",
"args": ["--from", "mcp-fastapi", "fast-mcp", "stdio", "main:app"]
}
}
}
If your FastAPI app is already running via Uvicorn/Hypercorn, connect over Server-Sent Events (SSE):
{
"mcpServers": {
"fastapi-remote-sse": {
"url": "http://127.0.0.1:8000/mcp/sse",
"transport": "sse"
}
}
}
Supported Capabilities & Registry Compliance
mcp-fastapi implements the complete Model Context Protocol specification and complies with major MCP directory crawlers (Smithery, Glama, Pulse MCP, MCP.so, awesome-mcp-servers).
| Capability | Support Level | Implementation Details |
|---|---|---|
| Tools | Full Support (Native) | Reflected FastAPI endpoints (tags=["mcp"]) and custom @mcp.tool decorators. Automatic Pydantic schema generation, docstring parsing, and dynamic discovery (search_tools). |
| Resources | Full Support (SEP-1865) | Custom ui:// scheme support, SEP-1865 interactive HTML iframe bundles, and developer web inspector at /mcp/docs. |
| Prompts | Extensible | Supports prompt registration and templating conforming to MCP prompt specs. |
| Transports | Dual (stdio + sse) | Standard I/O pipes for desktop AI clients (stdio) and HTTP/SSE ASGI mount (/mcp/sse & /mcp/messages). |
| Auth Bridging | Native ASGI | Bridges SSE/POST headers into in-memory ASGI Request, transparently solving FastAPI Depends() and Security(). |
Architecture & The Dual-Citizen ASGI Seam
How mcp-fastapi eliminates subprocess overhead, proxy latency, and authorization disconnects.
Traditional Subprocess Approach
Standard MCP implementations spawn external child processes or require external reverse proxies.
- Requires separate worker processes and port allocation.
- HTTP headers (Bearer tokens, cookies) do not reach internal dependencies.
- Code duplication between REST API endpoints and MCP tool handlers.
- Uncaught exceptions crash the transport pipe.
mcp-fastapi In-Process ASGI Mount
Mounts in-memory directly onto your existing FastAPI application.
- Zero subprocess overhead; runs inside standard ASGI server.
- ASGI Scope Bridging captures handshake headers into in-memory Request context.
- Existing Depends() and Security() providers resolve without changes.
- Traps HTTPException into actionable CallToolResult(is_error=True).
Comprehensive API Reference
Complete reference for all exported classes, methods, and functions in the fast_mcp package.
| Symbol & Signature | Type | Description & Usage |
|---|---|---|
FastMCP(...) |
Class | Main framework coordinator. Accepts app, name, version, mount_path, route_tag, dynamic_discovery, router, baseline_tools, debug, and enable_ui. |
mcp.mount(app=None) |
Method | Mounts MCP server endpoints (/mcp/sse, /mcp/messages, /mcp/docs) onto the bound FastAPI application. |
@mcp.tool(...) |
Decorator | Registers a custom composite AI tool with automatic schema and docstring extraction. Accepts name, description, tags, meta, and ui. |
@mcp.serializer(...) |
Decorator | Registers custom response serialization functions for specific return types or Pydantic models. Resolves inheritance via MRO. |
@mcp.app(...) |
Decorator | Registers an interactive in-chat MCP App widget (SEP-1865) with custom HTML and ui:// resource URI. |
run_stdio(mcp_or_app) |
Coroutine | Executes the MCP server over standard I/O (stdin/stdout) for desktop AI assistants like Claude Desktop and Cursor. |
get_current_request() |
Function | Returns the active synthesized Starlette/FastAPI Request object during tool execution. |
get_current_scope() |
Function | Returns the active ASGI scope dictionary during tool execution. |
RouteReflector(tag="mcp") |
Class | Scans FastAPI routes, extracting docstrings, Pydantic body schemas, query parameters, and path variables into MCP tools. |
KeywordTagRouter() |
Class | Zero-dependency keyword, tag, and tokenized ranker used for progressive dynamic tool discovery. |
format_validation_error() |
Function | Formats Pydantic or FastAPI validation exceptions into concise, human-readable strings for LLM self-correction. |
Testing Across the ASGI Protocol Seam
Verify your MCP server end-to-end using standard pytest and httpx.AsyncClient with ASGITransport. Zero network sockets required.
import pytest
from httpx import AsyncClient, ASGITransport
from fastapi import FastAPI
from fast_mcp import FastMCP
@pytest.mark.asyncio
async def test_mcp_endpoints_and_tools():
app = FastAPI()
mcp = FastMCP(app=app, name="test-store")
@app.get("/items/{item_id}", tags=["mcp"])
def get_item(item_id: int):
return {"id": item_id, "name": "Widget"}
mcp.mount()
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
# 1. Verify docs inspector
res = await client.get("/mcp/docs")
assert res.status_code == 200
# 2. Verify tool reflection
tools_res = await client.get("/mcp/docs/tools")
assert tools_res.status_code == 200
tools = tools_res.json()["tools"]
assert any(t["name"] == "get_item" for t in tools)
# 3. Test execution endpoint
call_res = await client.post("/mcp/docs/call", json={"name": "get_item", "arguments": {"item_id": 42}})
assert call_res.status_code == 200
assert call_res.json()["result"]["id"] == 42