🐍
Python
FastAPI
Package

MCP Server Tutorial

Python + FastAPI + Pydantic

Python Tutorial
20 min read
January 20, 2025

How to Create MCP Server in 2026

All Tools Directory Team
MCPPythonTutorialFastAPI

Who this guide is for

If you're new to MCP and want a minimal, production-ready Python example, this walkthrough is for you. In ~20 minutes you'll stand up an MCP server, expose one simple tool, validate inputs/outputs with types, run it locally, and package it for reuse.

Prerequisites

  • Python 3.11+
  • macOS/Linux/WSL (Windows works with minor path tweaks)
  • git and a terminal

1) Create a clean project & virtual environment

mkdir mcp-hello && cd mcp-hello
python -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip

2) Add the minimal dependencies

We'll use FastAPI for a lightweight HTTP server and Pydantic for contracts (schemas).

pip install fastapi uvicorn pydantic[dotenv]

3) Define your MCP tool contracts (I/O typing)

Create models.py:

from pydantic import BaseModel, Field
from typing import Literal, Optional

class ToolSpec(BaseModel):
    name: str
    description: str

class ListToolsResponse(BaseModel):
    tools: list[ToolSpec]

class AddArgs(BaseModel):
    a: float = Field(..., description="First number")
    b: float = Field(..., description="Second number")

class AddResult(BaseModel):
    sum: float

class ToolCall(BaseModel):
    request_id: str
    tool: Literal["math.add"]
    args: AddArgs
    deadline_ms: Optional[int] = 2000

class ToolResponse(BaseModel):
    request_id: str
    status: Literal["ok","error"]
    result: Optional[AddResult] = None
    error: Optional[str] = None
    latency_ms: Optional[int] = None

4) Implement a minimal MCP server (simple tool)

Create main.py:

import time
from fastapi import FastAPI, HTTPException
from models import (
    ToolSpec, ListToolsResponse, ToolCall, ToolResponse, AddResult
)

app = FastAPI(title="mcp-hello")

@app.get("/tools", response_model=ListToolsResponse)
def list_tools():
    return ListToolsResponse(
        tools=[ToolSpec(name="math.add", description="Add two numbers")]
    )

@app.post("/tools/execute", response_model=ToolResponse)
def execute(call: ToolCall):
    start = time.time()
    if call.tool != "math.add":
        raise HTTPException(status_code=400, detail="unknown tool")
    s = call.args.a + call.args.b
    return ToolResponse(
        request_id=call.request_id,
        status="ok",
        result=AddResult(sum=s),
        latency_ms=int((time.time() - start) * 1000),
    )

Run it

uvicorn main:app --reload --port 8787

Test with curl:

curl -s http://localhost:8787/tools | jq
curl -s -X POST http://localhost:8787/tools/execute \
  -H "content-type: application/json" \
  -d '{"request_id":"r1","tool":"math.add","args":{"a":2.5,"b":4.5},"deadline_ms":1500}' | jq

This is your minimal MCP Python example—typed contracts + a simple tool.

5) Wire it into your vibe/agent client (request/response flow)

Typical client call (pseudo):

import requests, uuid
req = {
  "request_id": str(uuid.uuid4()),
  "tool": "math.add",
  "args": {"a": 3, "b": 9},
  "deadline_ms": 1500
}
res = requests.post("http://localhost:8787/tools/execute", json=req, timeout=2).json()
print(res["result"]["sum"])  # 12

6) Add guardrails: timeouts, errors, logging

Update main.py:

import logging
from fastapi import Request
logger = logging.getLogger("mcp")
logging.basicConfig(level=logging.INFO)

@app.middleware("http")
async def add_request_logging(request: Request, call_next):
    rid = request.headers.get("x-request-id", "no-rid")
    t0 = time.time()
    response = await call_next(request)
    logger.info("rid=%s path=%s status=%s latency_ms=%d",
                rid, request.url.path, response.status_code, int((time.time()-t0)*1000))
    return response

Client can set X-Request-Id so you can trace calls across systems.

7) (Optional) Basic authentication for local testing

pip install fastapi[all] python-multipart

Add simple API key check:

from fastapi.security.api_key import APIKeyHeader
from fastapi import Security

API_KEY = "dev-key-change-me"
api_key_header = APIKeyHeader(name="x-api-key", auto_error=True)

def verify_key(key: str = Security(api_key_header)):
    if key != API_KEY:
        raise HTTPException(status_code=401, detail="invalid api key")

@app.get("/tools", dependencies=[Security(verify_key)])
def list_tools_secure(): ...
@app.post("/tools/execute", dependencies=[Security(verify_key)])
def execute_secure(call: ToolCall): ...

For production, prefer OIDC for humans and mTLS/OAuth for services (see our security guide).

8) Run & debug like a pro

Hot Reload

uvicorn main:app --reload

Pretty Errors

pip install rich then from rich import traceback; traceback.install()

Contracts First

Break a field type on purpose to confirm validation errors are descriptive.

9) Package for reuse

pyproject.toml

[project]
name = "mcp-hello"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["fastapi","uvicorn","pydantic"]

[project.scripts]
mcp-hello = "main:app"

Build & publish (optional):

pip install build
python -m build

10) Next steps

  • Add more tools (e.g., files.read, repo.search) with tight schemas.
  • Stream large outputs (chunked responses) for better UX.
  • Add rate limits, timeouts, and circuit breakers when calling downstream APIs.
  • Containerize with a minimal base image and non-root user.
  • Write contract tests in CI to prevent breaking changes.

FAQs

How to build an MCP server if I'm brand new to Python?

Follow this guide, then extend the math.add example. Keep contracts tiny and validated with Pydantic.

How to create MCP server endpoints for real tools?

Model each tool's inputs/outputs with Pydantic, implement the handler, and expose it under /tools/execute. Validate, log, and cap output size.

How to develop MCP server features safely?

Use feature flags, add structured logs with request_id, and write contract tests. Add auth (OIDC/mTLS) before exposing outside dev.

Key Takeaways

  • Setup: Python venv + FastAPI + Pydantic for type-safe contracts
  • Structure: Define schemas first, implement handlers, expose via REST endpoints
  • Testing: Use curl/requests to test locally with hot reload
  • Production: Add logging, auth, rate limits, and proper error handling
  • Packaging: Use pyproject.toml for distribution and reuse