Loading...
Loading...
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.
npx skill4agent add jezweb/claude-skills fastmcppip install fastmcp
# or
uv pip install fastmcpfrom 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()# Local development
python server.py
# With FastMCP CLI
fastmcp dev server.py
# HTTP mode
python server.py --transport http --port 8000auth_route$refctx.sample()AnthropicSamplingHandlerctx.sample_step()SampleSteptask=TrueBearerAuthProviderJWTVerifierOAuthProxyContext.get_http_request()fastmcp.Imagefrom fastmcp.utilities import Imageenable_docketenable_tasksrun_streamable_http_async()sse_app()streamable_http_app()run_sse_async()dependenciesoutput_schema=FalseFASTMCP_SERVER_FileSystemProviderSkillsProviderOpenAPIProviderProxyProviderfrom fastmcp import FastMCP
from fastmcp.providers import FileSystemProvider
mcp = FastMCP("server")
mcp.add_provider(FileSystemProvider(path="./tools", reload=True))ResourcesAsToolsPromptsAsToolsfrom fastmcp.transforms import Namespace, VersionFilter
mcp.add_transform(Namespace(prefix="api"))
mcp.add_transform(VersionFilter(min_version="2.0"))@mcp.tool(version="2.0")
async def fetch_data(query: str) -> dict:
# Clients see highest version by default
# Can request specific version
return {"data": [...]}@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}--reload@tool(auth=require_scopes("admin"))# requirements.txt
fastmcp<3# v2.x and v3.0 compatible
from fastmcp import FastMCP
mcp = FastMCP("server")
# ... rest of code works the same@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()data://file://resource://info://api://@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)@mcp.prompt("analyze")
def analyze_prompt(topic: str) -> str:
return f"Analyze {topic} considering: state, challenges, opportunities, recommendations."Contextfrom 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"}@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)}@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"]@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)}pendingrunningcompletedfailedcancelledstatusMessagectx.report_progress()mcp>=1.10.0ctx.sample()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}@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
}AnthropicSamplingHandlerOpenAISamplingHandlerctx.sample()py-key-value-aioFernetEncryptionWrapperfrom 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"))
))storagefrom 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)mcp = FastMCP("Server", lifespan=mcp_lifespan)
app = FastAPI(lifespan=mcp.lifespan) # ✅ MUST pass lifespan!context.fastmcp_context.set_state(key, value) # Store
context.fastmcp_context.get_state(key, default=None) # RetrieveRequest Flow:
→ ErrorHandlingMiddleware (catches errors)
→ TimingMiddleware (starts timer)
→ LoggingMiddleware (logs request)
→ RateLimitingMiddleware (checks rate limit)
→ ResponseCachingMiddleware (checks cache)
→ Tool/Resource Handlerfrom 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()))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)on_messageon_requeston_notificationon_call_toolon_read_resourceon_get_prompton_list_*import_server()mount()# 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@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 adminresource://prefix/pathprefix+resource://pathmain_server.mount(subserver, prefix="api", resource_prefix_format="path")JWTVerifierRemoteAuthProviderOAuthProxyOAuthProviderfrom 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)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)from fastmcp.auth import SupabaseProvider
auth = SupabaseProvider(
auth_route="/custom-auth", # Custom auth route (new in v2.14.2)
# ... other config
)Icon(url, size)Icon.from_file()Image.to_data_uri()httpx.AsyncClientFastMCP.from_openapi(spec, client, route_maps)FastMCP.from_fastapi(app, httpx_client_kwargs)mcpserverapp# ✅ CORRECT: Module-level export
mcp = FastMCP("server") # At module level!
# ❌ WRONG: Function-wrapped
def create_server():
return FastMCP("server") # Too late for cloud!{"mcpServers": {"my-server": {"url": "https://project.fastmcp.app/mcp", "transport": "http"}}}RuntimeError: No server object found at module levelmcp = FastMCP("server")RuntimeError: no running event loopTypeError: object coroutine can't be used in 'await'async defawaitdefTypeError: missing 1 required positional argument: 'context'Contextasync def tool(context: Context)ValueError: Invalid resource URI: missing scheme@mcp.resource("data://config")@mcp.resource("config")TypeError: get_user() missing 1 required positional argument@mcp.resource("user://{user_id}/profile")def get_user(user_id: str)ValidationError: value is not a valid integerclass Params(BaseModel): query: str = Field(min_length=1)ConnectionError: Server using different transportmcp.run(){"command": "python", "args": ["server.py"]}mcp.run(transport="http", port=8000){"url": "http://localhost:8000/mcp", "transport": "http"}ModuleNotFoundError: No module named 'my_package'pip install -e .export PYTHONPATH="/path/to/project"DeprecationWarning: 'mcp.settings' is deprecatedos.getenv("API_KEY")mcp.settings.get("API_KEY")OSError: [Errno 48] Address already in use--port 8001lsof -ti:8000 | xargs kill -9TypeError: Object of type 'ndarray' is not JSON serializablelist[float]{"values": np_array.tolist()}# ❌ 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!$refoutputSchemaTypeError: Object of type 'datetime' is not JSON serializabledatetime.now().isoformat().decode('utf-8')ImportError: cannot import name 'X' from partially initialized module__init__.pyfrom .api_client import APIClientDeprecationWarning: datetime.utcnow() is deprecateddatetime.now(timezone.utc)datetime.utcnow()RuntimeError: Event loop is closedconnect()RuntimeError: OAuth tokens lost on restartValueError: Cache not persistingFernetEncryptionWrapperRuntimeError: Database connection never initializedWarning: MCP lifespan hooks not runningapp = FastAPI(lifespan=mcp.lifespan)RuntimeError: Rate limit not checked before cachingRecursionError: maximum recursion depth exceededself.next()result = await self.next(tool_name, arguments, context)RuntimeError: Subserver changes not reflectedValueError: Unexpected tool namespacingimport_server()mount()import_server()mount()ValueError: Resource not found: resource://api/usersresource://prefix/pathprefix+resource://pathresource_prefix_format="path"SecurityWarning: Authorization bypass possibleenable_consent_screen=TrueValueError: JWT signing key required for OAuth Proxyjwt_signing_keysecrets.token_urlsafe(32)FASTMCP_JWT_SIGNING_KEYOAuthProxy(jwt_signing_key=...)ValueError: Invalid data URI formatIcon.from_file("/path/icon.png", size="medium")Image.to_data_uri()Warning: Lifespan runs per-server, not per-sessionImportError: cannot import name 'BearerAuthProvider' from 'fastmcp.auth'BearerAuthProviderJWTVerifierOAuthProxy# Before (v2.13.x)
from fastmcp.auth import BearerAuthProvider
# After (v2.14.0+)
from fastmcp.auth import JWTVerifier
auth = JWTVerifier(issuer="...", audience="...", public_key="...")AttributeError: 'Context' object has no attribute 'get_http_request'Context.get_http_request()InitializeResultImportError: cannot import name 'Image' from 'fastmcp'fastmcp.Image# Before (v2.13.x)
from fastmcp import Image
# After (v2.14.0+)
from fastmcp.utilities import Image/mcp/mcp/mcp/mcp/# ❌ 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"}lifespan=mcp.lifespanRuntimeError: No active context foundtask=True# 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.3utils.pyhttpx.AsyncClientget_client()retry_with_backoff(func, max_retries=3, initial_delay=1.0, exponential_base=2.0)TimeBasedCache(ttl=300).get().set()pytestcreate_test_client(test_server)await client.call_tool()Client("server.py")list_tools()call_tool()list_resources()fastmcp dev server.py # Run with inspector
fastmcp install server.py # Install to Claude Desktop
FASTMCP_LOG_LEVEL=DEBUG fastmcp dev # Debug loggingserver.pyrequirements.txt.envREADME.mdsrc/tests/pyproject.toml/jlowin/fastmcpimport_server()mount()fastmcp devtask=Truectx.sample(tools=[...])