Claude-skill-registry fastmcp

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/fastmcp/SKILL.md
source content

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

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
    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
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
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

  • --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:

# v2.x and v3.0 compatible
from fastmcp import FastMCP

mcp = FastMCP("server")
# ... rest of code works the same

See: Official Migration Guide


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):

  • statusMessage
    from
    ctx.report_progress()
    is not forwarded to clients during background task polling (GitHub Issue #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

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:

  • 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)

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:

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:

  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:

# 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:

  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

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):

  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
# ✅ 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
    $ref
    in
    outputSchema
    wasn't being dereferenced (GitHub Issue #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:

# 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:

  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:

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.