Noqta
  • Home
  • Services
  • About us
  • Writing
  • Sign in
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.

AI Bot
AI Bot
Author
·EN · FR · AR

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.

● Tags
#python#mcp#fastmcp#ai-agents#api#intermediate#25 min read
● Share
● A question?

Talk to a Noqta agent about this article.

AI Bot
AI Bot
Author · noqta
Follow ↗

● Read next

MCP‑Governed Agentic Automation: How to Ship AI Agents Safely in 2026
● Tutorial

MCP‑Governed Agentic Automation: How to Ship AI Agents Safely in 2026

Feb 3, 2026
Introduction to MCP: A Beginner's Quickstart Guide
● Tutorial

Introduction to MCP: A Beginner's Quickstart Guide

Jan 25, 2026
Building an Autonomous AI Agent with Agentic RAG and Next.js
● Tutorial

Building an Autonomous AI Agent with Agentic RAG and Next.js

Feb 11, 2026
Noqta
Terms and Conditions · Privacy Policy
Services
  • AI Automation
  • AI Agents
  • CX Automation
  • Vibe Coding
  • Project Management
  • Quality Assurance
  • Web Development
  • API Integration
  • Business Applications
  • Maintenance
  • Low-Code/No-Code
Links
  • About Us
  • How It Works?
  • News
  • Tutorials
  • Blog
  • Contact
  • FAQ
  • Resources
Regions
  • Saudi Arabia
  • UAE
  • Qatar
  • Bahrain
  • Oman
  • Libya
  • Tunisia
  • Algeria
  • Morocco
Company
  • Noqta, Tunisia, Tunis, phone +216 40 385 594
© Noqta. All rights reserved.