MCP and production · 13 September 2026 · 4 min read

Build a minimal MCP server in Python

Wrap one existing FastAPI endpoint as an MCP tool in about ten lines using the current mcp package, then register it with claude mcp add.

WRAP, DON'T REWRITE Claude Code MCP server FastAPI call_tool(get_order_status) GET /orders/{id} 200 {"status": "shipped"} tool_result: "shipped"
The server adds one hop between the agent and an API that already works.

The fastest way to get value from MCP is not writing a new backend — it's wrapping an endpoint you already have. This note builds the smallest server that does that, using the current MCP Python SDK.

Verify the SDK before you copy anyone's example

The Python SDK moved to a v2 release that reworked its API; code you'll find in older blog posts using from mcp.server.fastmcp import FastMCP targets the earlier line. The current stable mcp package (v2, requiring Python 3.10+) exposes a server directly as MCPServer, with the same @mcp.tool() decorator pattern carried over (modelcontextprotocol/python-sdk README). Install it with:

uv add "mcp[cli]"   # or: pip install "mcp[cli]"

Wrapping an existing endpoint

Assume you already have a FastAPI app serving GET /orders/{id}. The MCP tool is a thin async wrapper around that same endpoint:

from mcp.server import MCPServer
import httpx

mcp = MCPServer("Orders")

@mcp.tool()
async def get_order_status(order_id: str) -> str:
    """Look up an order's status from the existing orders API."""
    async with httpx.AsyncClient() as client:
        r = await client.get(f"http://localhost:8000/orders/{order_id}")
        return r.json()["status"]

No JSON Schema to hand-write — the type hints (order_id: str, return str) generate it — and no protocol handling, because the SDK owns that layer entirely (python-sdk README). The docstring becomes the tool description the model sees, so write it the way you'd describe the tool to a new engineer, not a marketing line.

Running and testing it

Point the SDK's Inspector at the file to poke at the tool by hand before connecting a real client:

uv run mcp dev server.py

That opens the MCP Inspector, where you can call get_order_status with a sample ID and see the raw result the model would get, before any client is involved.

The same mcp package is also a client, which is a faster way to write an automated check than clicking through the Inspector. Serve the file over HTTP instead of stdio:

uv run mcp run server.py --transport streamable-http

then call it from a short script:

import asyncio
from mcp import Client

async def main() -> None:
    async with Client("http://localhost:8000/mcp") as client:
        result = await client.call_tool("get_order_status", {"order_id": "42"})
        print(result.structured_content)

asyncio.run(main())

That's enough to put the tool under a real test — assert on structured_content the way you'd assert on any API response (python-sdk README).

Registering it with a client

Claude Code's CLI adds a local stdio server with claude mcp add [options] <name> -- <command> [args...] (Claude Code MCP docs):

claude mcp add --transport stdio orders -- python server.py

The -- separates Claude Code's own flags from the server's launch command, so anything after it is passed straight to Python. Once added, ask Claude Code to look up an order — it will discover get_order_status and call it directly instead of you pasting the API response into chat.

Try it today

  1. Pick one read-only internal endpoint you already trust — a status lookup, a search, a health check.
  2. Write the tool function exactly as shown above: one async function, type-hinted arguments, a docstring.
  3. Run it through uv run mcp dev server.py and call it manually in the Inspector.
  4. Register it with claude mcp add --transport stdio <name> -- python server.py and ask your agent a question only that endpoint can answer.
  5. Read the tool's description back as the model would see it, and rewrite it if it's vague about what the tool does or doesn't do.

Where this breaks

  • Start with read-only endpoints. A tool that can mutate data needs explicit confirmation flows and careful scoping before you hand it to an agent.
  • A stdio server has no built-in authentication of its own; anything with permission to launch the process can call every tool it exposes.
  • Pin your dependency (mcp>=1.28,<2 if you need the older API, or a floor with <3 once on v2) since a major SDK version bump changes import paths, as this one did.

Next: MCP security — tool poisoning and prompt injection.