writing/tutorial/2026/06
TutorialJun 24, 2026·25 min read

Building Production MCP Servers with FastMCP in Python

Learn to build, test, and deploy production-ready Model Context Protocol servers in Python using FastMCP 3.x. Covers tools, resources, prompts, authentication, and Docker deployment.

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 pip works 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_projects tool that queries a local in-memory project database
  • A get_project_status tool that returns live project metadata
  • A projects://list resource that exposes a static snapshot of all projects
  • A status_report prompt 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 fastmcp

Or with pip:

mkdir project-mcp-server && cd project-mcp-server
python -m venv .venv && source .venv/bin/activate
pip install fastmcp

Verify the installation:

fastmcp version
# FastMCP 3.2.4

Create the main server file:

touch server.py

Step 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 def tools allow awaiting I/O.
  • The ctx: Context parameter 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.py

This 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.py

Step 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.py

Cursor / 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 8000

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

For 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: 3

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

Set 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.py

Troubleshooting

Tool not appearing in Claude Desktop

  • Confirm the path in claude_desktop_config.json is absolute, not relative.
  • Check that fastmcp is on the PATH used by Claude Desktop (run which fastmcp and 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_scopes matches the scopes assigned to the token in StaticTokenVerifier.

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 Depends dependency.
  • Connect your server to LangGraph or PydanticAI using FastMCPToolset for 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.