Claude-skill-registry fastmcp
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fastmcp" ~/.claude/skills/majiayu000-claude-skill-registry-fastmcp-17b229 && rm -rf "$T"
skills/data/fastmcp/SKILL.mdFastMCP - 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
pip install fastmcp # or uv pip install fastmcp
Minimal Server
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:
# 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
parameterauth_route - Bug fixes: outputSchema
resolution, OAuth Proxy validation, OpenAPI 3.1 support$ref
v2.14.1: Sampling with Tools (SEP-1577)
now accepts tools for agentic workflowsctx.sample()
promoted from experimentalAnthropicSamplingHandler
for single LLM call returningctx.sample_step()SampleStep- Python 3.13 support added
v2.14.0: Background Tasks (SEP-1686)
- Protocol-native background tasks for long-running operations
- Add
to async decorators; progress tracking without blockingtask=True - 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):
module removed (useBearerAuthProvider
orJWTVerifier
)OAuthProxy
method removedContext.get_http_request()
top-level import removed (usefastmcp.Image
)from fastmcp.utilities import Image
,enable_docket
settings removed (always enabled)enable_tasks
,run_streamable_http_async()
,sse_app()
,streamable_http_app()
methods removedrun_sse_async()
parameter removed from decoratorsdependencies
support eliminatedoutput_schema=False
environment variable prefix deprecatedFASTMCP_SERVER_
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:
- Discover decorated functions from directories with hot-reloadFileSystemProvider
- Expose agent skill files as MCP resourcesSkillsProvider
- Auto-generate from OpenAPI specsOpenAPIProvider
- Proxy to remote MCP serversProxyProvider
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
- Expose resources as toolsResourcesAsTools
- Expose prompts as toolsPromptsAsTools
from fastmcp.transforms import Namespace, VersionFilter mcp.add_transform(Namespace(prefix="api")) mcp.add_transform(VersionFilter(min_version="2.0"))
Component Versioning
@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
@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
flag for auto-restart during development--reload- 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:
# v2.x and v3.0 compatible from fastmcp import FastMCP mcp = FastMCP("server") # ... rest of code works the same
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.
@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.
@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.
@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):
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:
@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):
@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:
@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):
fromstatusMessage
is not forwarded to clients during background task polling (GitHub Issue #2904)ctx.report_progress()- Progress messages appear in server logs but not in client UI
- Workaround: Use official MCP SDK (
) instead of FastMCP for nowmcp>=1.10.0 - Status: Fix pending in PR #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:
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:
@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:
- For Claude models (v2.14.1+)AnthropicSamplingHandler
- For GPT modelsOpenAISamplingHandler
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)
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
, platform-aware (Mac/Windows default)FernetEncryptionWrapper - Redis: Distributed, production, multi-instance
- Others: DynamoDB, MongoDB, Elasticsearch, Memcached, RocksDB, Valkey
Basic Usage:
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.
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):
mcp = FastMCP("Server", lifespan=mcp_lifespan) app = FastAPI(lifespan=mcp.lifespan) # ✅ MUST pass lifespan!
State Management:
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:
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:
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:
-
- Static snapshot: One-time copy at import, changes don't propagate, fast (no runtime delegation). Use for: Finalized component bundles.import_server() -
- Dynamic link: Live runtime link, changes immediately visible, runtime delegation (slower). Use for: Modular runtime composition.mount()
Basic Usage:
# 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:
@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
main_server.mount(subserver, prefix="api", resource_prefix_format="path")
OAuth & Authentication
4 Authentication Patterns:
- Token Validation (
): Validate external tokensJWTVerifier - External Identity Providers (
): OAuth 2.0/OIDC with DCRRemoteAuthProvider - OAuth Proxy (
): Bridge to providers without DCR (GitHub, Google, Azure, AWS, Discord, Facebook)OAuthProxy - Full OAuth (
): Complete authorization serverOAuthProvider
Pattern 1: Token Validation
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)
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+):
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):
- Manual:
with base_url/headers/timeouthttpx.AsyncClient - OpenAPI Auto-Gen:
- GET→Resources/Templates, POST/PUT/DELETE→ToolsFastMCP.from_openapi(spec, client, route_maps) - FastAPI Conversion:
FastMCP.from_fastapi(app, httpx_client_kwargs)
Cloud Deployment Critical Requirements:
- ❗ Module-level server named
,mcp
, orserverapp - PyPI dependencies only in requirements.txt
- Public GitHub repo (or accessible)
- Environment variables for config
# ✅ 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):
{"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)
- 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:
# ❌ 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
in$ref
wasn't being dereferenced (GitHub Issue #2720)outputSchema - 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:
# 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:
# 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
Cause: Mounting FastMCP at /mcp creates endpoint at /mcp/mcp due to path prefix duplication
Solution: Mount at root / or adjust client config
# ❌ 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
Cause: ContextVar propagation issue when FastMCP mounted in FastAPI/Starlette with background tasks (task=True)
Solution: Upgrade to fastmcp>=2.14.3
# 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:
- Utils Module: Single
with Config class, format_success/error helpersutils.py - Connection Pooling: Singleton
withhttpx.AsyncClient
class methodget_client() - Retry with Backoff:
retry_with_backoff(func, max_retries=3, initial_delay=1.0, exponential_base=2.0) - Time-Based Caching:
withTimeBasedCache(ttl=300)
and.get()
methods.set()
Testing:
- Unit:
+pytest
+create_test_client(test_server)await client.call_tool() - Integration:
+Client("server.py")
+list_tools()
+call_tool()list_resources()
CLI Commands:
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
,.envREADME.md - Production:
(server.py, utils.py, tools/, resources/, prompts/),src/
,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:
- Module-level server export (FastMCP Cloud)
- Persistent storage (Disk/Redis) for OAuth/caching
- Server lifespans for resource management
- Middleware order: errors → timing → logging → rate limiting → caching
- Composition:
(static) vsimport_server()
(dynamic)mount() - OAuth security: consent screens + encrypted storage + JWT signing
- Async/await properly (don't block event loop)
- Structured error handling
- Avoid circular imports
- Test locally (
)fastmcp dev - Environment variables (never hardcode secrets)
- Comprehensive docstrings (LLMs read!)
- Production patterns (utils, pooling, retry, caching)
- OpenAPI auto-generation
- Health checks + monitoring
- Background tasks for long-running operations (
)task=True - 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.