Introduction
The Model Context Protocol (MCP) has become the standard connector between AI assistants and external tools, data sources, and services. FastMCP — now at version 3.2 — powers roughly 70% of all MCP servers in production across every language and runtime.
If you need to expose a database, an internal API, a file system, or any custom logic to Claude Desktop, Claude Code, Cursor, or Cline, FastMCP is the fastest path. Its decorator-based API mirrors Flask and FastAPI, so Python developers can get a server running in minutes rather than hours.
In this guide, you will build a fully featured MCP server that demonstrates tools, resources, prompt templates, dependency injection, bearer-token authentication, and Docker deployment — everything you need to ship to production.
Prerequisites
- Python 3.10 or higher (3.12+ recommended for best type-inference support)
- UV package manager (or
pipworks too) - A recent version of Claude Desktop, Cursor, or Cline for local testing
- Basic familiarity with async Python
What You'll Build
A Project Intelligence Server — an MCP server that gives any AI assistant access to:
- A
search_projectstool that queries a local in-memory project database - A
get_project_statustool that returns live project metadata - A
projects://listresource that exposes a static snapshot of all projects - A
status_reportprompt template that instructs the AI on how to summarise findings - HTTP transport with bearer-token authentication for production use
Step 1: Project Setup
Create a new project with UV and install FastMCP:
uv init project-mcp-server
cd project-mcp-server
uv add fastmcpOr with pip:
mkdir project-mcp-server && cd project-mcp-server
python -m venv .venv && source .venv/bin/activate
pip install fastmcpVerify the installation:
fastmcp version
# FastMCP 3.2.4Create the main server file:
touch server.pyStep 2: Server Initialisation
Open server.py and set up the FastMCP instance:
from fastmcp import FastMCP, Context
# Server name appears in MCP client listings
mcp = FastMCP(
name="Project Intelligence Server",
instructions=(
"You have access to a project database. "
"Use search_projects to find projects by keyword, "
"and get_project_status to retrieve live metadata."
),
)The instructions parameter is sent to the AI at session start, framing how it should use your server's capabilities.
Step 3: Defining Tools
Tools are the most common MCP primitive — they are callable functions the AI can invoke to take action or retrieve computed data.
FastMCP infers the input schema automatically from Python type annotations and docstrings:
# Sample in-memory data — replace with your real database
PROJECTS = [
{"id": "p1", "name": "Noqta Platform", "status": "active", "lang": "TypeScript"},
{"id": "p2", "name": "Invoice API", "status": "active", "lang": "Python"},
{"id": "p3", "name": "Mobile App", "status": "paused", "lang": "React Native"},
{"id": "p4", "name": "Data Pipeline", "status": "archived", "lang": "Python"},
]
@mcp.tool
def search_projects(keyword: str, status: str = "all") -> list[dict]:
"""
Search projects by keyword and optional status filter.
Args:
keyword: Search term matched against project name and language.
status: Filter by project status. One of: active, paused, archived, all.
"""
results = PROJECTS
if status != "all":
results = [p for p in results if p["status"] == status]
keyword = keyword.lower()
return [p for p in results if keyword in p["name"].lower() or keyword in p["lang"].lower()]
@mcp.tool
async def get_project_status(project_id: str, ctx: Context) -> dict:
"""
Retrieve detailed status for a specific project by ID.
Args:
project_id: The unique project identifier (e.g. p1, p2).
"""
await ctx.info(f"Fetching status for project {project_id}")
project = next((p for p in PROJECTS if p["id"] == project_id), None)
if project is None:
raise ValueError(f"Project '{project_id}' not found")
# In production, you would fetch live data from a database here
return {
**project,
"last_commit": "2026-06-24",
"open_issues": 3,
"coverage": "87%",
}Key patterns here:
- Synchronous tools are fine for CPU-bound work;
async deftools allow awaiting I/O. - The
ctx: Contextparameter is injected automatically — you do not pass it when calling from the AI. ctx.info(),ctx.debug(),ctx.warning()emit log messages the MCP client can display.- Raising a standard Python exception surfaces a clean error message to the AI.
Step 4: Defining Resources
Resources expose data that the AI can read rather than compute. They map to URIs and are ideal for configuration, documentation, or snapshot data:
import json
@mcp.resource("projects://list")
def list_all_projects() -> str:
"""A snapshot of all projects in the system."""
return json.dumps(PROJECTS, indent=2)
@mcp.resource("projects://{project_id}/details")
def project_details(project_id: str) -> str:
"""Detailed view of a single project, addressed by its ID."""
project = next((p for p in PROJECTS if p["id"] == project_id), None)
if project is None:
return json.dumps({"error": f"Project {project_id} not found"})
return json.dumps(project, indent=2)URI templates with {placeholder} segments map automatically to function parameters. The mime_type defaults to text/plain; pass mime_type="application/json" for richer client rendering.
Step 5: Defining Prompt Templates
Prompts are reusable instructions that guide the AI's reasoning. They are surfaced in client UIs as selectable templates:
@mcp.prompt
def status_report(project_id: str, audience: str = "engineering") -> str:
"""
Generate a status report prompt for a given project and audience.
Args:
project_id: Target project identifier.
audience: Report audience — engineering, management, or client.
"""
tone = {
"engineering": "technical, focused on metrics and blockers",
"management": "concise, focused on timeline and risk",
"client": "non-technical, positive, focused on delivered value",
}.get(audience, "balanced")
return (
f"Using the project data for '{project_id}', write a {tone} status report. "
"Include: current status, recent progress, open issues, and next milestones. "
"Keep it under 300 words."
)Step 6: Dependency Injection with Depends
When multiple tools share an expensive resource — a database connection, an HTTP client, a cache — use Depends to initialise it once per request:
from contextlib import asynccontextmanager
from fastmcp.dependencies import Depends
async def get_db():
"""Yield a simulated DB client. In production, replace with a real connection."""
class FakeDB:
async def query(self, sql: str) -> list:
return [{"result": f"Executed: {sql}"}]
db = FakeDB()
yield db
# Cleanup happens here after the tool call completes
@mcp.tool
async def run_query(sql: str, db=Depends(get_db)) -> list:
"""
Execute a read-only SQL query against the project database.
Args:
sql: A SELECT statement to run.
"""
return await db.query(sql)The asynccontextmanager pattern ensures cleanup — closing connections, flushing buffers — always runs even if the tool raises an exception.
Step 7: Lifespan for Server-Level State
For state that should survive the entire server lifetime (a connection pool, a loaded ML model), use the lifespan API:
from fastmcp.server.lifespan import lifespan
@lifespan
async def app_lifespan(server):
"""Initialise and clean up server-wide resources."""
print("Server starting — loading project index...")
index = {p["id"]: p for p in PROJECTS}
yield {"index": index}
print("Server shutting down — index released.")
mcp_with_lifespan = FastMCP(
name="Project Intelligence Server (Stateful)",
lifespan=app_lifespan,
)
@mcp_with_lifespan.tool
def fast_lookup(project_id: str, ctx: Context) -> dict:
"""Instant lookup using the pre-built index."""
index = ctx.lifespan_context["index"]
return index.get(project_id, {"error": "not found"})Step 8: Testing with MCP Inspector
FastMCP ships with built-in support for the MCP Inspector — a browser-based testing UI:
fastmcp dev inspector server.pyThis launches a local server with hot-reload and opens the Inspector at http://localhost:6274. You can:
- Browse registered tools, resources, and prompts
- Call tools directly with custom arguments
- Inspect the full request/response cycle
For plain development without the Inspector UI:
fastmcp dev server.pyStep 9: Connecting to AI Clients
Claude Desktop
Add your server to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"project-intelligence": {
"command": "fastmcp",
"args": ["run", "/absolute/path/to/server.py"]
}
}
}Restart Claude Desktop. Your tools will appear in the tool list.
Claude Code
claude mcp add project-intelligence fastmcp run /absolute/path/to/server.pyCursor / Cline
In Cursor settings → MCP Servers, add:
{
"name": "project-intelligence",
"command": "fastmcp run /absolute/path/to/server.py"
}Cline's MCP settings file (.cline/mcp_settings.json) uses the same format as Claude Desktop.
Step 10: HTTP Transport with Authentication
For remote deployment or team-shared servers, switch to HTTP transport with bearer-token authentication:
from fastmcp.server.auth import StaticTokenVerifier
# Define allowed tokens with scopes
auth = StaticTokenVerifier(
tokens={
"prod-token-abc123": {"client_id": "prod-client", "scopes": ["read", "write"]},
"readonly-token-xyz": {"client_id": "viewer", "scopes": ["read"]},
},
required_scopes=["read"],
)
secured_mcp = FastMCP(
name="Project Intelligence Server",
auth=auth,
)
# Add the same tools, resources, and prompts as before
# ...
if __name__ == "__main__":
secured_mcp.run(transport="http", host="0.0.0.0", port=8000)Run the authenticated server:
python server.py
# or
fastmcp run server.py --transport http --host 0.0.0.0 --port 8000Connect a client with the token:
uvx fastmcp-remote https://your-server.example.com/mcp \
--header "Authorization: Bearer prod-token-abc123"For production, store tokens in environment variables — never hardcode them:
import os
auth = StaticTokenVerifier(
tokens={
os.environ["MCP_TOKEN_PROD"]: {"client_id": "prod", "scopes": ["read", "write"]},
}
)Step 11: Docker Deployment
Create a Dockerfile:
FROM python:3.12-slim
WORKDIR /app
# Install UV for fast dependency resolution
RUN pip install uv
COPY pyproject.toml uv.lock* ./
RUN uv sync --frozen --no-dev
COPY server.py .
EXPOSE 8000
CMD ["uv", "run", "fastmcp", "run", "server.py", "--transport", "http", "--host", "0.0.0.0", "--port", "8000"]Build and run:
docker build -t project-mcp-server .
docker run -p 8000:8000 \
-e MCP_TOKEN_PROD=your-secret-token \
project-mcp-serverFor teams using Docker Compose alongside an existing stack:
services:
mcp-server:
build: ./mcp
ports:
- "8000:8000"
environment:
MCP_TOKEN_PROD: ${MCP_TOKEN_PROD}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3Step 12: OpenTelemetry Tracing
FastMCP 3.x includes built-in OpenTelemetry support for production observability:
from fastmcp import FastMCP, Context
from fastmcp.telemetry import get_tracer
mcp = FastMCP("Project Intelligence Server")
@mcp.tool
async def search_projects_traced(keyword: str, ctx: Context) -> list[dict]:
"""Search projects with distributed tracing."""
tracer = get_tracer()
with tracer.start_as_current_span("search.filter") as span:
span.set_attribute("search.keyword", keyword)
results = [p for p in PROJECTS if keyword.lower() in p["name"].lower()]
span.set_attribute("search.result_count", len(results))
await ctx.info(f"Found {len(results)} projects for '{keyword}'")
return resultsSet your OTLP endpoint via environment variables to ship traces to Grafana, Jaeger, or Datadog:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_SERVICE_NAME=project-mcp-server \
python server.pyTroubleshooting
Tool not appearing in Claude Desktop
- Confirm the path in
claude_desktop_config.jsonis absolute, not relative. - Check that
fastmcpis on the PATH used by Claude Desktop (runwhich fastmcpand use the full path if needed). - Restart Claude Desktop fully after config changes.
TypeError: cannot unpack non-iterable NoneType object
This usually means a dependency generator returned None instead of yielding a value. Ensure your Depends function uses yield, not return.
Authentication errors on HTTP transport
- Confirm the
Authorization: Bearer <token>header is present and correctly cased. - Check
required_scopesmatches the scopes assigned to the token inStaticTokenVerifier.
Server crashes on Python 3.10 with type annotations
Use from __future__ import annotations at the top of the file, or upgrade to Python 3.12 where X | Y union syntax is fully supported.
Next Steps
- Explore FastMCP's OpenAPI integration to auto-generate an MCP server from any existing REST API spec.
- Add rate limiting using a Redis-backed counter inside a
Dependsdependency. - Connect your server to LangGraph or PydanticAI using
FastMCPToolsetfor multi-agent workflows. - Check the build-mcp-server-typescript-2026 tutorial for the TypeScript equivalent.
Conclusion
FastMCP 3.x gives Python developers a clean, production-ready path to the MCP ecosystem. With decorators for tools, resources, and prompts, dependency injection, bearer-token auth, and Docker-ready HTTP transport, you can ship a real MCP server in an afternoon. The same server connects to Claude Desktop, Claude Code, Cursor, and Cline without any client-specific code — one implementation, every AI assistant.