--- name: fastmcp description: | Build MCP servers in Python with FastMCP to expose tools, resources, and prompts to LLMs. Supports storage backends, middleware, OAuth Proxy, OpenAPI integration, and FastMCP Cloud deployment. Prevents 30+ errors. Use when: creating MCP servers, or troubleshooting module-level server, storage, lifespan, middleware, OAuth, background tasks, or FastAPI mount errors. user-invocable: true --- # FastMCP - Build MCP Servers in Python FastMCP is a Python framework for building Model Context Protocol (MCP) servers that expose tools, resources, and prompts to Large Language Models like Claude. This skill provides production-tested patterns, error prevention, and deployment strategies for building robust MCP servers. ## Quick Start ### Installation ```bash pip install fastmcp # or uv pip install fastmcp ``` ### Minimal Server ```python from fastmcp import FastMCP # MUST be at module level for FastMCP Cloud mcp = FastMCP("My Server") @mcp.tool() async def hello(name: str) -> str: """Say hello to someone.""" return f"Hello, {name}!" if __name__ == "__main__": mcp.run() ``` **Run it:** ```bash # Local development python server.py # With FastMCP CLI fastmcp dev server.py # HTTP mode python server.py --transport http --port 8000 ``` ## What's New in v2.14.x (December 2025) ### v2.14.2 (December 31, 2024) - MCP SDK pinned to <2.x for compatibility - Supabase provider gains `auth_route` parameter - Bug fixes: outputSchema `$ref` resolution, OAuth Proxy validation, OpenAPI 3.1 support ### v2.14.1: Sampling with Tools (SEP-1577) - **`ctx.sample()` now accepts tools** for agentic workflows - `AnthropicSamplingHandler` promoted from experimental - `ctx.sample_step()` for single LLM call returning `SampleStep` - Python 3.13 support added ### v2.14.0: Background Tasks (SEP-1686) - **Protocol-native background tasks** for long-running operations - Add `task=True` to async decorators; progress tracking without blocking - MCP 2025-11-25 specification support - SEP-1699: SSE polling and event resumability - SEP-1330: Multi-select enum elicitation schemas - SEP-1034: Default values for elicitation schemas **⚠️ Breaking Changes (v2.14.0):** - `BearerAuthProvider` module removed (use `JWTVerifier` or `OAuthProxy`) - `Context.get_http_request()` method removed - `fastmcp.Image` top-level import removed (use `from fastmcp.utilities import Image`) - `enable_docket`, `enable_tasks` settings removed (always enabled) - `run_streamable_http_async()`, `sse_app()`, `streamable_http_app()`, `run_sse_async()` methods removed - `dependencies` parameter removed from decorators - `output_schema=False` support eliminated - `FASTMCP_SERVER_` environment variable prefix deprecated **Known Compatibility:** - MCP SDK pinned to <2.x (v2.14.2+) ## What's New in v3.0.0 (Beta - January 2026) **⚠️ MAJOR BREAKING CHANGES** - FastMCP 3.0 is a complete architectural refactor. ### Provider Architecture All components now sourced via **Providers**: - `FileSystemProvider` - Discover decorated functions from directories with hot-reload - `SkillsProvider` - Expose agent skill files as MCP resources - `OpenAPIProvider` - Auto-generate from OpenAPI specs - `ProxyProvider` - Proxy to remote MCP servers ```python from fastmcp import FastMCP from fastmcp.providers import FileSystemProvider mcp = FastMCP("server") mcp.add_provider(FileSystemProvider(path="./tools", reload=True)) ``` ### Transforms (Component Middleware) Modify components without changing source code: - Namespace, rename, filter by version - `ResourcesAsTools` - Expose resources as tools - `PromptsAsTools` - Expose prompts as tools ```python from fastmcp.transforms import Namespace, VersionFilter mcp.add_transform(Namespace(prefix="api")) mcp.add_transform(VersionFilter(min_version="2.0")) ``` ### Component Versioning ```python @mcp.tool(version="2.0") async def fetch_data(query: str) -> dict: # Clients see highest version by default # Can request specific version return {"data": [...]} ``` ### Session-Scoped State ```python @mcp.tool() async def set_preference(key: str, value: str, ctx: Context) -> dict: await ctx.set_state(key, value) # Persists across session return {"saved": True} @mcp.tool() async def get_preference(key: str, ctx: Context) -> dict: value = await ctx.get_state(key, default=None) return {"value": value} ``` ### Other Features - `--reload` flag for auto-restart during development - Automatic threadpool dispatch for sync functions - Tool timeouts - OpenTelemetry tracing - Component authorization: `@tool(auth=require_scopes("admin"))` ### Migration Guide **Pin to v2 if not ready**: ``` # requirements.txt fastmcp<3 ``` **For most servers**, updating the import is all you need: ```python # v2.x and v3.0 compatible from fastmcp import FastMCP mcp = FastMCP("server") # ... rest of code works the same ``` **See**: [Official Migration Guide](https://github.com/jlowin/fastmcp/blob/main/docs/development/upgrade-guide.mdx) --- ## Core Concepts ### Tools Functions LLMs can call. Best practices: Clear names, comprehensive docstrings (LLMs read these!), strong type hints (Pydantic validates), structured returns, error handling. ```python @mcp.tool() async def async_tool(url: str) -> dict: # Use async for I/O async with httpx.AsyncClient() as client: return (await client.get(url)).json() ``` ### Resources Expose data to LLMs. URI schemes: `data://`, `file://`, `resource://`, `info://`, `api://`, or custom. ```python @mcp.resource("user://{user_id}/profile") # Template with parameters async def get_user(user_id: str) -> dict: # CRITICAL: param names must match return await fetch_user_from_db(user_id) ``` ### Prompts Pre-configured prompts with parameters. ```python @mcp.prompt("analyze") def analyze_prompt(topic: str) -> str: return f"Analyze {topic} considering: state, challenges, opportunities, recommendations." ``` ## Context Features Inject `Context` parameter (with type hint!) for advanced features: **Elicitation (User Input):** ```python from fastmcp import Context @mcp.tool() async def confirm_action(action: str, context: Context) -> dict: confirmed = await context.request_elicitation(prompt=f"Confirm {action}?", response_type=str) return {"status": "completed" if confirmed.lower() == "yes" else "cancelled"} ``` **Progress Tracking:** ```python @mcp.tool() async def batch_import(file_path: str, context: Context) -> dict: data = await read_file(file_path) for i, item in enumerate(data): await context.report_progress(i + 1, len(data), f"Importing {i + 1}/{len(data)}") await import_item(item) return {"imported": len(data)} ``` **Sampling (LLM calls from tools):** ```python @mcp.tool() async def enhance_text(text: str, context: Context) -> str: response = await context.request_sampling( messages=[{"role": "user", "content": f"Enhance: {text}"}], temperature=0.7 ) return response["content"] ``` ## Background Tasks (v2.14.0+) Long-running operations that report progress without blocking clients. Uses Docket task scheduler (always enabled in v2.14.0+). **Basic Usage:** ```python @mcp.tool(task=True) # Enable background task mode async def analyze_large_dataset(dataset_id: str, context: Context) -> dict: """Analyze large dataset with progress tracking.""" data = await fetch_dataset(dataset_id) for i, chunk in enumerate(data.chunks): # Report progress to client await context.report_progress( current=i + 1, total=len(data.chunks), message=f"Processing chunk {i + 1}/{len(data.chunks)}" ) await process_chunk(chunk) return {"status": "complete", "records_processed": len(data)} ``` **Task States:** `pending` → `running` → `completed` / `failed` / `cancelled` **When to Use:** - Operations taking >30 seconds (LLM timeout risk) - Batch processing with per-item status updates - Operations that may need user input mid-execution - Long-running API calls or data processing **Known Limitation (v2.14.x)**: - `statusMessage` from `ctx.report_progress()` is **not forwarded** to clients during background task polling ([GitHub Issue #2904](https://github.com/jlowin/fastmcp/issues/2904)) - Progress messages appear in server logs but not in client UI - **Workaround**: Use official MCP SDK (`mcp>=1.10.0`) instead of FastMCP for now - **Status**: Fix pending in [PR #2906](https://github.com/jlowin/fastmcp/pull/2906) **Important:** Tasks execute through Docket scheduler. Cannot execute tasks through proxies (will raise error). ## Sampling with Tools (v2.14.1+) Servers can pass tools to `ctx.sample()` for agentic workflows where the LLM can call tools during sampling. **Agentic Sampling:** ```python from fastmcp import Context from fastmcp.sampling import AnthropicSamplingHandler # Configure sampling handler mcp = FastMCP("Agent Server") mcp.add_sampling_handler(AnthropicSamplingHandler(api_key=os.getenv("ANTHROPIC_API_KEY"))) @mcp.tool() async def research_topic(topic: str, context: Context) -> dict: """Research a topic using agentic sampling with tools.""" # Define tools available during sampling research_tools = [ { "name": "search_web", "description": "Search the web for information", "inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}} }, { "name": "fetch_url", "description": "Fetch content from a URL", "inputSchema": {"type": "object", "properties": {"url": {"type": "string"}}} } ] # Sample with tools - LLM can call these tools during reasoning result = await context.sample( messages=[{"role": "user", "content": f"Research: {topic}"}], tools=research_tools, max_tokens=4096 ) return {"research": result.content, "tools_used": result.tool_calls} ``` **Single-Step Sampling:** ```python @mcp.tool() async def get_single_response(prompt: str, context: Context) -> dict: """Get a single LLM response without tool loop.""" # sample_step() returns SampleStep for inspection step = await context.sample_step( messages=[{"role": "user", "content": prompt}], temperature=0.7 ) return { "content": step.content, "model": step.model, "stop_reason": step.stop_reason } ``` **Sampling Handlers:** - `AnthropicSamplingHandler` - For Claude models (v2.14.1+) - `OpenAISamplingHandler` - For GPT models **Known Limitation**: `ctx.sample()` works when client connects to a single server but fails with "Sampling not supported" error when multiple servers are configured in client. Tools without sampling work fine. ([Community-sourced finding](https://github.com/jlowin/fastmcp/issues/699)) ## Storage Backends Built on `py-key-value-aio` for OAuth tokens, response caching, persistent state. **Available Backends:** - **Memory** (default): Ephemeral, fast, dev-only - **Disk**: Persistent, encrypted with `FernetEncryptionWrapper`, platform-aware (Mac/Windows default) - **Redis**: Distributed, production, multi-instance - **Others**: DynamoDB, MongoDB, Elasticsearch, Memcached, RocksDB, Valkey **Basic Usage:** ```python from key_value.stores import DiskStore, RedisStore from key_value.encryption import FernetEncryptionWrapper from cryptography.fernet import Fernet # Disk (persistent, single instance) mcp = FastMCP("Server", storage=DiskStore(path="/app/data/storage")) # Redis (distributed, production) mcp = FastMCP("Server", storage=RedisStore( host=os.getenv("REDIS_HOST"), password=os.getenv("REDIS_PASSWORD") )) # Encrypted storage (recommended) mcp = FastMCP("Server", storage=FernetEncryptionWrapper( key_value=DiskStore(path="/app/data"), fernet=Fernet(os.getenv("STORAGE_ENCRYPTION_KEY")) )) ``` **Platform Defaults:** Mac/Windows use Disk, Linux uses Memory. Override with `storage` parameter. ## Server Lifespans **⚠️ Breaking Change in v2.13.0**: Lifespan behavior changed from per-session to per-server-instance. Initialize/cleanup resources once per server (NOT per session) - critical for DB connections, API clients. ```python from contextlib import asynccontextmanager from dataclasses import dataclass @dataclass class AppContext: db: Database api_client: httpx.AsyncClient @asynccontextmanager async def app_lifespan(server: FastMCP): """Runs ONCE per server instance.""" db = await Database.connect(os.getenv("DATABASE_URL")) api_client = httpx.AsyncClient(base_url=os.getenv("API_BASE_URL"), timeout=30.0) try: yield AppContext(db=db, api_client=api_client) finally: await db.disconnect() await api_client.aclose() mcp = FastMCP("Server", lifespan=app_lifespan) # Access in tools @mcp.tool() async def query_db(sql: str, context: Context) -> list: app_ctx = context.fastmcp_context.lifespan_context return await app_ctx.db.query(sql) ``` **ASGI Integration (FastAPI/Starlette):** ```python mcp = FastMCP("Server", lifespan=mcp_lifespan) app = FastAPI(lifespan=mcp.lifespan) # ✅ MUST pass lifespan! ``` **State Management:** ```python context.fastmcp_context.set_state(key, value) # Store context.fastmcp_context.get_state(key, default=None) # Retrieve ``` ## Middleware System **8 Built-in Types:** TimingMiddleware, ResponseCachingMiddleware, LoggingMiddleware, RateLimitingMiddleware, ErrorHandlingMiddleware, ToolInjectionMiddleware, PromptToolMiddleware, ResourceToolMiddleware **Execution Order (order matters!):** ``` Request Flow: → ErrorHandlingMiddleware (catches errors) → TimingMiddleware (starts timer) → LoggingMiddleware (logs request) → RateLimitingMiddleware (checks rate limit) → ResponseCachingMiddleware (checks cache) → Tool/Resource Handler ``` **Basic Usage:** ```python from fastmcp.middleware import ErrorHandlingMiddleware, TimingMiddleware, LoggingMiddleware mcp.add_middleware(ErrorHandlingMiddleware()) # First: catch errors mcp.add_middleware(TimingMiddleware()) # Second: time requests mcp.add_middleware(LoggingMiddleware(level="INFO")) mcp.add_middleware(RateLimitingMiddleware(max_requests=100, window_seconds=60)) mcp.add_middleware(ResponseCachingMiddleware(ttl_seconds=300, storage=RedisStore())) ``` **Custom Middleware:** ```python from fastmcp.middleware import BaseMiddleware class AccessControlMiddleware(BaseMiddleware): async def on_call_tool(self, tool_name, arguments, context): user = context.fastmcp_context.get_state("user_id") if user not in self.allowed_users: raise PermissionError(f"User not authorized") return await self.next(tool_name, arguments, context) ``` **Hook Hierarchy:** `on_message` (all) → `on_request`/`on_notification` → `on_call_tool`/`on_read_resource`/`on_get_prompt` → `on_list_*` (list operations) ## Server Composition **Two Strategies:** 1. **`import_server()`** - Static snapshot: One-time copy at import, changes don't propagate, fast (no runtime delegation). Use for: Finalized component bundles. 2. **`mount()`** - Dynamic link: Live runtime link, changes immediately visible, runtime delegation (slower). Use for: Modular runtime composition. **Basic Usage:** ```python # Import (static) main_server.import_server(api_server) # One-time copy # Mount (dynamic) main_server.mount(api_server, prefix="api") # Tools: api.fetch_data main_server.mount(db_server, prefix="db") # Resources: resource://db/path ``` **Tag Filtering:** ```python @api_server.tool(tags=["public"]) def public_api(): pass main_server.import_server(api_server, include_tags=["public"]) # Only public main_server.mount(api_server, prefix="api", exclude_tags=["admin"]) # No admin ``` **Resource Prefix Formats:** - **Path** (default since v2.4.0): `resource://prefix/path` - **Protocol** (legacy): `prefix+resource://path` ```python main_server.mount(subserver, prefix="api", resource_prefix_format="path") ``` ## OAuth & Authentication **4 Authentication Patterns:** 1. **Token Validation** (`JWTVerifier`): Validate external tokens 2. **External Identity Providers** (`RemoteAuthProvider`): OAuth 2.0/OIDC with DCR 3. **OAuth Proxy** (`OAuthProxy`): Bridge to providers without DCR (GitHub, Google, Azure, AWS, Discord, Facebook) 4. **Full OAuth** (`OAuthProvider`): Complete authorization server **Pattern 1: Token Validation** ```python from fastmcp.auth import JWTVerifier auth = JWTVerifier(issuer="https://auth.example.com", audience="my-server", public_key=os.getenv("JWT_PUBLIC_KEY")) mcp = FastMCP("Server", auth=auth) ``` **Pattern 3: OAuth Proxy (Production)** ```python from fastmcp.auth import OAuthProxy from key_value.stores import RedisStore from key_value.encryption import FernetEncryptionWrapper from cryptography.fernet import Fernet auth = OAuthProxy( jwt_signing_key=os.environ["JWT_SIGNING_KEY"], client_storage=FernetEncryptionWrapper( key_value=RedisStore(host=os.getenv("REDIS_HOST"), password=os.getenv("REDIS_PASSWORD")), fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"]) ), upstream_authorization_endpoint="https://github.com/login/oauth/authorize", upstream_token_endpoint="https://github.com/login/oauth/access_token", upstream_client_id=os.getenv("GITHUB_CLIENT_ID"), upstream_client_secret=os.getenv("GITHUB_CLIENT_SECRET"), enable_consent_screen=True # CRITICAL: Prevents confused deputy attacks ) mcp = FastMCP("GitHub Auth", auth=auth) ``` **OAuth Proxy Features:** Token factory pattern (issues own JWTs), consent screens (prevents bypass), PKCE support, RFC 7662 token introspection **Supported Providers:** GitHub, Google, Azure, AWS Cognito, Discord, Facebook, WorkOS, AuthKit, Descope, Scalekit, OCI (v2.13.1) **Supabase Provider** (v2.14.2+): ```python from fastmcp.auth import SupabaseProvider auth = SupabaseProvider( auth_route="/custom-auth", # Custom auth route (new in v2.14.2) # ... other config ) ``` ## Icons, API Integration, Cloud Deployment **Icons:** Add to servers, tools, resources, prompts. Use `Icon(url, size)`, data URIs via `Icon.from_file()` or `Image.to_data_uri()` (v2.13.1). **API Integration (3 Patterns):** 1. **Manual**: `httpx.AsyncClient` with base_url/headers/timeout 2. **OpenAPI Auto-Gen**: `FastMCP.from_openapi(spec, client, route_maps)` - GET→Resources/Templates, POST/PUT/DELETE→Tools 3. **FastAPI Conversion**: `FastMCP.from_fastapi(app, httpx_client_kwargs)` **Cloud Deployment Critical Requirements:** 1. ❗ **Module-level server** named `mcp`, `server`, or `app` 2. **PyPI dependencies only** in requirements.txt 3. **Public GitHub repo** (or accessible) 4. **Environment variables** for config ```python # ✅ CORRECT: Module-level export mcp = FastMCP("server") # At module level! # ❌ WRONG: Function-wrapped def create_server(): return FastMCP("server") # Too late for cloud! ``` **Deployment:** https://fastmcp.cloud → Sign in → Create Project → Select repo → Deploy **Client Config (Claude Desktop):** ```json {"mcpServers": {"my-server": {"url": "https://project.fastmcp.app/mcp", "transport": "http"}}} ``` ## 30 Common Errors (With Solutions) ### Error 1: Missing Server Object **Error:** `RuntimeError: No server object found at module level` **Cause:** Server not exported at module level (FastMCP Cloud requirement) **Solution:** `mcp = FastMCP("server")` at module level, not inside functions ### Error 2: Async/Await Confusion **Error:** `RuntimeError: no running event loop`, `TypeError: object coroutine can't be used in 'await'` **Cause:** Mixing sync/async incorrectly **Solution:** Use `async def` for tools with `await`, sync `def` for non-async code ### Error 3: Context Not Injected **Error:** `TypeError: missing 1 required positional argument: 'context'` **Cause:** Missing `Context` type annotation **Solution:** `async def tool(context: Context)` - type hint required! ### Error 4: Resource URI Syntax **Error:** `ValueError: Invalid resource URI: missing scheme` **Cause:** Resource URI missing scheme prefix **Solution:** Use `@mcp.resource("data://config")` not `@mcp.resource("config")` ### Error 5: Resource Template Parameter Mismatch **Error:** `TypeError: get_user() missing 1 required positional argument` **Cause:** Function parameter names don't match URI template **Solution:** `@mcp.resource("user://{user_id}/profile")` → `def get_user(user_id: str)` - names must match exactly --- ### Error 6: Pydantic Validation Error **Error:** `ValidationError: value is not a valid integer` **Cause:** Type hints don't match provided data **Solution:** Use Pydantic models: `class Params(BaseModel): query: str = Field(min_length=1)` ### Error 7: Transport/Protocol Mismatch **Error:** `ConnectionError: Server using different transport` **Cause:** Client and server using incompatible transports **Solution:** Match transports - stdio: `mcp.run()` + `{"command": "python", "args": ["server.py"]}`, HTTP: `mcp.run(transport="http", port=8000)` + `{"url": "http://localhost:8000/mcp", "transport": "http"}` **HTTP Timeout Issue (Fixed in v2.14.3)**: - HTTP transport was defaulting to 5-second timeout instead of MCP's 30-second default ([GitHub Issue #2845](https://github.com/jlowin/fastmcp/issues/2845)) - Tools taking >5 seconds would fail silently in v2.14.2 and earlier - **Solution**: Upgrade to fastmcp>=2.14.3 (timeout now respects MCP's 30s default) ### Error 8: Import Errors (Editable Package) **Error:** `ModuleNotFoundError: No module named 'my_package'` **Cause:** Package not properly installed **Solution:** `pip install -e .` or use absolute imports or `export PYTHONPATH="/path/to/project"` ### Error 9: Deprecation Warnings **Error:** `DeprecationWarning: 'mcp.settings' is deprecated` **Cause:** Using old FastMCP v1 API **Solution:** Use `os.getenv("API_KEY")` instead of `mcp.settings.get("API_KEY")` ### Error 10: Port Already in Use **Error:** `OSError: [Errno 48] Address already in use` **Cause:** Port 8000 already occupied **Solution:** Use different port `--port 8001` or kill process `lsof -ti:8000 | xargs kill -9` ### Error 11: Schema Generation Failures **Error:** `TypeError: Object of type 'ndarray' is not JSON serializable` **Cause:** Unsupported type hints (NumPy arrays, custom classes) **Solution:** Return JSON-compatible types: `list[float]` or convert: `{"values": np_array.tolist()}` **Custom Classes Not Supported (Community-sourced)**: FastMCP supports all Pydantic-compatible types, but custom classes must be converted to dictionaries or Pydantic models for tool returns: ```python # ❌ NOT SUPPORTED class MyCustomClass: def __init__(self, value: str): self.value = value @mcp.tool() async def get_custom() -> MyCustomClass: return MyCustomClass("test") # Serialization error # ✅ SUPPORTED - Use dict or Pydantic @mcp.tool() async def get_custom() -> dict[str, str]: obj = MyCustomClass("test") return {"value": obj.value} # OR use Pydantic BaseModel from pydantic import BaseModel class MyModel(BaseModel): value: str @mcp.tool() async def get_model() -> MyModel: return MyModel(value="test") # Works! ``` **OutputSchema $ref Resolution (Fixed in v2.14.2)**: - Root-level `$ref` in `outputSchema` wasn't being dereferenced ([GitHub Issue #2720](https://github.com/jlowin/fastmcp/issues/2720)) - Caused MCP spec non-compliance and client compatibility issues - **Solution**: Upgrade to fastmcp>=2.14.2 (auto-dereferences $ref) ### Error 12: JSON Serialization **Error:** `TypeError: Object of type 'datetime' is not JSON serializable` **Cause:** Returning non-JSON-serializable objects **Solution:** Convert: `datetime.now().isoformat()`, bytes: `.decode('utf-8')` ### Error 13: Circular Import Errors **Error:** `ImportError: cannot import name 'X' from partially initialized module` **Cause:** Circular dependency (common in cloud deployment) **Solution:** Use direct imports in `__init__.py`: `from .api_client import APIClient` or lazy imports in functions ### Error 14: Python Version Compatibility **Error:** `DeprecationWarning: datetime.utcnow() is deprecated` **Cause:** Using deprecated Python 3.12+ methods **Solution:** Use `datetime.now(timezone.utc)` instead of `datetime.utcnow()` ### Error 15: Import-Time Execution **Error:** `RuntimeError: Event loop is closed` **Cause:** Creating async resources at module import time **Solution:** Use lazy initialization - create connection class with async `connect()` method, call when needed in tools --- ### Error 16: Storage Backend Not Configured **Error:** `RuntimeError: OAuth tokens lost on restart`, `ValueError: Cache not persisting` **Cause:** Using default memory storage in production without persistence **Solution:** Use encrypted DiskStore (single instance) or RedisStore (multi-instance) with `FernetEncryptionWrapper` ### Error 17: Lifespan Not Passed to ASGI App **Error:** `RuntimeError: Database connection never initialized`, `Warning: MCP lifespan hooks not running` **Cause:** FastMCP with FastAPI/Starlette without passing lifespan (v2.13.0 requirement) **Solution:** `app = FastAPI(lifespan=mcp.lifespan)` - MUST pass lifespan! ### Error 18: Middleware Execution Order Error **Error:** `RuntimeError: Rate limit not checked before caching` **Cause:** Incorrect middleware ordering (order matters!) **Solution:** ErrorHandling → Timing → Logging → RateLimiting → ResponseCaching (this order) ### Error 19: Circular Middleware Dependencies **Error:** `RecursionError: maximum recursion depth exceeded` **Cause:** Middleware not calling `self.next()` or calling incorrectly **Solution:** Always call `result = await self.next(tool_name, arguments, context)` in middleware hooks ### Error 20: Import vs Mount Confusion **Error:** `RuntimeError: Subserver changes not reflected`, `ValueError: Unexpected tool namespacing` **Cause:** Using `import_server()` when `mount()` was needed (or vice versa) **Solution:** `import_server()` for static bundles (one-time copy), `mount()` for dynamic composition (live link) ### Error 21: Resource Prefix Format Mismatch **Error:** `ValueError: Resource not found: resource://api/users` **Cause:** Using wrong resource prefix format **Solution:** Path format (default v2.4.0+): `resource://prefix/path`, Protocol (legacy): `prefix+resource://path` - set with `resource_prefix_format="path"` ### Error 22: OAuth Proxy Without Consent Screen **Error:** `SecurityWarning: Authorization bypass possible` **Cause:** OAuth Proxy without consent screen (security vulnerability) **Solution:** Always set `enable_consent_screen=True` - prevents confused deputy attacks (CRITICAL) ### Error 23: Missing JWT Signing Key in Production **Error:** `ValueError: JWT signing key required for OAuth Proxy` **Cause:** OAuth Proxy missing `jwt_signing_key` **Solution:** Generate: `secrets.token_urlsafe(32)`, store in `FASTMCP_JWT_SIGNING_KEY` env var, pass to `OAuthProxy(jwt_signing_key=...)` ### Error 24: Icon Data URI Format Error **Error:** `ValueError: Invalid data URI format` **Cause:** Incorrectly formatted data URI for icons **Solution:** Use `Icon.from_file("/path/icon.png", size="medium")` or `Image.to_data_uri()` (v2.13.1) - don't manually format ### Error 25: Lifespan Behavior Change (v2.13.0) **Error:** `Warning: Lifespan runs per-server, not per-session` **Cause:** Expecting v2.12 behavior (per-session) in v2.13.0+ (per-server) **Solution:** v2.13.0+ lifespans run ONCE per server, not per session - use middleware for per-session logic ### Error 26: BearerAuthProvider Removed (v2.14.0) **Error:** `ImportError: cannot import name 'BearerAuthProvider' from 'fastmcp.auth'` **Cause:** `BearerAuthProvider` module removed in v2.14.0 **Solution:** Use `JWTVerifier` for token validation or `OAuthProxy` for full OAuth flows: ```python # Before (v2.13.x) from fastmcp.auth import BearerAuthProvider # After (v2.14.0+) from fastmcp.auth import JWTVerifier auth = JWTVerifier(issuer="...", audience="...", public_key="...") ``` ### Error 27: Context.get_http_request() Removed (v2.14.0) **Error:** `AttributeError: 'Context' object has no attribute 'get_http_request'` **Cause:** `Context.get_http_request()` method removed in v2.14.0 **Solution:** Access request info through middleware or use `InitializeResult` exposed to middleware ### Error 28: Image Import Path Changed (v2.14.0) **Error:** `ImportError: cannot import name 'Image' from 'fastmcp'` **Cause:** `fastmcp.Image` top-level import removed in v2.14.0 **Solution:** Use new import path: ```python # Before (v2.13.x) from fastmcp import Image # After (v2.14.0+) from fastmcp.utilities import Image ``` ### Error 29: FastAPI Mount Path Doubling **Error:** Client can't connect to `/mcp` endpoint, gets 404 **Source:** [GitHub Issue #2961](https://github.com/jlowin/fastmcp/issues/2961) **Cause:** Mounting FastMCP at `/mcp` creates endpoint at `/mcp/mcp` due to path prefix duplication **Solution:** Mount at root `/` or adjust client config ```python # ❌ WRONG - Creates /mcp/mcp endpoint from fastapi import FastAPI from fastmcp import FastMCP mcp = FastMCP("server") app = FastAPI(lifespan=mcp.lifespan) app.mount("/mcp", mcp) # Endpoint becomes /mcp/mcp # ✅ CORRECT - Mount at root app.mount("/", mcp) # Endpoint is /mcp # ✅ OR adjust client config # In claude_desktop_config.json: {"url": "http://localhost:8000/mcp/mcp", "transport": "http"} ``` **Critical**: Must also pass `lifespan=mcp.lifespan` to FastAPI (see Error #17). ### Error 30: Background Tasks Fail with "No Active Context" (ASGI Mount) **Error:** `RuntimeError: No active context found` **Source:** [GitHub Issue #2877](https://github.com/jlowin/fastmcp/issues/2877) **Cause:** ContextVar propagation issue when FastMCP mounted in FastAPI/Starlette with background tasks (`task=True`) **Solution:** Upgrade to fastmcp>=2.14.3 ```python # In v2.14.2 and earlier - FAILS from fastapi import FastAPI from fastmcp import FastMCP, Context mcp = FastMCP("server") app = FastAPI(lifespan=mcp.lifespan) @mcp.tool(task=True) async def sample(name: str, ctx: Context) -> dict: # RuntimeError: No active context found await ctx.report_progress(1, 1, "Processing") return {"status": "OK"} app.mount("/", mcp) # ✅ FIXED in v2.14.3 # pip install fastmcp>=2.14.3 ``` **Note**: Related to Error #17 (Lifespan Not Passed to ASGI App). --- ## Production Patterns, Testing, CLI **4 Production Patterns:** 1. **Utils Module**: Single `utils.py` with Config class, format_success/error helpers 2. **Connection Pooling**: Singleton `httpx.AsyncClient` with `get_client()` class method 3. **Retry with Backoff**: `retry_with_backoff(func, max_retries=3, initial_delay=1.0, exponential_base=2.0)` 4. **Time-Based Caching**: `TimeBasedCache(ttl=300)` with `.get()` and `.set()` methods **Testing:** - Unit: `pytest` + `create_test_client(test_server)` + `await client.call_tool()` - Integration: `Client("server.py")` + `list_tools()` + `call_tool()` + `list_resources()` **CLI Commands:** ```bash fastmcp dev server.py # Run with inspector fastmcp install server.py # Install to Claude Desktop FASTMCP_LOG_LEVEL=DEBUG fastmcp dev # Debug logging ``` **Best Practices:** Factory pattern with module-level export, environment config with validation, comprehensive docstrings (LLMs read these!), health check resources **Project Structure:** - Simple: `server.py`, `requirements.txt`, `.env`, `README.md` - Production: `src/` (server.py, utils.py, tools/, resources/, prompts/), `tests/`, `pyproject.toml` --- ## References & Summary **Official:** https://github.com/jlowin/fastmcp, https://fastmcp.cloud, https://modelcontextprotocol.io, Context7: `/jlowin/fastmcp` **Related Skills:** openai-api, claude-api, cloudflare-worker-base, typescript-mcp **Package Versions:** fastmcp>=2.14.2 (PyPI), Python>=3.10 (3.13 supported in v2.14.1+), httpx, pydantic, py-key-value-aio, cryptography **Last Updated**: 2026-01-21 **17 Key Takeaways:** 1. Module-level server export (FastMCP Cloud) 2. Persistent storage (Disk/Redis) for OAuth/caching 3. Server lifespans for resource management 4. Middleware order: errors → timing → logging → rate limiting → caching 5. Composition: `import_server()` (static) vs `mount()` (dynamic) 6. OAuth security: consent screens + encrypted storage + JWT signing 7. Async/await properly (don't block event loop) 8. Structured error handling 9. Avoid circular imports 10. Test locally (`fastmcp dev`) 11. Environment variables (never hardcode secrets) 12. Comprehensive docstrings (LLMs read!) 13. Production patterns (utils, pooling, retry, caching) 14. OpenAPI auto-generation 15. Health checks + monitoring 16. **Background tasks** for long-running operations (`task=True`) 17. **Sampling with tools** for agentic workflows (`ctx.sample(tools=[...])`) **Production Readiness:** Encrypted storage, 4 auth patterns, 8 middleware types, modular composition, OAuth security (consent screens, PKCE, RFC 7662), response caching, connection pooling, timing middleware, background tasks, agentic sampling, FastAPI/Starlette mounting, v3.0 provider architecture **Prevents 30+ errors. 90-95% token savings.**