Skip to main content
If you’ve been building MCP servers directly on the mcp package’s Server class — writing list_tools() and call_tool() handlers, hand-crafting JSON Schema dicts, and wiring up transport boilerplate — this guide is for you. FastMCP replaces all of that machinery with a declarative, Pythonic API where your functions are the protocol surface. The core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears.

The SDK v2 Transition

MCP SDK v2 is a substantial, deliberate modernization of the protocol layer. Protocol types moved into a standalone mcp_types package, wire fields moved from camelCase to snake_case, and the low-level Server was rebuilt so handlers are passed to the constructor as on_* callables taking (ctx, params) rather than registered with decorators. A v1 server meets that change the moment its environment resolves mcp to v2:
Often nobody chose that moment. An unpinned mcp dependency, a fresh lockfile, or a rebuilt container picks up the new major version. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to. Pinning the SDK back restores the decorator API immediately, with no code changes, and buys you time to choose deliberately:

Two Upgrade Paths

Both directions are reasonable, and the choice is about which code you’d rather maintain. Porting the low-level Server to SDK v2 keeps you in direct control of the protocol surface, which is the point of the low-level API and the right call for some servers. The work is real: your imports, every handler signature, every handler’s return type, and your error construction all move. Adopting FastMCP is what the rest of this page walks through. What makes it less work is not that FastMCP is better — it’s that the code most affected by the SDK v2 changes is precisely the code FastMCP doesn’t ask you to write. Your list_tools/call_tool pair, hand-written JSON Schema, and content-block wrappers aren’t ported to new signatures; they’re deleted, and FastMCP derives all of it from your function signatures instead. FastMCP 4 runs on MCP SDK v2 underneath, so both paths land you on the same modern protocol layer.
Already on SDK v2’s rebuilt Server class, with constructor-registered on_* handlers? See Upgrading from the Low-Level SDK v2 instead — the before-and-after code is different enough to warrant its own guide.Using FastMCP 1.0 via from mcp.server.fastmcp import FastMCP? Your upgrade is a single import — see Upgrading from MCP SDK v1.

Copy this prompt into any LLM along with your server code to get automated upgrade guidance.

Install

FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare pip install fastmcp or uv add fastmcp resolves to the latest stable release, which today is FastMCP 3:
An exact version pin installs even though it’s a prerelease — neither installer needs --pre or --prerelease allow for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see Install the v4 Prerelease. FastMCP depends on the mcp package, so the SDK stays installed. FastMCP 4 builds on SDK v2, where the protocol types live in a standalone mcp_types package that stays importable as mcp.types. Most of your mcp.types imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures.

Server and Transport

The Server class requires you to choose a transport, connect streams, build initialization options, and run an event loop. FastMCP collapses all of that into a constructor and a run() call.
Need HTTP instead of stdio? With the Server class, you’d wire up Starlette routes and SseServerTransport or StreamableHTTPSessionManager. With FastMCP:

Tools

This is where the difference is most dramatic. The Server class requires two handlers — one to describe your tools (with hand-written JSON Schema) and another to dispatch calls by name. FastMCP eliminates both by deriving everything from your function signature.
Each @mcp.tool function is self-contained: its name becomes the tool name, its docstring becomes the description, its type annotations become the JSON Schema, and its return value is serialized automatically. No routing. No schema dictionaries. No content-type wrappers.

Type Mapping

When converting your inputSchema to Python type hints:

Return Values

With the Server class, tools return list[types.TextContent | types.ImageContent | ...]. In FastMCP, return plain Python values — strings, numbers, dicts, lists, dataclasses, Pydantic models — and serialization is handled for you. For images or other non-text content, FastMCP provides helpers:

Resources

The Server class uses three handlers for resources: list_resources() to enumerate them, list_resource_templates() for URI templates, and read_resource() to serve content — all with manual routing by URI. FastMCP replaces all three with per-resource decorators.
Static resources and URI templates use the same @mcp.resource decorator — FastMCP detects {placeholders} in the URI and automatically registers a template. The function parameter user_id maps directly to the {user_id} placeholder.

Prompts

Same pattern: the Server class uses list_prompts() and get_prompt() with manual routing. FastMCP uses one decorator per prompt.
Returning a str from a prompt function automatically wraps it as a user message. For multi-turn prompts, return a list[Message]:

Request Context

The Server class exposes request context through server.request_context, which gives you the raw ServerSession for sending notifications. FastMCP replaces this with a typed Context object injected into any function that declares it.
The Context object provides logging (ctx.debug(), ctx.info(), ctx.warning(), ctx.error()), progress reporting (ctx.report_progress()), resource subscriptions, session state, and more. See Context for the full API.

Errors

Most of the errors a low-level server raises disappear with the dispatch that raised them: the ValueError(f"Unknown tool: {name}") fallback is unnecessary once FastMCP routes calls, and an exception from your function body is converted to a tool error for you. Deliberate protocol errors are the exception, and they need a small rewrite. The v1 pattern wrapped an ErrorData and passed it positionally; FastMCP’s McpError takes the fields directly:
An optional third argument, data=, carries the structured payload ErrorData used to hold. Catching is unchanged — except McpError still works, and err.error.code still reads the code — so only construction sites need touching.

Complete Example

A full server upgrade, showing how all the pieces fit together:

What You Gain

Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server. Server composition mounts one server inside another, so a surface that grew unwieldy as a single call_tool dispatch splits into modules developed and tested independently. Middleware runs across every request for logging, rate limiting, error handling, and caching — the cross-cutting concerns that, on the low-level Server, meant threading the same code through every handler. Proxy servers put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don’t control, and the OpenAPI integration generates an entire server from an API specification you already have. Authentication arrives as a single auth= provider covering token verification, OAuth, and named providers for GitHub, Google, Auth0, and others. The change most likely to affect your daily work is testing. FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up.