Skip to main content
If your server builds on the mcp package’s low-level Server class as SDK v2 rebuilt it — handlers passed to the constructor as on_list_tools, on_call_tool, and their siblings, each taking (ctx, params) and returning a wrapped result object — this guide is for you. FastMCP replaces that machinery with a declarative API where your functions are the protocol surface. The core idea: instead of describing your tools to the SDK 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 dispatch you wrote to route a call by name, and the schemas you wrote by hand to describe it, both disappear. Migrating from SDK v2 is the most direct of the four upgrade paths, because you and FastMCP already share a protocol layer. FastMCP 4 is built on SDK v2, so mcp_types imports keep working, field names are already snake_case, and the era negotiation you get is the one you have. Almost nothing about the wire changes — the one exception is argument strictness, covered below.
On SDK v1’s decorator-registered Server@server.list_tools(), @server.call_tool() — instead? See Upgrading from the Low-Level SDK v1, where the before-and-after code matches that API.Using SDK v2’s high-level MCPServer class? See Upgrading from MCP SDK v2 — that migration is mostly renaming.

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 4 depends on the MCP SDK v2 you are already using, so mcp_types stays importable and every protocol type keeps its current name and fields. Most of those imports vanish from your code anyway — FastMCP derives them — but the ones you keep need no changes.

Server and Transport

The Server class asks you to open a transport, connect its streams, build initialization options, and run an event loop. FastMCP collapses that into a constructor and a run() call.
Serving HTTP is the same shape. Where the low-level class hands you a Starlette app from server.streamable_http_app() and leaves the hosting to you, FastMCP runs it directly:
mcp.http_app() still returns a Starlette app when you need to mount the server inside a larger application.

Tools

This is where the difference is largest. SDK v2 requires two handlers — one describing your tools with hand-written JSON Schema, one dispatching calls by name — and both are passed to the constructor, so the connection between a tool’s declaration and its implementation lives only in your head. FastMCP derives both from the function.
Each @mcp.tool function is self-contained: its name becomes the tool name, its docstring becomes the description, its annotations become the JSON Schema, and its return value is serialized for you. The dispatch chain, the schema dicts, the CallToolResult wrapper, the TextContent wrapper, and the unknown-tool fallback all go away — a tool that doesn’t exist is now the framework’s problem, not a branch you maintain.

Type Mapping

Your hand-written input_schema becomes the function’s parameters: Constraints carry over too. A schema with "minimum" and "maximum" becomes a Pydantic Field, and a nested object schema becomes a Pydantic model or dataclass used as the annotation — FastMCP generates the same schema back out of it.

Return Values

The low-level class requires tools to return a CallToolResult wrapping a list of content blocks. FastMCP takes the value itself — strings, numbers, dicts, lists, dataclasses, Pydantic models — and handles both the content block and the structured output. For images and audio, FastMCP provides wrapper types that carry the format:
When you need full control over the wire result — multiple content blocks, or structured content that differs from the content blocks — return a ToolResult from fastmcp.tools instead.

Stricter Arguments

Deriving the schema from your signature also tightens what callers may send, and this is the one behavior change the migration introduces. Your on_call_tool handler reads params.arguments as a plain dict and never looks at keys it doesn’t need, so a call carrying an unexpected key succeeds. FastMCP declares "additionalProperties": false on the generated schema and enforces it, so the same call fails:
For most servers this is an improvement that costs nothing — a caller sending keys your handler never read was already a bug, and the hand-written schema never advertised that they were allowed. It matters if a client in your fleet attaches metadata alongside real arguments, since those calls start failing the moment you migrate. Accept them explicitly as optional parameters if you need to keep them working.

Resources

Resources take three handlers on the low-level class: one to list static resources, one to list URI templates, and one to read whichever URI arrives, with routing you write by hand. FastMCP replaces all three with a decorator per resource, and detects templates from the URI itself.
The URI does the routing. A {placeholder} in the URI makes the resource a template, and FastMCP matches the parameter to the function argument of the same name — so the uri.split("/")[2] parsing goes away along with the handler that held it. Return a str for text content and bytes for binary; FastMCP builds the TextResourceContents or BlobResourceContents wrapper. Templated resources also gain a protection the low-level version left to you: FastMCP screens extracted parameter values for path traversal, absolute paths, and null bytes before your function runs. See Path Security if a template legitimately accepts those values.

Prompts

The same collapse, one more time: on_list_prompts declares arguments as PromptArgument objects, on_get_prompt routes by name and assembles a GetPromptResult of PromptMessage objects. FastMCP takes a function whose parameters are the arguments.
Returning a str wraps it as a single user message. Whether an argument is required is read from the signature: code has no default, so it’s required; language defaults to None, so it isn’t. Multi-turn prompts return a list of Message objects, which take their text positionally and default to the user role:

Request Context

The low-level class hands each handler a ServerRequestContext carrying the raw ServerSession, and you reach through it to send notifications. FastMCP injects a typed Context into any function that declares one, and puts the operations you actually want on it directly.
The Context parameter is injected by type annotation and never appears in the tool’s schema, so clients see process_data as taking no arguments. Beyond logging and progress, it carries resource reads, session state, elicitation, and component visibility — see Context for the full surface. One thing to check as you migrate: ctx.session still exists on a FastMCP Context as an escape hatch, and it hands back the same raw SDK session your handlers use today. That makes it a working translation for anything with no Context equivalent — but it’s also the one part of your server that stays coupled to SDK internals, so reach for the Context method first and keep the escape hatch for what genuinely has no equivalent.

Complete Example

Everything above, applied at once:

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 dispatch chain splits into modules developed and tested independently. Middleware runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need — the cross-cutting concerns that, on the low-level class, 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 consolidates the SDK’s separate token verifier, authorization-server provider, and AuthSettings into a single auth= provider, with named providers for GitHub, Google, Auth0, Keycloak, 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.