Updated May 2026Comparison22 min read

Build MCP in Python: FastMCP vs FastAPI-MCP vs Python SDK

Building an MCP server in Python in 2026? You have three serious options, each with a different trade-off between ergonomics and control. FastMCP wraps everything in decorators. FastAPI-MCP turns an existing FastAPI app into an MCP server with one import. The official Python SDK gives you raw spec control at the cost of more boilerplate. We wrote the same add(a, b) tool in all three so you can see exactly where they pull apart.

Editorial illustration: three luminous teal Python-snake glyphs in a row — a FastMCP decorator badge, a FastAPI-MCP bridge arch, an official Python SDK Anthropic-style hex — connected by softly glowing protocol arrows on a midnight navy backdrop.
On this page · 14 sections
  1. TL;DR + decision tree
  2. What each framework is for
  3. Side-by-side matrix
  4. FastMCP — install + recipe
  5. FastAPI-MCP — install + recipe
  6. Python SDK — install + recipe
  7. Transport handling deep dive
  8. Decorator vs handler ergonomics
  9. Migration paths
  10. Decision tree
  11. Common pitfalls
  12. Community signal
  13. FAQ
  14. Sources

TL;DR + decision tree

  • Building from scratch? Pick FastMCP. The decorator API is the lowest-boilerplate way to ship an MCP server in Python today, supports stdio and streamable HTTP, and is the de-facto community standard. It’s also what most server examples in the wild are written in.
  • Already have a FastAPI app? Pick FastAPI-MCP. It inspects your existing routes and exposes them as MCP tools — five-minute setup, no rewrite. The trade-off is that it’s tuned for the “wrap existing API” shape; pure MCP primitives like prompts and resources need more work.
  • Need protocol-level control? Pick the official Python SDK from modelcontextprotocol/python-sdk. It’s the reference implementation, so when you read the spec, the SDK’s API maps 1:1 to the wire format. Use it when FastMCP’s abstractions hide something you need to control — custom transports, capability negotiation, unusual handler shapes.

These aren’t mutually exclusive. FastMCP is built on top of the official SDK, so dropping down for one tricky bit is easy. FastAPI-MCP can sit next to a FastMCP server in the same deployment if you want to expose both an HTTP API and an MCP server. We’ll get into the specifics with code below.

What each framework is for

Quick aside on why Python at all. The canonical Anthropic SDK lineage for MCP is Python-first — the reference implementation at modelcontextprotocol/python-sdk shipped first and the TypeScript SDK followed. That early-mover effect cascaded: most public MCP server examples are in Python, the largest collection of community tutorials is in Python, and FastMCP’s rise as the de-facto framework reinforced the pattern. There’s also a softer reason — MCP servers often wrap data, ML, or scripting workflows that live in notebooks and REPL sessions, and Python’s interactive culture pairs naturally with iterative server development. You write a tool, hot-reload, watch the agent call it, tweak the docstring, repeat. TypeScript can do the same loop but with more friction.

The TypeScript SDK is real and active — modelcontextprotocol/typescript-sdk tracks the spec in lockstep with the Python one — but for now, if you ask any MCP-curious developer to point at five servers worth reading, four will be Python. That’s the lens for this post: if your team is shipping in another language, the comparison still applies in shape (low-level SDK vs high-level decorator framework vs “wrap your existing service” bridge) even though the specific names change.

Before going deeper, here’s the one-line shape of each:

  • FastMCP — “FastAPI for MCP.” Decorate Python functions; the framework handles the protocol. Most ergonomic of the three.
  • FastAPI-MCP — point it at a FastAPI app, get an MCP server. Bridge-shaped. Fastest path when you already have endpoints.
  • Official Python SDK — the reference implementation. Lowest level, most explicit. What FastMCP is built on.

MCP’s three primitives — tools (function-call), resources (read-only data), prompts (parameterised system messages) — show up in all three. The difference is how much code you write to register one. We’ll show the same add(a, b) -> int tool in each framework so you can eyeball the ergonomics. If you’re completely new to the protocol, our What is MCP primer covers the JSON-RPC wire format underneath, and the OAuth 2.1 deep dive covers what changes when you take any of these remote.

Side-by-side matrix

Every cell comes from the official repo or docs. Numbers and version-specific behaviour checked 2026-05-11.

DimensionFastMCPFastAPI-MCPPython SDK
Repojlowin/fastmcptadata-org/fastapi-mcpmodelcontextprotocol/python-sdk
LicenseMITMITMIT
MaintainerJeremiah Lowin (community-led)Tadata (community)Anthropic / MCP working group (official)
API styleDecorator (@tool, @resource, @prompt)Bridge — inspects FastAPI routesExplicit handler registration
Best forBuilding servers from scratchWrapping existing FastAPI appsSpec-level control, custom transports
Transportsstdio + streamable HTTPHTTP (via FastAPI host)stdio + streamable HTTP
Built on top ofOfficial Python SDKFastAPI + MCP SDK pieces
Boilerplate for add(a,b)~7 lines~10 lines (FastAPI app + bridge)~25 lines
OAuth helpersFirst-class (FastMCP v2)Inherits FastAPI middlewareManual wiring
Pydantic input schemasAuto via type hintsAuto via FastAPIAuto via type hints, can override

Three things jump out. First, FastMCP is the only framework whose entire API surface is built around MCP’s three primitives — tools, resources, prompts — each with its own decorator. Second, FastAPI-MCP is the only one designed for the “I already have an HTTP API” case; everyone else assumes you’re writing MCP-first. Third, the official SDK is the only one where the API maps 1:1 to the spec — useful when you’re debugging wire-level behaviour or implementing the protocol yourself.

FastMCP — install + recipe

Python MCP framework

FastMCP

MIT · jlowin (community-led)

View on GitHub →

What it does best

FastMCP is the right answer when you’re building an MCP server from scratch and you don’t already have a Python HTTP API to wrap. The decorator model is the lowest-boilerplate way to register tools, resources, and prompts in any language, Python or otherwise. Type-annotated function arguments become tool input schemas via Pydantic; docstrings become tool descriptions; the function body is the implementation. That’s it. Most of the published Python MCP servers in the wild today are written in FastMCP, which means examples, idioms, and Stack Overflow answers are easy to find.

Pick this if you...

  • Are starting from scratch and want the shortest path to a working MCP server
  • Need both stdio (for Claude Desktop / Cursor / Claude Code) and streamable HTTP (for remote agents) from the same codebase
  • Want OAuth 2.1 wiring for free when you go remote — v2 ships helpers that handle the discovery and PKCE dance
  • Like the FastAPI mental model (functions + decorators + Pydantic) and want the same vibe for MCP

Recipe: a minimal add(a, b) tool in FastMCP

Install with uv add fastmcp or pip install fastmcp, then save this as server.py:

from fastmcp import FastMCP

mcp = FastMCP("calculator")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b

if __name__ == "__main__":
    mcp.run()

That’s the whole server. mcp.run() defaults to stdio, which means you can wire this directly into Claude Desktop or Cursor with a one-liner in mcp_servers.json pointing command at python and args at ["server.py"]. To serve over HTTP, swap the last line for mcp.run(transport="streamable-http", port=8000) and the same tool is reachable from any remote MCP client. The input schema, output type, and tool description are all derived from the function signature and docstring — no separate schema file, no manual JSON Schema, no handler registration.

Skip it if...

You already have a FastAPI app with the endpoints your agent needs — re-implementing them as FastMCP tools is wasted work. Use FastAPI-MCP to bridge the existing app instead. Or, if you’re implementing custom transport behaviour or doing something the protocol doesn’t standardise yet, drop to the official Python SDK for full control.

FastAPI-MCP — install + recipe

Python MCP bridge

FastAPI-MCP

MIT · Tadata (community)

View on GitHub →

What it does best

FastAPI-MCP is the only framework in this trio designed for the “I already have an API, I want an MCP” case. Point it at a FastAPI app and it inspects every route — pulls operation IDs, request bodies (Pydantic v2 already), and response models — then exposes each route as an MCP tool. Your FastAPI clients keep working over HTTP; new MCP clients see the same endpoints as tools. Zero rewrite, no duplicated logic. For a team with 30 well-typed endpoints, this is a hour-long migration to MCP rather than a week-long one.

Pick this if you...

  • Already have a FastAPI app and want to expose it to agents without writing parallel code
  • Want to keep your existing FastAPI auth (fastapi-users, authlib, dependency-injection bearers) and have it apply to MCP calls too
  • Are comfortable with FastAPI’s patterns — operation IDs, response models, dependencies — and want them to carry over
  • Need the MCP server and the HTTP API to live in one deployment unit (same container, same uvicorn process)

Recipe: the same add(a, b) tool, wrapped from FastAPI

Install with uv add fastapi-mcp or pip install fastapi-mcp:

from fastapi import FastAPI
from fastapi_mcp import FastApiMCP

app = FastAPI()

@app.post("/add", operation_id="add")
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b

mcp = FastApiMCP(app)
mcp.mount()

# uvicorn server:app --reload

Two extra lines on top of a normal FastAPI app: FastApiMCP(app) and mcp.mount(). Theoperation_id="add" hint becomes the tool name (without it, FastAPI auto-generates an ID from the route path and method that’s harder for the model to read). Notice the function body is identical to the FastMCP version — the difference is the route wrapper and the bridge mount. HTTP clients now hit POST /add; MCP clients hit the mounted MCP endpoint (default /mcp) and see add in their tool list. One process, two protocols.

Skip it if...

You’re writing MCP-first — adding FastAPI as a dependency purely to get a bridge is overkill. FastMCP has roughly the same ergonomics for that case with a lighter footprint. Also skip it if your server is heavy on prompts and resources rather than tools: FastAPI-MCP’s sweet spot is endpoint-to-tool conversion, and the non-tool primitives need more manual plumbing.

Official Python SDK — install + recipe

Python MCP framework

MCP Python SDK

MIT · Anthropic / MCP working group (official)

View on GitHub →

What it does best

The official SDK is the reference implementation of MCP in Python — the codebase the spec authors use to validate the wire format. Its API maps 1:1 to the protocol: you explicitly register handlers for list_tools, call_tool, list_resources, and so on. That’s verbose for the common case, but it’s the right shape when you’re doing something off-script — implementing a custom transport, debugging a wire-level bug against another client, or porting the spec to a new environment. FastMCP is built on top of it, which is why dropping down for one tricky bit and back up is straightforward.

Pick this if you...

  • Need protocol-level control — custom transports, manual capability negotiation, unusual handler shapes
  • Are implementing or debugging against the spec directly and want zero abstraction between you and the wire format
  • Want the leanest dependency tree available (no extra framework on top, just what the spec needs)
  • Are writing library code that other MCP servers might depend on — the official SDK is the stable API surface

Recipe: the same add(a, b) tool, low-level

Install with uv add mcp or pip install mcp. Here’s the same tool with the SDK’s explicit handler style:

import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

server = Server("calculator")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="add",
            description="Add two integers and return the sum.",
            inputSchema={
                "type": "object",
                "properties": {
                    "a": {"type": "integer"},
                    "b": {"type": "integer"},
                },
                "required": ["a", "b"],
            },
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "add":
        result = arguments["a"] + arguments["b"]
        return [TextContent(type="text", text=str(result))]
    raise ValueError(f"unknown tool: {name}")

async def main():
    async with stdio_server() as (read, write):
        await server.run(read, write, server.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Roughly four times the code of the FastMCP version for the same behaviour. That cost buys explicit control — the input schema is hand-written (no Pydantic-derived guesses), tool dispatch is a single function you own, and the transport plumbing is visible. For a single-tool server it’s overkill; for a server with non-standard handler patterns or custom transport logic, the explicitness is the whole point. The SDK also lets you use FastMCP from within it (yes, there’s a high-level facade in the official package called mcp.server.fastmcp.FastMCP) — but the standalone jlowin/fastmcp framework is more actively developed in v2.

Skip it if...

You’re shipping product, not building protocol-level tooling. FastMCP gets the same server up in a quarter of the code, with the same wire-level behaviour, and v2 supports everything the spec requires. Reach for the official SDK only when you specifically need the lower-level API.

Transport handling deep dive

The MCP spec defines two transports: stdio (a child process talks JSON-RPC over its own stdin/stdout) and streamable HTTP (long-lived HTTP connection with server-sent events for responses). Stdio is the default for local clients — Claude Desktop, Cursor, Claude Code, and most IDE integrations launch MCP servers as subprocesses and pipe JSON-RPC frames through stdin/stdout. Streamable HTTP is the 2025 transport story for remote servers; it replaced the earlier SSE-only transport and is the wire format you want for any MCP server reachable over a network.

FastMCP v2 handles both transports as a runtime option. Calling mcp.run() defaults to stdio; calling mcp.run(transport="streamable-http", port=8000) spins up an HTTP server with the same decorated tools, no code changes elsewhere. If you want full control of the ASGI lifecycle — embedding in another HTTP stack, sharing middleware, mounting at a specific path — mcp.streamable_http_app() returns a starlette-ish ASGI application you can hand to uvicorn, hypercorn, or any serverless adaptor (Mangum for Lambda, Cloud Run’s default ingress, Fly Machines). OAuth 2.1 helpers slot into the HTTP transport path: FastMCP v2 ships helpers for the authorisation-server discovery dance, PKCE verification, and bearer-token validation, so adding OAuth to a remote FastMCP server is closer to configuration than implementation.

FastAPI-MCP is HTTP-only by construction — it lives inside a FastAPI app, and FastAPI is an HTTP framework. That’s fine; the trade-off is that you can’t use FastAPI-MCP for a stdio-only local server without standing up an HTTP server you don’t need. On the upside, you inherit FastAPI’s middleware story for auth: any FastAPI authentication dependency you’ve already wired (OAuth, JWT bearer, custom dependency-injected session checks) applies to the MCP routes for free. If your team has spent two years getting auth right in FastAPI, that’s a real edge.

Official Python SDK exposes both transports but with more manual plumbing. For stdio: async with stdio_server() as (read, write): then await server.run(read, write, ...). For HTTP, the SDK provides a streamable_http_app() helper that builds the ASGI app the same way FastMCP does — in fact, FastMCP’s HTTP support is built on this primitive. You wire OAuth yourself if you go remote: validate bearer tokens in middleware, implement .well-known/oauth-authorization-server endpoints, handle PKCE if you’re the authorisation server. It’s not hard, but it’s code FastMCP wrote for you.

Which framework makes remote MCP easiest? FastMCP for greenfield, FastAPI-MCP if you already have a FastAPI app and a working auth story you want to reuse. The official SDK rarely wins here unless you’re shipping a custom transport (a websocket variant, a binary protocol, an in-process bridge) that needs the lower-level API. For the auth-and-deploy story end-to-end, our OAuth 2.1 for remote MCP servers walks through it framework by framework.

Quick deployment shape comparison since this question comes up a lot. Cloudflare Workers: the official approach uses the JavaScript/TypeScript SDK with the Workers runtime — Python frameworks don’t run there natively, so you’d need Pyodide or a sidecar. AWS Lambda: FastMCP’s ASGI app deploys cleanly via Mangum or AWS Lambda Web Adapter; FastAPI-MCP does the same since FastAPI is also ASGI. Both have cold-start considerations that get worse with heavy dependency trees — keep imports lean. Cloud Run / Fly / Render: all three frameworks deploy as standard container images, no special adaptation needed. Set min_instances=1 on Cloud Run to avoid cold starts on each MCP connection. Long-lived VM or Kubernetes: same story — uvicorn or hypercorn fronting the FastMCP or FastAPI app. One thing worth knowing: streamable HTTP holds connections open longer than typical REST, so configure your load balancer and proxy timeouts generously (5+ minutes is normal, not 30 seconds).

Decorator vs handler-class ergonomics

The add(a, b) example earlier showed line-count differences; the bigger gap is in how each framework handles schema descriptions, type validation, and error propagation. Below is the same tool written three ways with full plumbing — parameter descriptions, error handling, and a validation-error path.

FastMCP:

from typing import Annotated
from pydantic import Field
from fastmcp import FastMCP

mcp = FastMCP("calculator")

@mcp.tool()
def add(
    a: Annotated[int, Field(description="First addend", ge=-1_000_000, le=1_000_000)],
    b: Annotated[int, Field(description="Second addend", ge=-1_000_000, le=1_000_000)],
) -> int:
    """Add two integers and return the sum."""
    if a + b > 2_000_000:
        raise ValueError("sum overflow safety guard")
    return a + b

The Annotated[..., Field(description=...)] shape is Pydantic v2’s idiom for attaching descriptions to primitive types. FastMCP picks those up and threads them into the tool’s input schema, so the agent sees not just “a is an int” but “a is the first addend, bounded -1M..1M.” Validation errors raised inside the function bubble up as tool errors that the client sees as structured failures, not server crashes.

FastAPI-MCP:

from typing import Annotated
from fastapi import FastAPI, Query, HTTPException
from fastapi_mcp import FastApiMCP

app = FastAPI()

@app.post("/add", operation_id="add", summary="Add two integers")
def add(
    a: Annotated[int, Query(description="First addend", ge=-1_000_000, le=1_000_000)],
    b: Annotated[int, Query(description="Second addend", ge=-1_000_000, le=1_000_000)],
) -> int:
    if a + b > 2_000_000:
        raise HTTPException(status_code=400, detail="sum overflow safety guard")
    return a + b

mcp = FastApiMCP(app)
mcp.mount()

Same Pydantic-style annotations, but the error path is FastAPI’s — HTTPException becomes an MCP tool error via FastAPI-MCP’s response translator. Thesummary field on the route decorator carries through as the tool description, which is one of those tiny-but-important details that catches teams who skip it.

Official Python SDK:

from mcp.server import Server
from mcp.types import Tool, TextContent

server = Server("calculator")

ADD_SCHEMA = {
    "type": "object",
    "properties": {
        "a": {"type": "integer", "description": "First addend", "minimum": -1_000_000, "maximum": 1_000_000},
        "b": {"type": "integer", "description": "Second addend", "minimum": -1_000_000, "maximum": 1_000_000},
    },
    "required": ["a", "b"],
}

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(name="add", description="Add two integers", inputSchema=ADD_SCHEMA)]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name != "add":
        raise ValueError(f"unknown tool: {name}")
    a, b = arguments["a"], arguments["b"]
    if not (-1_000_000 <= a <= 1_000_000) or not (-1_000_000 <= b <= 1_000_000):
        return [TextContent(type="text", text="error: parameter out of range")]
    return [TextContent(type="text", text=str(a + b))]

The schema is hand-written JSON Schema rather than derived from type hints, and you have to enforce bounds yourself inside the handler — Pydantic doesn’t run unless you ask it to. Errors are typically returned as text content rather than raised, because raising from call_tool surfaces as a protocol-level error, not a tool-level error the agent can recover from. That’s the design choice you’re making with the official SDK: you own the semantics. For most teams the FastMCP shape is the right default; for teams writing protocol-level libraries, the explicit shape is the point.

Migration paths

Most projects don’t start at “which framework will I be using in two years.” They start with what fits the first week and grow into something different. Two migrations are common enough to plan for.

Python SDK → FastMCP. The trigger here is always boilerplate. You started on the official SDK because you wanted to learn the wire format, or because the early tutorials used it, or because you needed one weird transport thing — and now your list_tools handler is 200 lines of schema dictionaries and your call_tool dispatch is a 40-case if-else. FastMCP is the answer: it wraps the same SDK underneath, so the wire-level behaviour is identical. The migration is mostly mechanical — for each tool, delete the schema dictionary, add a typed function signature with Pydantic Field descriptions, and decorate with @mcp.tool(). The function body stays as-is. If you have unusual handler shapes (custom cancellation, streaming responses, mid-call notifications) you can keep those on the official SDK and use FastMCP for the rest — they share the underlying server primitive.

FastAPI-MCP → FastMCP. This one’s trickier and worth thinking about up front. The pitch for FastAPI-MCP is “wrap your existing API,” which works perfectly when your MCP server really is your existing API. The pain shows up when your MCP server starts wanting things that aren’t routes — prompts, resources, custom tool descriptions that don’t map cleanly to HTTP verbs, tool names that should differ from operation IDs, parameters the agent sees that the HTTP client never does. At that point FastAPI-MCP becomes a tax: every decision is “how would I express this in FastAPI” rather than “how would I express this in MCP.” The migration is more involved than the SDK case — you’re replacing route decorators with tool decorators, dropping operation IDs in favour of tool names, and pulling auth out of FastAPI middleware into FastMCP’s OAuth helpers. The win is that the resulting code is shaped like the protocol you’re implementing.

A reasonable hedge if you’re unsure: run both. Keep the FastAPI app for HTTP clients, but add a FastMCP server next to it for tools you want to design MCP-first. They can share business logic via a shared module; they don’t have to share a transport. Several teams in production do exactly this — FastAPI for the dashboard, FastMCP for the agent.

One migration tip that saves grief: don’t do tool-by-tool rewrites all at once. Pick the noisiest five tools (the ones with the worst descriptions, the most awkward parameter shapes, or the ones the agent gets wrong most often) and migrate just those. Run the new FastMCP server in parallel on a different port, point a test client at it, compare the agent’s behaviour against the original. If the tools behave better — better descriptions, better validation, fewer rejected calls — that’s your signal to migrate the rest. If they don’t, the migration probably isn’t worth the effort and you should focus on improving descriptions and types in the existing setup instead. FastMCP’s value over FastAPI-MCP is mostly about shape fit for MCP, not raw capability — measure the shape fit before you migrate.

Decision tree

Three questions, in order. Stop at the first “yes.”

  1. Do you already have a FastAPI app you want to expose to agents? → FastAPI-MCP. Your existing routes become MCP tools with two extra lines of code; auth, validation, and deployment stay as they are.
  2. Are you building from scratch and want the shortest path to a working MCP server? → FastMCP. Decorator-based, supports both transports, ships OAuth helpers, has the deepest community example library.
  3. Do you need protocol-level control over transports, handlers, or capability negotiation? → Official Python SDK. Verbose but precise; FastMCP is built on top, so you can mix.

Most teams land on FastMCP. A meaningful minority with mature FastAPI services land on FastAPI-MCP. The official SDK is usually pulled in for a specific feature rather than as the primary framework — for example, a custom transport plug-in that the higher-level frameworks don’t support yet. If you’re unsure, start with FastMCP and drop down only when you hit something it abstracts away.

One more useful framing: think about whether your server is primarily tool-shaped, resource-shaped, orprompt-shaped. Most servers are tool-shaped (an agent calls actions), and that’s where FastMCP and FastAPI-MCP both shine. Resource-shaped servers expose read-only data (a documentation index, a CRM contacts view) — FastMCP’s @resource decorator handles this cleanly while FastAPI-MCP has more friction because resources don’t map naturally to HTTP routes. Prompt-shaped servers (parameterised system messages, templated chains-of-thought) are rare in the wild but when they show up, FastMCP’s @prompt is the cleanest API. So: tools mean any of the three; resources or prompts heavy mean FastMCP or the official SDK.

Common pitfalls

FastMCP — picking v1 by accident

FastMCP shipped a v1 that was bundled into the official SDK as mcp.server.fastmcp. The standalone v2 package at jlowin/fastmcp is a separate project under more active development. If you pip install fastmcp you get v2; if you import from mcp.server.fastmcp you get a frozen-ish v1 inside the SDK. Use v2 for new work unless you have a specific reason not to. Examples in old tutorials may import from either path — check the version before you copy.

FastAPI-MCP — missing operation IDs

Without explicit operation_id on each route, FastAPI generates IDs from the path and method — add_add_post instead of add. The model sees those as tool names and they’re harder to reason about. Either set operation_id on every route you bridge, or use the FastAPI-MCPdescribe_full_response_schema options to customise the tool name and description before mounting.

Python SDK — async everywhere

Every handler in the official SDK is async. If you have synchronous database calls, blocking HTTP libraries, or legacy code, you’ll trip the event loop and silently drop request throughput. Use anyio.to_thread.run_sync or replace blockers with async equivalents (httpx, asyncpg, aiosqlite). FastMCP papers over this with sync-callable sugar where possible, but the underlying primitive is still async — never block the loop in production.

Tool description bloat

All three frameworks pull tool descriptions from docstrings or response models. A FastAPI app with 40 routes becomes 40 tools, and every prompt the agent makes carries 40 tool descriptions in context. That’s a real cost — see our MCP context bloat deep dive. Trim descriptions, group related tools under one entry where possible, or use a tool-search layer to expose only relevant ones per turn.

FastMCP — over-strict schema inference

Type hints become JSON Schema constraints, which is usually what you want — but sometimes it produces a schema that’s tighter than your real contract. A parameter typed dict[str, int] generates a schema that rejects mixed-value dicts; a model might legitimately want to pass {"foo": 1, "bar": "auto"}. The escape hatch is to type the parameter as dict[str, Any] or Any and validate inside the function. Worth knowing before you spend an hour wondering why the agent’s calls keep getting rejected at the schema layer.

FastAPI-MCP — nested Pydantic models flatten poorly

A FastAPI endpoint that takes a deeply nested Pydantic model (Order containing list[LineItem] containing Product) generates a perfectly fine OpenAPI schema but a clunky MCP tool: the model sees one huge nested input object and struggles to construct it correctly turn after turn. The fix is either to flatten the model at the MCP boundary (one tool per logical operation rather than one tool per endpoint) or to add helper tools that take flat parameters and assemble the nested model internally. Deep nesting is the FastAPI-MCP sharp edge — most other things just work.

Python SDK — forgetting to await async tool handlers

The handler signatures in the official SDK are async def, and any function you call from them that returns a coroutine needs an explicit await. Miss one — a database query, an httpx call, a sleep — and Python doesn’t error. The coroutine object is silently returned as the result, the agent sees garbage, and the server runs fine. Catch this in CI with a linter that flags un-awaited coroutines (ruff’s RUF006, mypy with--strict), or in dev with a logging wrapper that asserts each return value isn’t a coroutine before sending it.

Community signal

The Python MCP framework space consolidated faster than most people expected. FastMCP went from a side project to the de-facto community standard inside a year — the v2 release cemented it. The official SDK keeps a steady cadence of spec-tracking releases but isn’t pitching itself as a production server framework; it’s the substrate. FastAPI-MCP has carved a strong niche with teams already running FastAPI in production — common in fintech, data platforms, and ML serving stacks — and the “wrap your existing API” pitch resonates clearly.

Three quotes worth pulling out, all from the official project READMEs (these are the load-bearing positioning statements each project chose for itself).

“FastMCP gives you everything you need to go from prototype to production.”

jlowin/fastmcp README

That tagline captures the practical pitch — not “we make MCP servers possible” (the official SDK does that) but “we make them shippable.” The README also leans hard on adoption numbers it claims for itself, which is honest framing for an open-source project that’s become the default.

“The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing.”

modelcontextprotocol/python-sdk README

Notable that the official SDK’s own README positions the FastMCP-style high-level API as the core interface and tucks the low-level Server class under “Advanced Usage.” The official team agrees with the community: most servers should be written at the FastMCP layer, and the raw spec API is for unusual cases. That endorsement is a real signal — it means using FastMCP isn’t a community fork, it’s the path the spec authors recommend.

“FastAPI-MCP is designed as a native extension of FastAPI, not just a converter that generates MCP tools from your API.”

tadata-org/fastapi_mcp README

This is the right framing for what FastAPI-MCP is. A naive converter would inspect routes and emit MCP tools as a side-deployment; FastAPI-MCP keeps everything inside the ASGI app so FastAPI’s dependency injection, auth, and middleware apply to MCP calls without translation. The README is explicit: “Native dependencies: Secure your MCP endpoints using familiar FastAPI Depends() for authentication and authorization” and “ASGI transport: Communicates directly with your FastAPI app using its ASGI interface, eliminating the need for HTTP calls.” If you have a working FastAPI auth story, that’s hours-not-days saved.

What you’ll see in the discourse beyond these official pitches: FastMCP examples dominate Python MCP tutorials, FastAPI-MCP shows up in migration write-ups (“here’s how we turned our existing service into an MCP server”), and the official SDK shows up in protocol-level discussions, transport implementations, and bug reports against the spec. None of the three is going away; pick by what you’re actually building, not by community size alone.

Frequently asked questions

What's the difference between FastMCP and the official Python SDK?

The official Python SDK (github.com/modelcontextprotocol/python-sdk) is the reference implementation of the MCP spec — verbose but precise, with explicit control over every protocol message. FastMCP (github.com/jlowin/fastmcp) is a higher-level decorator framework built on top of that SDK. You write `@tool`, `@resource`, and `@prompt` decorators instead of registering handlers manually. For 90% of servers, FastMCP cuts boilerplate by roughly 5x without giving up anything important. Pick the official SDK when you need protocol-level control — custom transports, unusual capabilities, or you're implementing the spec yourself. Pick FastMCP when you're shipping product.

Is FastAPI-MCP just a wrapper around FastMCP?

No — they solve different problems. FastAPI-MCP (github.com/tadata-org/fastapi-mcp) is a bridge: it inspects an existing FastAPI application and exposes its endpoints as MCP tools, no code rewrite needed. FastMCP is a framework for building MCP servers from scratch using decorators. If you already have a FastAPI app with 30 endpoints and you want to give an agent access to all of them, FastAPI-MCP gets you there in five minutes. If you're starting fresh and want clean MCP-native ergonomics, FastMCP is the better fit. They can coexist: a FastAPI app for HTTP clients, a FastMCP server next to it for tools the agent calls.

Which Python MCP framework has the smallest install footprint?

The official Python SDK has the lightest dependency tree — it's the reference implementation, so it only pulls what the spec strictly requires (pydantic, anyio, httpx for HTTP transport). FastMCP adds a thin layer on top with a handful of extra utilities for prompts and resources. FastAPI-MCP is the heaviest because it depends on FastAPI itself (Starlette, Pydantic v2, uvicorn) plus inspection utilities. For a serverless deployment where cold start matters, the official SDK or FastMCP both deploy in well under 50MB; FastAPI-MCP plus FastAPI is closer to 120MB. Use uv or pip with `--no-cache-dir` and pin versions in production.

Does FastMCP support both stdio and HTTP transports?

Yes — FastMCP v2 supports stdio (default, for desktop MCP clients like Claude Desktop and Cursor) and streamable HTTP (for remote deployments). You pick the transport when you call `mcp.run()`: `mcp.run()` defaults to stdio, `mcp.run(transport="streamable-http")` runs an HTTP server. The decorators you write don't change; the same tool functions work over both transports. The official Python SDK supports the same two transports but you wire them up more explicitly via `stdio_server()` or `streamable_http_app()` context managers. FastAPI-MCP runs as part of your FastAPI app, so it inherits FastAPI's HTTP transport by default.

Can I use Pydantic models for tool inputs in all three?

Yes, with different levels of plumbing. FastMCP and FastAPI-MCP both lean on Pydantic v2 hard — type-annotated function arguments become tool input schemas automatically via Pydantic's JSON Schema generator. The official Python SDK also supports Pydantic but you typically declare the input schema explicitly using `inputSchema` on the `Tool` object, with the Pydantic model used for runtime validation inside your handler. In practice, that means FastMCP and FastAPI-MCP feel identical for the common case (annotate the arg, get the schema for free), while the official SDK gives you an escape hatch when the model's auto-generated schema isn't quite right.

Which Python MCP framework should I pick for a serverless deployment?

FastMCP for serverless functions where the MCP server is the whole app — its `streamable_http_app()` returns an ASGI application that drops into AWS Lambda (via Mangum), Cloud Run, Fly Machines, or any container runtime. FastAPI-MCP if you're already deploying a FastAPI app and want to add MCP without standing up a second service. The official Python SDK works too but you'll write more glue. Watch cold starts: keep imports lean, use `--no-cache-dir` in your build, and prefer a single small container image over heavy multi-purpose ones. For high-traffic remote MCP, run it on a long-lived process (Cloud Run min-instances 1, ECS, or a VM) — cold-starting on every connection is painful.

Do these frameworks support MCP prompts and resources?

All three support the full MCP primitive set — tools, prompts, and resources — but they make it easier in different ways. FastMCP exposes `@prompt` and `@resource` decorators that mirror `@tool`, so you stay in one mental model. The official Python SDK has explicit `list_prompts()`, `get_prompt()`, `list_resources()`, `read_resource()` handlers you register on the server — more code, more control. FastAPI-MCP is the odd one out: its bread-and-butter is converting FastAPI endpoints to tools, and prompts/resources require dropping down to the underlying SDK or writing them as separate handlers. If your server is prompt- or resource-heavy, FastMCP or the official SDK are the cleaner choices.

How do I add authentication to a Python MCP server?

Depends on transport. For stdio servers (local MCP clients), authentication usually means reading environment variables at startup — there's no per-connection auth because the client is a child process you control. For remote HTTP servers, MCP 2025-06-18 standardised OAuth 2.1 and FastMCP v2 ships with first-class OAuth helpers. The official Python SDK also supports OAuth but you wire the authorisation server discovery, token validation, and PKCE flow yourself. FastAPI-MCP inherits FastAPI's middleware stack, so you can use any FastAPI auth plugin (fastapi-users, authlib, custom dependency-injection bearers) — that's a real edge if your team already has FastAPI auth dialled in. Full OAuth deep dive lives at /blog/oauth-21-for-remote-mcp-servers-streamable-http-explained-2026.

Should I use FastMCP if I'm building a one-off MCP server for personal use?

Yes — FastMCP's whole point is that the boilerplate-to-value ratio is best at the small end. A ten-line server that exposes one tool to Claude Desktop is the canonical FastMCP use case. You won't need OAuth (stdio servers don't need it), you won't need a custom transport, and the official SDK's explicit handler style buys you nothing here. The migration path is also forgiving: if your one-off grows into something serious, FastMCP scales to dozens of tools, prompts, and resources without changing the decorator API. The only reason to skip FastMCP for a personal server is if you specifically want to learn the wire protocol — in which case write it once on the official SDK to see the moving parts, then move to FastMCP for everything afterwards.

How do I publish my FastMCP server to PyPI?

Standard Python packaging. Add a pyproject.toml with your server module as the project, declare fastmcp as a dependency, and define a console-script entry point that calls mcp.run() so users can launch your server with a single command after pip install. The pattern most published FastMCP servers use is a `[project.scripts]` entry like `my-server = "my_server.__main__:main"`, where main() builds the FastMCP instance and calls run(). Once on PyPI, users install with `pip install my-server` or `uv add my-server` and reference the binary in their MCP client config — `command: my-server`, no Python invocation needed. For private distribution, the same shape works against a self-hosted index. Worth versioning against the MCP spec date you tested with in your README so users know the protocol version assumed.

Sources

FastMCP

FastAPI-MCP

Official Python SDK

Related

Keep reading