mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Split the SDK upgrade guides by SDK version (#4684)
This commit is contained in:
parent
d6b9daecb1
commit
0175bc9235
15 changed files with 2170 additions and 322 deletions
|
|
@ -358,10 +358,12 @@
|
|||
"group": "Upgrading",
|
||||
"icon": "up",
|
||||
"pages": [
|
||||
"getting-started/upgrading/from-fastmcp-2",
|
||||
"getting-started/upgrading/from-fastmcp-3",
|
||||
"getting-started/upgrading/from-mcp-sdk",
|
||||
"getting-started/upgrading/from-low-level-sdk"
|
||||
"getting-started/upgrading/from-fastmcp-2",
|
||||
"getting-started/upgrading/from-mcp-sdk-v1",
|
||||
"getting-started/upgrading/from-mcp-sdk-v2",
|
||||
"getting-started/upgrading/from-low-level-sdk-v1",
|
||||
"getting-started/upgrading/from-low-level-sdk-v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -509,13 +511,21 @@
|
|||
"source": "/development/upgrade-guide"
|
||||
},
|
||||
{
|
||||
"destination": "/getting-started/upgrading/from-mcp-sdk",
|
||||
"destination": "/getting-started/upgrading/from-mcp-sdk-v1",
|
||||
"source": "/getting-started/upgrading-from-sdk"
|
||||
},
|
||||
{
|
||||
"destination": "/getting-started/upgrading/from-low-level-sdk",
|
||||
"destination": "/getting-started/upgrading/from-mcp-sdk-v1",
|
||||
"source": "/getting-started/upgrading/from-mcp-sdk"
|
||||
},
|
||||
{
|
||||
"destination": "/getting-started/upgrading/from-low-level-sdk-v1",
|
||||
"source": "/getting-started/low-level-sdk"
|
||||
},
|
||||
{
|
||||
"destination": "/getting-started/upgrading/from-low-level-sdk-v1",
|
||||
"source": "/getting-started/upgrading/from-low-level-sdk"
|
||||
},
|
||||
{
|
||||
"destination": "/getting-started/upgrading/from-fastmcp-3",
|
||||
"source": "/getting-started/upgrading/to-mcp-sdk-v2"
|
||||
|
|
|
|||
|
|
@ -68,13 +68,17 @@ See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complet
|
|||
|
||||
### From the MCP SDK
|
||||
|
||||
#### From FastMCP 1.0
|
||||
Which guide you want depends on which `mcp` version you're on and which of its two server APIs you used.
|
||||
|
||||
If you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details.
|
||||
#### From the high-level server
|
||||
|
||||
#### From the Low-Level Server API
|
||||
If you're using FastMCP 1.0 via SDK v1 (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers it's a single import change. See [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1), which also explains why that route is usually easier than moving to MCP SDK v2.
|
||||
|
||||
If you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough.
|
||||
If you already moved to SDK v2 and write against `MCPServer`, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming.
|
||||
|
||||
#### From the low-level server
|
||||
|
||||
If you built your server directly on the `mcp` package's `Server` class, the guide you want depends on how its handlers are registered. Decorators like `@server.list_tools()` mean SDK v1 — see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1). Handlers passed to the constructor as `on_list_tools=` mean SDK v2 — see [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
---
|
||||
title: Upgrading from FastMCP 2
|
||||
sidebarTitle: "From FastMCP 2"
|
||||
description: Migration instructions for upgrading between FastMCP versions
|
||||
description: What changed in FastMCP 3 for servers written against FastMCP 2
|
||||
icon: up
|
||||
---
|
||||
|
||||
This guide covers breaking changes and migration steps when upgrading FastMCP.
|
||||
This guide covers the breaking changes a FastMCP 2 server meets on its way to FastMCP 3, newest release first.
|
||||
|
||||
<Note>
|
||||
**Going all the way to FastMCP 4?** You need this page and [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), in that order. The two describe different transitions: this one covers the v3 API changes, while the FastMCP 3 guide covers the MCP Python SDK v2 rebuild underneath v4. Where a v3 deprecation was later removed outright, this page marks it **Removed in v4**.
|
||||
</Note>
|
||||
|
||||
## v3.0.0
|
||||
|
||||
|
|
@ -101,7 +105,7 @@ For each issue found, show the original line, explain why it breaks, and provide
|
|||
|
||||
In v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint.
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
mcp = FastMCP("server", host="0.0.0.0", port=8080)
|
||||
mcp.run()
|
||||
|
|
@ -140,7 +144,7 @@ Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-in
|
|||
|
||||
In v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
tool = await server.get_tool("my_tool")
|
||||
tool.disable()
|
||||
|
|
@ -155,7 +159,7 @@ Calling `.enable()` or `.disable()` on a component object now raises `NotImpleme
|
|||
|
||||
The `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
tools = await server.get_tools()
|
||||
tool = tools["my_tool"]
|
||||
|
|
@ -169,7 +173,7 @@ tool = next((t for t in tools if t.name == "my_tool"), None)
|
|||
|
||||
Prompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role="user"`, so most prompts become one-liners:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
from mcp.types import PromptMessage, TextContent
|
||||
|
||||
|
|
@ -187,7 +191,7 @@ def my_prompt() -> Message:
|
|||
|
||||
If your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages):
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before (v2 accepted this)
|
||||
@mcp.prompt
|
||||
def my_prompt():
|
||||
|
|
@ -211,7 +215,7 @@ def my_prompt() -> list[Message]:
|
|||
|
||||
`ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
ctx.set_state("key", "value")
|
||||
value = ctx.get_state("key")
|
||||
|
|
@ -223,7 +227,7 @@ value = await ctx.get_state("key")
|
|||
|
||||
State values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
await ctx.set_state("client", my_http_client, serializable=False)
|
||||
```
|
||||
|
||||
|
|
@ -245,7 +249,7 @@ parent.mount(child, namespace="child")
|
|||
|
||||
In v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before (v2) — client_id and client_secret loaded automatically
|
||||
# from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.
|
||||
auth = GitHubProvider()
|
||||
|
|
@ -278,7 +282,7 @@ transport = StreamableHttpTransport("http://localhost:8000/mcp")
|
|||
|
||||
`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx2 client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
provider = OpenAPIProvider(spec, client, timeout=60)
|
||||
|
||||
|
|
@ -291,7 +295,7 @@ provider = OpenAPIProvider(spec, client)
|
|||
|
||||
The FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
tags = tool.meta.get("_fastmcp", {}).get("tags", [])
|
||||
|
||||
|
|
@ -309,7 +313,7 @@ Metadata is now always included — the `include_fastmcp_meta` parameter has bee
|
|||
|
||||
In v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
|
@ -335,7 +339,7 @@ These were deprecated in v3. Items marked **Removed in v4** no longer work at al
|
|||
|
||||
**mount() prefix → namespace** (Removed in v4)
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Removed in v4
|
||||
main.mount(subserver, prefix="api")
|
||||
|
||||
|
|
@ -345,7 +349,7 @@ main.mount(subserver, namespace="api")
|
|||
|
||||
**import_server() → mount()** (Removed in v4)
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Removed in v4
|
||||
main.import_server(subserver)
|
||||
|
||||
|
|
@ -382,7 +386,7 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
|
|||
|
||||
**add_tool_transformation() → add_transform()** (Removed in v4)
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Removed in v4
|
||||
mcp.add_tool_transformation("name", config)
|
||||
|
||||
|
|
@ -395,7 +399,7 @@ mcp.add_transform(ToolTransform({"name": config}))
|
|||
|
||||
The proxy target is passed positionally in both APIs, so most calls migrate unchanged. If you passed the target by keyword, note that the parameter was renamed from `backend=` to `target=`.
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Removed in v4
|
||||
proxy = FastMCP.as_proxy("http://example.com/mcp")
|
||||
proxy = FastMCP.as_proxy(backend="http://example.com/mcp") # keyword form
|
||||
|
|
@ -424,12 +428,18 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
|
|||
|
||||
### Removed Deprecated Features
|
||||
|
||||
- `BearerAuthProvider` → use `JWTVerifier`
|
||||
- `Context.get_http_request()` → use `get_http_request()` from dependencies
|
||||
- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image`
|
||||
- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration
|
||||
- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...`
|
||||
- `output_schema=False` → use `output_schema=None`
|
||||
A batch of long-deprecated surfaces came out in 2.14. Each fails loudly at import or call time, and each has a direct replacement:
|
||||
|
||||
| Removed | Replacement |
|
||||
|---|---|
|
||||
| `BearerAuthProvider` | `JWTVerifier` — the same JWT validation under a name that says what it does |
|
||||
| `Context.get_http_request()` | `get_http_request()` from [dependency injection](/servers/dependency-injection) |
|
||||
| `from fastmcp import Image` | `from fastmcp.utilities.types import Image` |
|
||||
| `FastMCP(dependencies=[...])` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
|
||||
| `FastMCPProxy(client=...)` | `client_factory=lambda: ...` |
|
||||
| `output_schema=False` | `output_schema=None` |
|
||||
|
||||
Two of these are worth understanding rather than just swapping. `FastMCPProxy` takes a factory instead of a client because a single shared client cannot serve concurrent proxied sessions safely — the factory gives each session its own backend connection. And `output_schema=False` became `output_schema=None` because `False` read as "this tool has a schema, and it is false"; `None` says plainly that there is no schema.
|
||||
|
||||
## v2.13.0
|
||||
|
||||
|
|
@ -437,7 +447,7 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
|
|||
|
||||
The OAuth proxy now issues its own JWT tokens. For production, provide explicit keys:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
auth = GitHubProvider(
|
||||
client_id=os.environ["GITHUB_CLIENT_ID"],
|
||||
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
---
|
||||
title: Upgrading from FastMCP 3
|
||||
sidebarTitle: "From FastMCP 3.x"
|
||||
sidebarTitle: "From FastMCP 3"
|
||||
description: What changes when you upgrade to FastMCP 4, which builds on the MCP Python SDK v2
|
||||
icon: up
|
||||
---
|
||||
|
||||
FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it splits the protocol types out of `mcp.types` into a standalone `mcp_types` package, and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on).
|
||||
|
||||
FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. The sections below describe what FastMCP handles for you, the small number of changes you must make in your own code, and the deprecation timeline for the compatibility shims.
|
||||
FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. What the SDK cannot hide is the protocol's own direction: the new sessionless era removes the server's ability to call back into a client mid-request, and background tasks moved out of the core spec into an extension. Those two shape the changes a working server is most likely to feel.
|
||||
|
||||
## Install the v4 prerelease
|
||||
The sections below cover what FastMCP handles for you, the changes you must make in your own code, the surfaces removed outright in 4.0, the behavior shifts that compile fine but act differently, and the deprecation timeline for the compatibility shims.
|
||||
|
||||
## Install the v4 Prerelease
|
||||
|
||||
While FastMCP 4 is in prerelease, pin the beta and its prerelease protocol dependencies explicitly. For a uv project, add the following to `pyproject.toml`:
|
||||
|
||||
|
|
@ -27,26 +29,92 @@ constraint-dependencies = [
|
|||
|
||||
Then run `uv lock` or `uv sync` normally. The constraints opt only these transitive packages into their prerelease versions; you do not need `--prerelease allow`, which permits prereleases throughout the dependency graph.
|
||||
|
||||
## Environment requirements
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server or client from FastMCP 3.x to FastMCP 4, which is built on the MCP Python SDK v2.
|
||||
|
||||
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
|
||||
|
||||
Then search the provided code for each signal below. Most FastMCP 3 servers upgrade untouched, so report only what you actually find.
|
||||
|
||||
ENVIRONMENT
|
||||
- a pydantic pin below 2.12
|
||||
- a FastAPI pin below 0.133.0, the first release admitting Starlette 1.x (earlier ones cap it, e.g. 0.115.12 requires `starlette<0.47.0`), or any direct Starlette pin below 1.0.1
|
||||
|
||||
IMPORTS THAT NO LONGER RESOLVE
|
||||
- `mcp.types` (anywhere, in any form)
|
||||
- `fastmcp.server.proxy`, `fastmcp.server.openapi`, `FastMCPOpenAPI`
|
||||
- `fastmcp.experimental.server.openapi`, `fastmcp.experimental.utilities.openapi`
|
||||
- `fastmcp.experimental.sampling.handlers`
|
||||
- `fastmcp.server.apps`, `fastmcp.server.app`
|
||||
- `fastmcp.tools.tool`, `fastmcp.resources.resource`, `fastmcp.prompts.prompt`
|
||||
- `fastmcp.server.tasks`, `fastmcp.server.sampling`
|
||||
- `fastmcp.server.auth.authorization`
|
||||
- `CurrentDocket` or `CurrentWorker` from `fastmcp.dependencies`
|
||||
- `SkillsProvider`
|
||||
- `CachableToolResult`, `CachablePromptResult`, and their siblings (the misspelling was corrected with no alias)
|
||||
- `PromptToolMiddleware`, `ResourceToolMiddleware`
|
||||
|
||||
REMOVED SERVER METHODS AND KEYWORDS
|
||||
- `FastMCP.as_proxy(...)`
|
||||
- `import_server(...)` ← flag this one loudly: `mount()` is the replacement but NOT an equivalent. `import_server` took a static snapshot and skipped the child's lifespan and middleware; `mount` is a live composition that runs both.
|
||||
- `mount(prefix=...)`, `mount(as_proxy=...)`
|
||||
- `add_tool_transformation(...)`, `remove_tool_transformation(...)`
|
||||
- `remove_tool(...)` ← its replacement raises KeyError where this raised NotFoundError, so check surrounding except clauses
|
||||
- tool `serializer=`, tool `exclude_args=`
|
||||
- `StreamableHttpTransport(sse_read_timeout=...)`
|
||||
- `FASTMCP_DECORATOR_MODE` / `settings.decorator_mode`
|
||||
- `FastMCP(sampling_handler=...)`, `sampling_handler_behavior=`
|
||||
|
||||
REMOVED CONTEXT METHODS
|
||||
- `ctx.sample(...)`, `ctx.sample_step(...)`, `ctx.list_roots(...)`
|
||||
- Note for the user: if borrowing the CALLER's model is the whole point of the server, the guide's recommendation is to stay on FastMCP 3.x rather than migrate.
|
||||
- The client side is NOT affected — `Client(sampling_handler=...)` and `Client(roots=...)` still mean what they meant.
|
||||
|
||||
RUNTIME BREAKS THAT STILL COMPILE — the ones most likely to reach production
|
||||
- `ctx.elicit(...)` anywhere. It is era-gated in 4.0 and raises on modern connections, which is what `Client` now negotiates by default. This is the single most likely runtime failure.
|
||||
- `ctx.elicit(...)` called without `response_type`
|
||||
- `except httpx.` around any FastMCP call. FastMCP raises httpx2 exceptions now, but httpx is usually still installed transitively, so the handler imports, type-checks, and silently never matches.
|
||||
- a custom `httpx.AsyncClient`, `httpx_client_factory=`, or `httpx.Auth` handed to a FastMCP transport, `OAuth`, or `from_openapi`
|
||||
- `Middleware.on_initialize` hooks, and `ctx.set_state` values read back in a later call — neither survives a modern connection
|
||||
- middleware assuming `on_message` only sees routable requests
|
||||
- camelCase field reads (`inputSchema`, `isError`, `mimeType`, `nextCursor`, `structuredContent`, `serverInfo`, and the rest) — these still work but warn, and are scheduled for removal
|
||||
- clients matching on the resource-not-found error code -32002
|
||||
- templated resources whose parameters legitimately carry `..` or absolute paths
|
||||
- an OAuth server (`OAuthProxy` or anything built on it) with `issuer_url` set to something other than `base_url` — this forces a one-time re-authorization of every client
|
||||
|
||||
BACKGROUND TASKS
|
||||
- `@mcp.tool(task=True)` or `TaskConfig` without `mcp.add_extension(TasksExtension())`
|
||||
- `task=` on a `@mcp.resource` or `@mcp.prompt` decorator (tools only now)
|
||||
- `client.call_tool(..., task=True)`, `read_resource(task=True)`, `get_prompt(task=True)`
|
||||
|
||||
ERRORS
|
||||
- `McpError(ErrorData(...))` positional construction. Catching and `err.error.code` are unchanged; only construction moved.
|
||||
|
||||
For each item found, show the original line, name what changed, and give the corrected code from the guide. Where you could not confirm a replacement in the docs, say so instead of guessing.
|
||||
</Prompt>
|
||||
|
||||
## Environment Requirements
|
||||
|
||||
The SDK v2 raises FastMCP's dependency floors, which matters before any of your code runs.
|
||||
|
||||
**pydantic >= 2.12 is now the floor.** If your project pins an older pydantic (for example `pydantic==2.11.*`), installing this FastMCP release fails with an unsatisfiable-resolution error from your installer — bump your pin to `>=2.12` first. If you don't pin pydantic at all, installers upgrade it silently as part of the FastMCP upgrade.
|
||||
|
||||
**The server extra floors Starlette >= 1.0.1.** Modern FastAPI (0.11x and later) already runs on Starlette 1.x, so mounting a FastMCP server inside a FastAPI app coexists cleanly — verified with FastAPI 0.138.2. Only very old FastAPI versions pinned below Starlette 1.0.1 conflict; upgrade FastAPI if your resolver complains about Starlette.
|
||||
**The server extra floors Starlette >= 1.0.1.** This is the requirement most likely to force an unrelated upgrade, because FastAPI pinned Starlette to a sub-1.0 range for a long time — FastAPI 0.115.12, for example, requires `starlette<0.47.0`. **FastAPI 0.133.0 is the first release that admits Starlette 1.x**, so a project pinned below that gets an unsatisfiable resolution rather than a version bump. Raise your FastAPI pin to `>=0.133.0` before upgrading FastMCP. Mounting a FastMCP server inside a FastAPI app is otherwise unaffected — verified against FastAPI 0.135.2 on Starlette 1.3.1.
|
||||
|
||||
## What FastMCP absorbs
|
||||
## What FastMCP Absorbs
|
||||
|
||||
### Legacy camelCase field access keeps working
|
||||
### camelCase Field Access
|
||||
|
||||
Objects that FastMCP hands back to you — the results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to your sampling and elicitation handlers — are SDK v2 objects with snake_case fields. FastMCP installs a compatibility bridge at import time that routes the old camelCase names to their new snake_case fields, so code written against FastMCP 2.x still reads correctly:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
async with Client("my_mcp_server.py") as client:
|
||||
tools = await client.list_tools()
|
||||
schema = tools[0].inputSchema # still works, warns once
|
||||
|
||||
async def read_schema():
|
||||
async with Client("my_mcp_server.py") as client:
|
||||
tools = await client.list_tools()
|
||||
return tools[0].inputSchema # still works, warns once
|
||||
```
|
||||
|
||||
Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools; `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` on tool annotations; `mimeType` on resources and content; `isError`/`structuredContent` on tool results; `nextCursor` on paginated results; `serverInfo`/`protocolVersion` on the initialize result; the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`); and `requestedSchema` on elicitation parameters.
|
||||
|
|
@ -61,7 +129,7 @@ fastmcp.settings.mcp_camelcase_compat = False
|
|||
|
||||
See [Settings](/more/settings) for the full reference.
|
||||
|
||||
### Protocol types moved to `mcp_types`
|
||||
### Protocol Types
|
||||
|
||||
The `mcp.types` module no longer exists. Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in the standalone `mcp_types` package. Update your imports to point there:
|
||||
|
||||
|
|
@ -71,7 +139,7 @@ from mcp_types import TextContent, Tool, ToolAnnotations
|
|||
|
||||
`fastmcp.types` still exists, but holds only types FastMCP defines itself (currently just `Textarea`, used to render a multiline textarea in form-based UIs) — it does not re-export protocol types.
|
||||
|
||||
### `McpError` has an alias
|
||||
### The `McpError` Alias
|
||||
|
||||
`fastmcp.exceptions.McpError` is an alias of the SDK's `MCPError`. Catching errors is unchanged — `except McpError` still catches SDK-raised errors, and reading `err.error.code` still works:
|
||||
|
||||
|
|
@ -84,7 +152,7 @@ except McpError as err:
|
|||
print(err.error.code)
|
||||
```
|
||||
|
||||
### Behavior preserved across the SDK boundary
|
||||
### Preserved Behavior
|
||||
|
||||
A few client behaviors that touch the SDK are preserved so you don't have to change anything:
|
||||
|
||||
|
|
@ -92,7 +160,7 @@ A few client behaviors that touch the SDK are preserved so you don't have to cha
|
|||
- `client.ping()` returns a `bool`.
|
||||
- `client.transport.get_session_id()` returns `None` on protocol eras that have no session, rather than raising. (The SDK v2 removed session-id access from its streamable HTTP transport; FastMCP reconstructs it on the transport object.)
|
||||
|
||||
## What you must change
|
||||
## What You Must Change
|
||||
|
||||
Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — your imports, how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix.
|
||||
|
||||
|
|
@ -112,7 +180,7 @@ TypeError: MCPError.__init__() missing 1 required positional argument: 'message'
|
|||
|
||||
Note the message prints the class as `MCPError` (uppercase) even though your code wrote `McpError` — the old name is an alias for the SDK's renamed class. Construct the error with keyword arguments instead:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
# Before (raises TypeError under SDK v2):
|
||||
|
|
@ -128,7 +196,7 @@ Catching and `err.error.code` are unchanged — only construction moved.
|
|||
|
||||
**FastMCP now uses httpx2 exclusively.** FastMCP has replaced `httpx` with [httpx2](https://pypi.org/project/httpx2/), a next-generation httpx fork, across its entire HTTP stack — client transports and every server-side path (auth providers, the OpenAPI integration, the version check). `httpx` is no longer a FastMCP dependency. If you pass a custom client or factory into a FastMCP client transport — `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, `OAuth(httpx_client_factory=...)`, or a custom `httpx.Auth` as `Client(auth=...)` — those objects must now be httpx2. httpx2 is a drop-in fork with the same public API, so the change is an import swap:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
import httpx
|
||||
|
||||
|
|
@ -153,10 +221,12 @@ The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvide
|
|||
```python
|
||||
import httpx # still installed transitively — this import works
|
||||
|
||||
try:
|
||||
result = await client.call_tool("fetch", {"url": url})
|
||||
except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError
|
||||
return fallback()
|
||||
|
||||
async def fetch(client, url):
|
||||
try:
|
||||
return await client.call_tool("fetch", {"url": url})
|
||||
except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError
|
||||
return fallback()
|
||||
```
|
||||
|
||||
Grep your codebase for `except httpx.` and move those handlers to `httpx2`. The exception hierarchies match name-for-name, so the fix is an import swap — the hard part is remembering to look. One place you are covered automatically: exceptions raised *inside your tools and resources* (for example, a tool whose own old-httpx call gets a 429) are still mapped to `ToolError`/`ResourceError` by FastMCP's error boundary, which recognizes both libraries' exceptions during the transition.
|
||||
|
|
@ -167,7 +237,7 @@ Two runtime behaviors shift with httpx2, and because the switch is now wholesale
|
|||
|
||||
Deprecations that warned throughout the 3.x line are removed in 4.0. Unlike the bridged changes above, these fail immediately at the call site — a `ModuleNotFoundError`, `ImportError`, `AttributeError`, or `TypeError` — so nothing degrades silently. Every one has a direct replacement, and the fix is mechanical.
|
||||
|
||||
### Moved imports
|
||||
### Moved Imports
|
||||
|
||||
The proxy, OpenAPI, and app integrations moved to their permanent homes, and the internal component classes are no longer re-exported from their old aliases:
|
||||
|
||||
|
|
@ -188,10 +258,13 @@ The proxy, OpenAPI, and app integrations moved to their permanent homes, and the
|
|||
| `AuthCheck` / `AuthContext` / `require_scopes` / `require_roles` / `restrict_tag` / `run_auth_checks` from `fastmcp.server.auth.authorization` | `fastmcp.server.auth` |
|
||||
| `run_auth_checks_with_shortfall` / `scope_requirements` from `fastmcp.server.auth.authorization` | `fastmcp.utilities.authorization` |
|
||||
| `SkillsProvider` | `SkillsDirectoryProvider` from `fastmcp.server.providers.skills` |
|
||||
| `TaskConfig` from `fastmcp.server.tasks` | `fastmcp.utilities.tasks` |
|
||||
| `CurrentDocket` / `CurrentWorker` from `fastmcp.dependencies` | `fastmcp_tasks.dependencies` |
|
||||
| `fastmcp.server.sampling` (and `SamplingTool`) | removed with [server-side sampling](#protocol-version-support) |
|
||||
|
||||
Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained).
|
||||
|
||||
### Removed server methods and `mount()` keywords
|
||||
### Removed Server Methods
|
||||
|
||||
These `FastMCP` methods and keywords have warned since 3.0 and are now removed:
|
||||
|
||||
|
|
@ -211,7 +284,7 @@ Two of these replacements are not exact behavioral swaps. `create_proxy` takes i
|
|||
|
||||
`import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers.
|
||||
|
||||
### Removed parameters and settings
|
||||
### Removed Parameters
|
||||
|
||||
Several parameters and settings that warned in 3.x are gone:
|
||||
|
||||
|
|
@ -221,7 +294,7 @@ Several parameters and settings that warned in 3.x are gone:
|
|||
- **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.)
|
||||
- **`ctx.elicit()` now requires `response_type`.** Omitting it (or passing `None`) has warned since 3.2 and now raises `TypeError`. The empty-object schema it produced gave clients nothing to render, and some showed an empty, non-functional form. Pass a type describing what you expect back — `bool` is the right answer for a confirmation:
|
||||
|
||||
```python
|
||||
```python test="skip"
|
||||
# Before
|
||||
result = await ctx.elicit("Approve this action?")
|
||||
|
||||
|
|
@ -231,9 +304,91 @@ Several parameters and settings that warned in 3.x are gone:
|
|||
|
||||
This is the server-authoring API only. Client elicitation handlers still receive `response_type=None` for URL requests and for empty schemas sent by other servers — that contract is unchanged.
|
||||
|
||||
## Behavior changes to verify
|
||||
### Background Tasks
|
||||
|
||||
Three server-side behaviors changed in ways that compile fine but can surface at runtime.
|
||||
Background tasks left the core MCP spec during the SDK v2 rebuild and came back as the `io.modelcontextprotocol/tasks` extension (SEP-2663). FastMCP follows the protocol: what was a built-in server feature in 3.x is now a registered extension, and the authoring surface changed on both sides of the connection.
|
||||
|
||||
The extension ships in a separate package, so the pin from [Install the v4 Prerelease](#install-the-v4-prerelease) needs one more entry before any of this imports:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = ["fastmcp[tasks]==4.0.0b1"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = [
|
||||
"fastmcp-slim==4.0.0b1",
|
||||
"fastmcp-tasks==4.0.0b1",
|
||||
"mcp==2.0.0b2",
|
||||
"mcp-types==2.0.0b2",
|
||||
]
|
||||
```
|
||||
|
||||
On the server, `task=True` still marks a tool as capable of running in the background, but it no longer runs anything by itself — the extension does. Register it, or the server refuses to start:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp_tasks import TasksExtension
|
||||
|
||||
mcp = FastMCP("MyServer")
|
||||
mcp.add_extension(TasksExtension())
|
||||
|
||||
@mcp.tool(task=True)
|
||||
async def slow_computation(duration: int) -> str:
|
||||
"""A long-running operation."""
|
||||
return "done"
|
||||
```
|
||||
|
||||
Without the registration, a `task=True` tool raises at startup rather than the first time a client calls the tool:
|
||||
|
||||
```
|
||||
RuntimeError: Task-enabled tools (slow_computation) require the tasks extension,
|
||||
but no extension with identifier 'io.modelcontextprotocol/tasks' is registered.
|
||||
```
|
||||
|
||||
`TaskConfig` moved from `fastmcp.server.tasks` to `fastmcp.utilities.tasks`, and the `CurrentDocket` and `CurrentWorker` dependencies moved to `fastmcp_tasks.dependencies`.
|
||||
|
||||
`task=` is now a tool-only keyword. FastMCP 3 accepted it on resource, resource-template, and prompt decorators as well; passing it to `@mcp.resource` or `@mcp.prompt` now raises `TypeError`, and there is no replacement — the extension tasks tool calls only.
|
||||
|
||||
The client API changed shape entirely. In 3.x you opted a single call into background execution with `task=True` and got a handle back. In 4.0 `call_tool` handles a tasked call transparently: if the server runs the call in the background, the client polls it to completion and returns the same result a synchronous call would have produced.
|
||||
|
||||
```python
|
||||
import fastmcp_tasks # noqa: F401 — importing anywhere enables client task support
|
||||
from fastmcp import Client
|
||||
|
||||
|
||||
async def run(server):
|
||||
async with Client(server) as client:
|
||||
return await client.call_tool("slow_computation", {"duration": 10})
|
||||
```
|
||||
|
||||
When you want the handle — to do other work while the task runs, check on it, or cancel it — `call_tool_task` returns one immediately:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp_tasks import call_tool_task
|
||||
|
||||
|
||||
async def run(server):
|
||||
async with Client(server) as client:
|
||||
task = await call_tool_task(client, "slow_computation", {"duration": 10})
|
||||
return await task.result()
|
||||
```
|
||||
|
||||
Three things follow from this. `client.call_tool(name, args, task=True)` raises `TypeError`, as do `read_resource(task=True)` and `get_prompt(task=True)` — and those last two have no replacement. Client task support requires `fastmcp_tasks` to be imported somewhere in the process, since that import is what makes a `Client` advertise the capability. And tasks are negotiated only on modern connections, so a `mode="legacy"` client never gets them. See [Background Tasks](/servers/tasks) for the full picture.
|
||||
|
||||
## Behavior Changes
|
||||
|
||||
These changes compile fine and can surface at runtime. The first is the one most likely to bite a working 3.x server.
|
||||
|
||||
**`ctx.elicit()` no longer reaches a default client.** Elicitation is era-gated in 4.0: `ctx.elicit()` works on handshake-era connections (≤ 2025-11-25) and raises on the modern `2026-07-28` protocol, which has no back-channel for a running tool to push a request down. Because `fastmcp.Client` now defaults to `mode="auto"`, an ordinary client negotiates the modern era against a FastMCP server — so a tool that elicited happily in 3.x now fails the call:
|
||||
|
||||
```
|
||||
ToolError: elicitation via server-initiated requests is unavailable on 2026-07-28 connections.
|
||||
```
|
||||
|
||||
The gate is strict in both directions, which is what makes it debuggable: a guard tool that returns an input request on a handshake connection raises the mirror-image error rather than misbehaving quietly. You have three ways forward. Rewrite the tool as a guard tool that *returns* a description of the input it needs, which is the form that works on modern connections. Branch on `ctx.request_context.protocol_version` and keep both paths if you serve both eras. Or keep this server's clients on the handshake era with `Client(server, mode="legacy")`, which leaves `ctx.elicit()` working as written. See [Elicitation](/servers/elicitation#which-approach-to-use) for the two shapes side by side.
|
||||
|
||||
**Middleware sees traffic it never saw before.** Dispatch now begins in the SDK's middleware layer, the single point every inbound message passes through, so `on_message`, `on_request`, and `on_notification` observe *every* message a client sends — including `notifications/cancelled`, `notifications/initialized`, and `notifications/progress`, and including requests that fail before reaching a handler, such as an unknown method or a `tools/call` whose params fail validation. In 3.x those never reached your hooks. Middleware that assumed every message it saw was a routable request, or that counted messages to measure tool traffic, needs a guard on the message type. The operation hooks (`on_call_tool`, `on_list_tools`, and the rest) are unaffected: they still fire exactly once per request and still receive typed component results. See [What middleware sees](/servers/middleware#what-middleware-sees).
|
||||
|
||||
**Templated resources are path-screened by default.** Every templated resource now has its extracted parameter values checked for path-traversal (`..` segments), absolute paths, and null bytes *before your handler runs*, at the server's read chokepoint. A rejected read returns a non-leaky "resource not found" error. Only a standalone `..` segment counts as traversal, so values that merely contain dots (`file.tar.gz`, `HEAD~3..HEAD`) and dotfiles (`.env`) still pass. If a template legitimately accepts `..`-bearing or absolute values, exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable the check per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
|
||||
|
||||
|
|
@ -245,11 +400,11 @@ The cost of the correction is the `iss` on tokens already in the wild, so it fal
|
|||
|
||||
Servers that leave `issuer_url` unset, or set it to the same value as `base_url`, are unaffected. It defaults to `base_url`, and the metadata and minted `iss` are byte-identical to what 3.x produced.
|
||||
|
||||
## Deprecation timeline
|
||||
## Deprecation Timeline
|
||||
|
||||
The camelCase bridge is a migration aid, not a permanent fixture. It works today and warns on every bridged read so you can find and update the affected call sites. Plan to migrate your reads to snake_case: the shims will be removed in a future release, after which only the snake_case names resolve — the same state you get today by setting `mcp_camelcase_compat = False`. Turning the setting off is a good way to surface every remaining camelCase read in your code as a hard `AttributeError` before the shims go away.
|
||||
|
||||
## SDK deprecation warnings you may see
|
||||
## SDK Deprecation Warnings
|
||||
|
||||
Ordinary use of `ctx.info` (client logging) emits an SDK-level `MCPDeprecationWarning`:
|
||||
|
||||
|
|
@ -259,7 +414,7 @@ The logging capability is deprecated as of 2026-07-28 (SEP-2577)
|
|||
|
||||
The warning comes from the MCP SDK, not from FastMCP, and it is benign. `ctx.info` and the rest of the logging methods keep working on every era, including the modern one — a log message is a *notification*, which rides the response stream the caller already opened. The SDK is signaling the protocol's direction for the capability declaration, not the notification itself.
|
||||
|
||||
## Protocol version support
|
||||
## Protocol Version Support
|
||||
|
||||
FastMCP servers built on the SDK v2 serve multiple protocol eras from the same server. The SDK negotiates the era each client speaks: the sessionless `2026-07-28` era (which discovers capabilities through `server/discover`) and earlier session-based handshake versions are all handled simultaneously. This formally supersedes FastMCP's earlier "latest protocol only" stance — a single server now works with clients across the protocol transition.
|
||||
|
||||
|
|
@ -273,7 +428,7 @@ Migrating differs by capability. For **roots**, the guard pattern is the direct
|
|||
| --- | --- | --- |
|
||||
| `ctx.info` / logging notifications | Supported | Supported |
|
||||
| Tools, resources, prompts, completions | Supported | Supported |
|
||||
| `ctx.elicit` | Supported | Use the guard pattern (return `InputRequiredResult`) |
|
||||
| `ctx.elicit` | Supported | Raises — use the guard pattern (return `InputRequiredResult`) |
|
||||
| `ctx.sample` / `ctx.sample_step` | Method removed — call an LLM server-side | Method removed — call an LLM server-side, or ask via the guard pattern |
|
||||
| `ctx.list_roots` | Method removed — take paths as tool arguments | Method removed — ask via the guard pattern, or take paths as tool arguments |
|
||||
| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` needs session state the era lacks |
|
||||
|
|
@ -281,23 +436,25 @@ Migrating differs by capability. For **roots**, the guard pattern is the direct
|
|||
| Session state (`ctx.set_state` across calls) | Persists for the session | Does not persist — every request is a fresh connection |
|
||||
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
|
||||
|
||||
Two of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; and each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next. A server that gates access in `on_initialize` or relies on per-session state must keep its clients on the session-based era. The narrow escape is per-client: `Client(server, mode="legacy")`. The durable, server-side answer is to declare the versions the server actually serves so a modern client is refused at connect time rather than silently losing those features — see the server's protocol-version restriction (added alongside this change).
|
||||
Several of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next; and a tool that calls [`ctx.elicit()`](#behavior-changes) raises. A server that gates access in `on_initialize`, relies on per-session state, or elicits mid-tool must keep its clients on the session-based era. The control is per-client: `Client(server, mode="legacy")`. There is no server-side setting that restricts which protocol versions a server offers, so a server whose behavior depends on the handshake era depends on its callers opting into it — which is only practical when you control them. If you don't, port the behavior instead: a guard tool for elicitation, [session state](/servers/sessions) for what `ctx.set_state` held, and per-request auth checks for what `on_initialize` gated.
|
||||
|
||||
The client side is unaffected. `sampling_handler=` and `roots=` mean what they always did — see [client sampling](/clients/sampling) and [client roots](/clients/roots) — and one registration serves both routes, since a handshake-era server's pushed request and a modern server's returned one dispatch to the same handler.
|
||||
|
||||
## Upgrade checklist
|
||||
## Upgrade Checklist
|
||||
|
||||
Most servers upgrade untouched. Work down this list to find the ones that don't:
|
||||
|
||||
1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`.
|
||||
2. **Fix imports that moved out.** Replace `from mcp.types import X` with `from mcp_types import X`, and update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims).
|
||||
3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods-and-mount-keywords).
|
||||
3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods).
|
||||
4. **Replace `ctx.sample` and `ctx.list_roots`.** Both are gone from `Context`, as are `FastMCP(sampling_handler=...)` and `sampling_handler_behavior=`. Call an LLM directly from your server for generation; ask for roots through the guard pattern, or take file paths as tool arguments. A server whose purpose is to use the caller's model should stay on FastMCP 3.x rather than migrate.
|
||||
5. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
|
||||
6. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
|
||||
7. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
|
||||
8. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize` or per-session state, keep its clients on `mode="legacy"` or restrict the server's served protocol versions.
|
||||
9. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, update any client that matched the old `-32002` resource-not-found code, and if your server mints its own OAuth tokens (`OAuthProxy` and the providers built on it) under an `issuer_url` that differs from its `base_url`, schedule the [one-time re-authorization](#behavior-changes-to-verify) its clients now need.
|
||||
10. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
|
||||
5. **Find every `ctx.elicit()` call.** It raises on modern connections, which is what a default client now negotiates. Rewrite the tool as a guard tool, branch on `ctx.request_context.protocol_version`, or keep its clients on `mode="legacy"` — see [the era gate](#behavior-changes).
|
||||
6. **Register the tasks extension.** A `task=True` tool needs `mcp.add_extension(TasksExtension())` or the server won't start. Drop `task=` from resource and prompt decorators, move `TaskConfig` to `fastmcp.utilities.tasks`, and replace client-side `call_tool(..., task=True)` with plain `call_tool` or `call_tool_task`.
|
||||
7. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
|
||||
8. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
|
||||
9. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
|
||||
10. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize`, per-session state, or `ctx.elicit()`, keep its clients on `mode="legacy"`, or port the behavior forward — there is no server-side protocol-version restriction.
|
||||
11. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, guard any middleware that now sees notifications and unroutable requests, update any client that matched the old `-32002` resource-not-found code, and if your server mints its own OAuth tokens (`OAuthProxy` and the providers built on it) under an `issuer_url` that differs from its `base_url`, schedule the [one-time re-authorization](#behavior-changes) its clients now need.
|
||||
12. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
|
||||
|
||||
The executable version of this checklist lives in [`tests/test_upgrade_from_v3.py`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/test_upgrade_from_v3.py): it builds representative 3.x-style servers and asserts they run unchanged, and pins every removed surface to the exact error it now raises.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Upgrading from the MCP Low-Level SDK
|
||||
sidebarTitle: "From MCP Low-Level SDK"
|
||||
description: Upgrade your MCP server from the low-level Python SDK's Server class to FastMCP
|
||||
title: Upgrading from the Low-Level SDK v1
|
||||
sidebarTitle: "From Low-Level SDK v1"
|
||||
description: Upgrade your MCP server from v1 of the low-level Python SDK's Server class to FastMCP
|
||||
icon: up
|
||||
---
|
||||
|
||||
|
|
@ -9,78 +9,90 @@ If you've been building MCP servers directly on the `mcp` package's `Server` cla
|
|||
|
||||
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.
|
||||
|
||||
## Why now is the moment to switch
|
||||
## The SDK v2 Transition
|
||||
|
||||
MCP SDK v2 landed sweeping breaking changes on the low-level `Server`: the protocol types moved out of `mcp.types` into a separate `mcp_types` package, every field was renamed from camelCase to snake_case, the `Server` class was rebuilt, `McpError` was renamed, and sessions were removed on the new sessionless protocol era. If you build directly on the low-level SDK, all of that lands on you — you have to rewrite your imports, your handler signatures, and your error construction to match the new surface.
|
||||
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:
|
||||
|
||||
Adopting FastMCP is the easier path. FastMCP 4 runs on SDK v2 and hides that entire surface behind a high-level API that did not change. You write `@mcp.tool` and never touch the renamed internals — FastMCP derives the protocol layer from your function signatures, so the SDK v2 rename simply isn't something your code has to know about. Migrating low-level-SDK-v1 code to FastMCP is less work than migrating it to raw SDK v2, and you come out the other side with the whole framework: composition, middleware, proxies, authentication, and testing. The SDK v2 break is the natural moment to make the jump.
|
||||
```
|
||||
ModuleNotFoundError: No module named 'mcp.types'
|
||||
AttributeError: 'Server' object has no attribute 'list_tools'
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
pip install "mcp<2"
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
<Note>
|
||||
Already using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is simpler — see the [FastMCP 1.0 upgrade guide](/getting-started/upgrading/from-mcp-sdk) instead.
|
||||
Already on SDK v2's rebuilt `Server` class, with constructor-registered `on_*` handlers? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-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](/getting-started/upgrading/from-mcp-sdk-v1).
|
||||
</Note>
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from the `mcp` package's low-level Server class (v1) to FastMCP 4. The server currently uses `mcp.server.Server` (or `mcp.server.lowlevel.server.Server`) with manual handler registration. Analyze the provided code and rewrite it using FastMCP's high-level API. The full guide is at https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
You are rewriting an MCP server built on v1 of the `mcp` package's low-level `Server` class (`mcp.server.Server` or `mcp.server.lowlevel.server.Server`, with decorator-registered handlers) using FastMCP 4's high-level API.
|
||||
|
||||
UPGRADE RULES:
|
||||
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1 — it explains every item below, with before-and-after code for each handler group. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
|
||||
|
||||
1. IMPORTS: Replace all `mcp.*` imports with FastMCP equivalents.
|
||||
- `from mcp.server import Server` or `from mcp.server.lowlevel.server import Server` → `from fastmcp import FastMCP`
|
||||
- `import mcp.types as types` → remove (not needed for most code)
|
||||
- `from mcp.server.stdio import stdio_server` → remove (handled by mcp.run())
|
||||
- `from mcp.server.sse import SseServerTransport` → remove (handled by mcp.run())
|
||||
Then work through the provided code. This is a rewrite, not a patch: most of what you find gets deleted rather than translated.
|
||||
|
||||
2. SERVER: Replace `Server("name")` with `FastMCP("name")`.
|
||||
CONSTRUCTION AND TRANSPORT
|
||||
- `Server("name")`
|
||||
- `async with stdio_server() as (r, w): await server.run(r, w, server.create_initialization_options())`
|
||||
- `SseServerTransport` / `StreamableHTTPSessionManager` and any Starlette wiring around them
|
||||
- `asyncio.run(main())` boilerplate
|
||||
- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do.
|
||||
|
||||
3. TOOLS: Replace the list_tools + call_tool handler pair with individual @mcp.tool decorators.
|
||||
- Delete the `@server.list_tools()` handler entirely
|
||||
- Delete the `@server.call_tool()` handler entirely
|
||||
- For each tool that was listed in list_tools and dispatched in call_tool, create a new function:
|
||||
- Decorate it with `@mcp.tool`
|
||||
- Use the tool name as the function name (or pass name= to the decorator)
|
||||
- Use the docstring for the description (or pass description= to the decorator)
|
||||
- Convert the inputSchema JSON Schema into typed Python parameters (e.g., `{"type": "integer"}` → `int`, `{"type": "string"}` → `str`, `{"type": "array", "items": {"type": "string"}}` → `list[str]`)
|
||||
- Return plain Python values (`str`, `int`, `dict`, etc.) instead of `list[types.TextContent(...)]`
|
||||
- If the tool returned `types.ImageContent` or `types.EmbeddedResource`, use `from fastmcp.utilities.types import Image` or return the appropriate type
|
||||
HANDLERS TO DELETE (each becomes one or more decorated functions)
|
||||
- `@server.list_tools()` + `@server.call_tool()` — note the `if name == ...` dispatch chain inside call_tool; each branch becomes its own `@mcp.tool`
|
||||
- `@server.list_resources()` + `@server.list_resource_templates()` + `@server.read_resource()` — note any manual URI parsing, which the `{placeholder}` syntax replaces
|
||||
- `@server.list_prompts()` + `@server.get_prompt()`
|
||||
- any other `@server.*()` handler in the file — completion, resource subscribe/unsubscribe, logging level, progress. Look these up in the FastMCP docs rather than assuming a decorator name maps one-to-one.
|
||||
|
||||
4. RESOURCES: Replace the list_resources + list_resource_templates + read_resource handler trio with individual @mcp.resource decorators.
|
||||
- Delete all three handlers
|
||||
- For each static resource, create a function decorated with `@mcp.resource("uri://...")`
|
||||
- For each resource template, use `@mcp.resource("uri://{param}/path")` with `{param}` in the URI and a matching function parameter
|
||||
- Return str for text content, bytes for binary content
|
||||
- Set `mime_type=` in the decorator if needed
|
||||
TYPES THAT DISAPPEAR FROM YOUR CODE
|
||||
- hand-written `inputSchema` JSON Schema dicts — these come from type hints now
|
||||
- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument`
|
||||
- `types.TextContent` wrappers around return values — return plain Python values instead
|
||||
- `types.ImageContent`, `types.EmbeddedResource`
|
||||
- `types.PromptMessage`, `types.GetPromptResult`
|
||||
- Note that `mcp.types` no longer exists at all in the SDK v2 that FastMCP 4 builds on; any type that genuinely survives the rewrite comes from `mcp_types` now.
|
||||
|
||||
5. PROMPTS: Replace the list_prompts + get_prompt handler pair with individual @mcp.prompt decorators.
|
||||
- Delete both handlers
|
||||
- For each prompt, create a function decorated with `@mcp.prompt`
|
||||
- Convert PromptArgument definitions into typed function parameters
|
||||
- Return str for simple single-message prompts (auto-wrapped as user message)
|
||||
- Return `list[Message]` for multi-message prompts: `from fastmcp.prompts import Message`
|
||||
- `Message("text")` defaults to `role="user"`; use `Message("text", role="assistant")` for assistant messages
|
||||
CONTEXT AND SIDE CHANNELS
|
||||
- `server.request_context`
|
||||
- `session.send_log_message(...)`, `session.send_progress_notification(...)`
|
||||
- direct session use for anything else — a FastMCP `Context` has a `ctx.session` property returning the underlying SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled
|
||||
|
||||
6. TRANSPORT: Replace all transport boilerplate with mcp.run().
|
||||
- `async with stdio_server() as (r, w): await server.run(r, w, ...)` → `mcp.run()` (`stdio` is the default)
|
||||
- SSE/Starlette setup → `mcp.run(transport="sse", host="...", port=...)`
|
||||
- Streamable HTTP setup → `mcp.run(transport="http", host="...", port=...)`
|
||||
- Delete asyncio.run(main()) boilerplate — use `if __name__ == "__main__": mcp.run()`
|
||||
ERRORS
|
||||
- `raise ValueError(f"Unknown tool: ...")` and other dispatch fallbacks — these become unnecessary
|
||||
- `McpError` construction and any error-code mapping
|
||||
|
||||
7. CONTEXT: Replace `server.request_context` with FastMCP's Context parameter.
|
||||
- Add `from fastmcp import Context` and add a `ctx: Context` parameter to any tool that needs it
|
||||
- `server.request_context.session.send_log_message(...)` → `await ctx.info("message")` or `await ctx.warning("message")`
|
||||
- Progress reporting → `await ctx.report_progress(current, total)`
|
||||
|
||||
For each change, show the original code, explain what it did, and provide the FastMCP equivalent.
|
||||
For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one.
|
||||
</Prompt>
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
pip install --upgrade fastmcp
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add fastmcp
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
```
|
||||
|
||||
FastMCP includes the `mcp` package as a transitive dependency, so you don't lose access to anything.
|
||||
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](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
|
||||
FastMCP depends on the `mcp` package, so the SDK stays installed. Note that FastMCP 4 builds on SDK v2, where `mcp.types` no longer exists — protocol types live in the standalone `mcp_types` package now. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures. For the few you still need, import them from `mcp_types`.
|
||||
|
||||
## Server and Transport
|
||||
|
||||
|
|
@ -88,7 +100,7 @@ The `Server` class requires you to choose a transport, connect streams, build in
|
|||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
```python Before test="skip"
|
||||
import asyncio
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
|
@ -124,7 +136,12 @@ if __name__ == "__main__":
|
|||
Need HTTP instead of stdio? With the `Server` class, you'd wire up Starlette routes and `SseServerTransport` or `StreamableHTTPSessionManager`. With FastMCP:
|
||||
|
||||
```python
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("my-server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
|
@ -133,7 +150,7 @@ This is where the difference is most dramatic. The `Server` class requires two h
|
|||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
```python Before test="skip"
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
||||
|
|
@ -240,7 +257,7 @@ The `Server` class uses three handlers for resources: `list_resources()` to enum
|
|||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
```python Before test="skip"
|
||||
import json
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
|
@ -333,7 +350,7 @@ Same pattern: the `Server` class uses `list_prompts()` and `get_prompt()` with m
|
|||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
```python Before test="skip"
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
||||
|
|
@ -420,7 +437,7 @@ The `Server` class exposes request context through `server.request_context`, whi
|
|||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
```python Before test="skip"
|
||||
import mcp.types as types
|
||||
from mcp.server import Server
|
||||
|
||||
|
|
@ -458,13 +475,31 @@ async def process_data(ctx: Context) -> str:
|
|||
|
||||
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](/servers/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:
|
||||
|
||||
```python test="skip"
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
# Before (SDK v1):
|
||||
# raise McpError(ErrorData(code=-32000, message="Upstream unavailable"))
|
||||
|
||||
# After:
|
||||
raise McpError(code=-32000, message="Upstream unavailable")
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before expandable
|
||||
```python Before expandable test="skip"
|
||||
import asyncio
|
||||
import json
|
||||
import mcp.types as types
|
||||
|
|
@ -580,15 +615,10 @@ if __name__ == "__main__":
|
|||
|
||||
</CodeGroup>
|
||||
|
||||
## What's Next
|
||||
## What You Gain
|
||||
|
||||
Once you've upgraded, you have access to everything FastMCP provides beyond the basics:
|
||||
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](/servers/composition)** — Mount sub-servers to build modular applications
|
||||
- **[Middleware](/servers/middleware)** — Add logging, rate limiting, error handling, and caching
|
||||
- **[Proxy servers](/servers/providers/proxy)** — Create a proxy to any existing MCP server
|
||||
- **[OpenAPI integration](/integrations/openapi)** — Generate an MCP server from an OpenAPI spec
|
||||
- **[Authentication](/servers/auth/authentication)** — Built-in OAuth and token verification
|
||||
- **[Testing](/servers/testing)** — Test your server directly in Python without running a subprocess
|
||||
[Server composition](/servers/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](/servers/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](/servers/providers/proxy) 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](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) arrives as a single `auth=` provider covering token verification, OAuth, and named providers for GitHub, Google, Auth0, and others.
|
||||
|
||||
Explore the full documentation at [gofastmcp.com](https://gofastmcp.com).
|
||||
The change most likely to affect your daily work is [testing](/servers/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.
|
||||
622
docs/getting-started/upgrading/from-low-level-sdk-v2.mdx
Normal file
622
docs/getting-started/upgrading/from-low-level-sdk-v2.mdx
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
---
|
||||
title: Upgrading from the Low-Level SDK v2
|
||||
sidebarTitle: "From Low-Level SDK v2"
|
||||
description: Move a server built on v2 of the low-level Python SDK's Server class to FastMCP
|
||||
icon: up
|
||||
---
|
||||
|
||||
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](#stricter-arguments), covered below.
|
||||
|
||||
<Note>
|
||||
On SDK v1's decorator-registered `Server` — `@server.list_tools()`, `@server.call_tool()` — instead? See [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-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](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming.
|
||||
</Note>
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are rewriting an MCP server built on the MCP Python SDK v2's low-level `Server` class (`mcp.server.lowlevel.server.Server`, with `on_*` handlers passed to the constructor) using FastMCP 4's high-level API.
|
||||
|
||||
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2 — it explains every item below in full, with before-and-after code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not guess at a FastMCP API you have not confirmed in the docs.
|
||||
|
||||
Then work through the provided code looking for each of these. The guide has the replacement for every one:
|
||||
|
||||
CONSTRUCTION AND TRANSPORT
|
||||
- `Server(name, on_list_tools=..., on_call_tool=..., ...)` — the whole constructor, including every handler passed to it
|
||||
- `server.run(read_stream, write_stream, server.create_initialization_options())` and its `stdio_server()` context manager
|
||||
- `server.streamable_http_app()` and any Starlette app assembled around it
|
||||
- `asyncio.run(main())` boilerplate
|
||||
- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do.
|
||||
|
||||
HANDLERS TO DELETE, EACH REPLACED BY ONE DECORATOR (not simply removed)
|
||||
- `on_list_tools` + `on_call_tool` → one `@mcp.tool` function per branch of the `if params.name == ...` dispatch chain inside `on_call_tool`
|
||||
- `on_list_resources` + `on_list_resource_templates` + `on_read_resource` → one `@mcp.resource` function per resource/template
|
||||
- `on_list_prompts` + `on_get_prompt` → one `@mcp.prompt` function per prompt
|
||||
- `on_completion` → one `@mcp.completion` function. This one is easy to drop by mistake: skipping it does not just remove autocomplete cleanly, it silently stops FastMCP from advertising the completions capability at all, since that capability is only advertised when a handler is registered.
|
||||
- `on_subscribe_resource` / `on_unsubscribe_resource` / `on_subscriptions_listen` — flag for the user, no single-decorator equivalent
|
||||
- `on_set_logging_level`, `on_progress`, `on_roots_list_changed`, `on_ping` — flag for the user, these are protocol-level hooks with no direct FastMCP surface
|
||||
|
||||
TYPES THAT DISAPPEAR FROM YOUR CODE
|
||||
- Hand-written `input_schema` / `output_schema` JSON Schema dicts — these come from type hints now
|
||||
- `types.ListToolsResult`, `types.CallToolResult`, `types.ListResourcesResult`, `types.ListResourceTemplatesResult`, `types.ReadResourceResult`, `types.ListPromptsResult`, `types.GetPromptResult` — result wrappers FastMCP builds for you
|
||||
- `types.TextContent`, `types.TextResourceContents`, `types.BlobResourceContents` — return plain Python values instead
|
||||
- `types.ImageContent` / `types.AudioContent` — `fastmcp.utilities.types.Image` / `Audio`
|
||||
- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument` — declaration types FastMCP derives
|
||||
- `types.PromptMessage` — `fastmcp.prompts.Message`
|
||||
- Note which `mcp_types` imports are still needed afterward; protocol types are unchanged in FastMCP, so surviving imports stay as they are.
|
||||
|
||||
CONTEXT AND SIDE CHANNELS
|
||||
- `ctx.session.send_log_message(...)` — `ctx.info()` / `ctx.debug()` / `ctx.warning()` / `ctx.error()` on a `fastmcp.Context` parameter
|
||||
- `ctx.session.report_progress(...)` — `ctx.report_progress()`
|
||||
- `ctx.request_id`, `ctx.meta`, `ctx.protocol_version` — these live on `ctx.request_context` in FastMCP (`ctx.request_context.request_id`, and so on); note that `ctx.protocol_version` directly on the Context does not exist
|
||||
- `ctx.params` — no equivalent, and none is needed: the raw request params were how a low-level handler read the tool's arguments, and those are now the decorated function's typed parameters. `ctx.request_context.params` does NOT exist and raises AttributeError.
|
||||
- Direct `ctx.session` use for anything else — `Context.session` exists in FastMCP too and returns the same raw SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled
|
||||
|
||||
ERRORS AND AUTH
|
||||
- `raise ValueError(f"Unknown tool: ...")` dispatch fallbacks — these become unnecessary
|
||||
- `MCPError` construction and any error-code mapping
|
||||
- `auth=AuthSettings(...)`, `token_verifier=`, `auth_server_provider=` — one `auth=` provider in FastMCP
|
||||
- `TransportSecuritySettings`
|
||||
|
||||
For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one.
|
||||
</Prompt>
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
```
|
||||
|
||||
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](/getting-started/upgrading/from-fastmcp-3#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.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before test="skip"
|
||||
import asyncio
|
||||
|
||||
from mcp.server.lowlevel.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
server = Server("my-server") # plus every on_* handler
|
||||
|
||||
async def main():
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
server.create_initialization_options(),
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("my-server")
|
||||
|
||||
# ... register tools, resources, prompts ...
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("my-server")
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import mcp_types as types
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel.server import Server
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult:
|
||||
number = {"type": "number"}
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"a": number, "b": number},
|
||||
"required": ["a", "b"],
|
||||
}
|
||||
return types.ListToolsResult(
|
||||
tools=[
|
||||
types.Tool(name="add", description="Add two numbers", input_schema=schema),
|
||||
types.Tool(
|
||||
name="multiply", description="Multiply two numbers", input_schema=schema
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def call_tool(
|
||||
ctx: ServerRequestContext, params: types.CallToolRequestParams
|
||||
) -> types.CallToolResult:
|
||||
arguments = params.arguments or {}
|
||||
if params.name == "add":
|
||||
result = arguments["a"] + arguments["b"]
|
||||
elif params.name == "multiply":
|
||||
result = arguments["a"] * arguments["b"]
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {params.name}")
|
||||
return types.CallToolResult(content=[types.TextContent(type="text", text=str(result))])
|
||||
|
||||
|
||||
server = Server("math", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("math")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def add(a: float, b: float) -> float:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def multiply(a: float, b: float) -> float:
|
||||
"""Multiply two numbers"""
|
||||
return a * b
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
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:
|
||||
|
||||
| JSON Schema | Python type |
|
||||
|---|---|
|
||||
| `{"type": "string"}` | `str` |
|
||||
| `{"type": "number"}` | `float` |
|
||||
| `{"type": "integer"}` | `int` |
|
||||
| `{"type": "boolean"}` | `bool` |
|
||||
| `{"type": "array", "items": {"type": "string"}}` | `list[str]` |
|
||||
| `{"type": "object"}` | `dict` |
|
||||
| A property absent from `required` | `param: str \| None = None` |
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.types import Image
|
||||
|
||||
mcp = FastMCP("media")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def create_chart(data: list[float]) -> Image:
|
||||
"""Generate a chart from data."""
|
||||
png_bytes = render_png(data) # your logic
|
||||
return Image(data=png_bytes, format="png")
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```python test="skip"
|
||||
# Against the low-level handler: succeeds, "extra" never read.
|
||||
# Against FastMCP: raises, "extra" is not a parameter of greet().
|
||||
await client.call_tool("greet", {"name": "World", "extra": "surprise"})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import json
|
||||
|
||||
import mcp_types as types
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel.server import Server
|
||||
|
||||
|
||||
async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult:
|
||||
return types.ListResourcesResult(
|
||||
resources=[
|
||||
types.Resource(
|
||||
uri="config://app",
|
||||
name="app_config",
|
||||
description="Application configuration",
|
||||
mime_type="application/json",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def list_resource_templates(
|
||||
ctx: ServerRequestContext, params
|
||||
) -> types.ListResourceTemplatesResult:
|
||||
return types.ListResourceTemplatesResult(
|
||||
resource_templates=[
|
||||
types.ResourceTemplate(
|
||||
uri_template="users://{user_id}/profile",
|
||||
name="user_profile",
|
||||
description="User profile by ID",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def read_resource(
|
||||
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
|
||||
) -> types.ReadResourceResult:
|
||||
uri = str(params.uri)
|
||||
if uri == "config://app":
|
||||
text = json.dumps({"debug": False, "version": "1.0"})
|
||||
elif uri.startswith("users://"):
|
||||
user_id = uri.split("/")[2]
|
||||
text = json.dumps({"id": user_id, "name": f"User {user_id}"})
|
||||
else:
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
return types.ReadResourceResult(
|
||||
contents=[
|
||||
types.TextResourceContents(
|
||||
uri=params.uri, mime_type="application/json", text=text
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
server = Server(
|
||||
"data",
|
||||
on_list_resources=list_resources,
|
||||
on_list_resource_templates=list_resource_templates,
|
||||
on_read_resource=read_resource,
|
||||
)
|
||||
```
|
||||
|
||||
```python After
|
||||
import json
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("data")
|
||||
|
||||
|
||||
@mcp.resource("config://app", mime_type="application/json")
|
||||
def app_config() -> str:
|
||||
"""Application configuration"""
|
||||
return json.dumps({"debug": False, "version": "1.0"})
|
||||
|
||||
|
||||
@mcp.resource("users://{user_id}/profile", mime_type="application/json")
|
||||
def user_profile(user_id: str) -> str:
|
||||
"""User profile by ID"""
|
||||
return json.dumps({"id": user_id, "name": f"User {user_id}"})
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
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](/servers/resources#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.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import mcp_types as types
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel.server import Server
|
||||
|
||||
|
||||
async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult:
|
||||
return types.ListPromptsResult(
|
||||
prompts=[
|
||||
types.Prompt(
|
||||
name="review_code",
|
||||
description="Review code for issues",
|
||||
arguments=[
|
||||
types.PromptArgument(
|
||||
name="code", description="The code to review", required=True
|
||||
),
|
||||
types.PromptArgument(
|
||||
name="language", description="Programming language", required=False
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def get_prompt(
|
||||
ctx: ServerRequestContext, params: types.GetPromptRequestParams
|
||||
) -> types.GetPromptResult:
|
||||
if params.name != "review_code":
|
||||
raise ValueError(f"Unknown prompt: {params.name}")
|
||||
arguments = params.arguments or {}
|
||||
language = arguments.get("language", "")
|
||||
note = f" (written in {language})" if language else ""
|
||||
text = f"Please review this code{note}:\n\n{arguments.get('code', '')}"
|
||||
return types.GetPromptResult(
|
||||
description="Code review prompt",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="user", content=types.TextContent(type="text", text=text)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
server = Server("prompts", on_list_prompts=list_prompts, on_get_prompt=get_prompt)
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
|
||||
@mcp.prompt
|
||||
def review_code(code: str, language: str | None = None) -> str:
|
||||
"""Review code for issues"""
|
||||
note = f" (written in {language})" if language else ""
|
||||
return f"Please review this code{note}:\n\n{code}"
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
|
||||
@mcp.prompt
|
||||
def debug_session(error: str) -> list[Message]:
|
||||
"""Start a debugging conversation"""
|
||||
return [
|
||||
Message(f"I'm seeing this error:\n\n{error}"),
|
||||
Message("I'll help you debug that. Can you share the relevant code?", role="assistant"),
|
||||
]
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import mcp_types as types
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel.server import Server
|
||||
|
||||
|
||||
async def call_tool(
|
||||
ctx: ServerRequestContext, params: types.CallToolRequestParams
|
||||
) -> types.CallToolResult:
|
||||
if params.name == "process_data":
|
||||
await ctx.session.send_log_message(level="info", data="Starting processing...")
|
||||
await ctx.session.report_progress(1, 2)
|
||||
# ... do work ...
|
||||
await ctx.session.send_log_message(level="info", data="Done!")
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text="Processed")]
|
||||
)
|
||||
raise ValueError(f"Unknown tool: {params.name}")
|
||||
|
||||
|
||||
server = Server("worker", on_call_tool=call_tool)
|
||||
```
|
||||
|
||||
```python After
|
||||
from fastmcp import FastMCP, Context
|
||||
|
||||
mcp = FastMCP("worker")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def process_data(ctx: Context) -> str:
|
||||
"""Process data with progress logging"""
|
||||
await ctx.info("Starting processing...")
|
||||
await ctx.report_progress(1, 2)
|
||||
# ... do work ...
|
||||
await ctx.info("Done!")
|
||||
return "Processed"
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
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](/servers/sessions), elicitation, and component visibility — see [Context](/servers/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:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before expandable
|
||||
import json
|
||||
|
||||
import mcp_types as types
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel.server import Server
|
||||
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(
|
||||
tools=[
|
||||
types.Tool(
|
||||
name="greet",
|
||||
description="Greet someone by name",
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def call_tool(
|
||||
ctx: ServerRequestContext, params: types.CallToolRequestParams
|
||||
) -> types.CallToolResult:
|
||||
if params.name == "greet":
|
||||
name = (params.arguments or {})["name"]
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"Hello, {name}!")]
|
||||
)
|
||||
raise ValueError(f"Unknown tool: {params.name}")
|
||||
|
||||
|
||||
async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult:
|
||||
return types.ListResourcesResult(
|
||||
resources=[
|
||||
types.Resource(
|
||||
uri="info://version", name="version", description="Server version"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def read_resource(
|
||||
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
|
||||
) -> types.ReadResourceResult:
|
||||
if str(params.uri) != "info://version":
|
||||
raise ValueError(f"Unknown resource: {params.uri}")
|
||||
return types.ReadResourceResult(
|
||||
contents=[
|
||||
types.TextResourceContents(
|
||||
uri=params.uri, text=json.dumps({"version": "1.0.0"})
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult:
|
||||
return types.ListPromptsResult(
|
||||
prompts=[
|
||||
types.Prompt(
|
||||
name="summarize",
|
||||
description="Summarize text",
|
||||
arguments=[types.PromptArgument(name="text", required=True)],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def get_prompt(
|
||||
ctx: ServerRequestContext, params: types.GetPromptRequestParams
|
||||
) -> types.GetPromptResult:
|
||||
if params.name != "summarize":
|
||||
raise ValueError(f"Unknown prompt: {params.name}")
|
||||
text = (params.arguments or {}).get("text", "")
|
||||
return types.GetPromptResult(
|
||||
description="Summarize text",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="user",
|
||||
content=types.TextContent(type="text", text=f"Summarize:\n\n{text}"),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
server = Server(
|
||||
"demo",
|
||||
on_list_tools=list_tools,
|
||||
on_call_tool=call_tool,
|
||||
on_list_resources=list_resources,
|
||||
on_read_resource=read_resource,
|
||||
on_list_prompts=list_prompts,
|
||||
on_get_prompt=get_prompt,
|
||||
)
|
||||
```
|
||||
|
||||
```python After
|
||||
import json
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("demo")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone by name"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
|
||||
@mcp.resource("info://version")
|
||||
def version() -> str:
|
||||
"""Server version"""
|
||||
return json.dumps({"version": "1.0.0"})
|
||||
|
||||
|
||||
@mcp.prompt
|
||||
def summarize(text: str) -> str:
|
||||
"""Summarize text"""
|
||||
return f"Summarize:\n\n{text}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## 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](/servers/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](/servers/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](/servers/providers/proxy) 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](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/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](/servers/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.
|
||||
264
docs/getting-started/upgrading/from-mcp-sdk-v1.mdx
Normal file
264
docs/getting-started/upgrading/from-mcp-sdk-v1.mdx
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
---
|
||||
title: Upgrading from MCP SDK v1
|
||||
sidebarTitle: "From MCP SDK v1"
|
||||
description: Upgrade from FastMCP 1.0, bundled in v1 of the MCP Python SDK, to the standalone FastMCP framework
|
||||
icon: up
|
||||
---
|
||||
|
||||
If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.**
|
||||
|
||||
```python test="skip"
|
||||
# Before
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# After
|
||||
from fastmcp import FastMCP
|
||||
```
|
||||
|
||||
That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is.
|
||||
|
||||
<Tip>
|
||||
**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes.
|
||||
</Tip>
|
||||
|
||||
## The SDK v2 Transition
|
||||
|
||||
MCP SDK v2 is a substantial, deliberate modernization of the protocol layer, and part of that work rebuilt the high-level server as `MCPServer` under `mcp.server.mcpserver`. `mcp.server.fastmcp` does not exist there — so a FastMCP 1.0 server meets the change the moment its environment resolves `mcp` to v2:
|
||||
|
||||
```
|
||||
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
|
||||
```
|
||||
|
||||
Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version and the module your server imports on line one has moved. 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 old module immediately, with no code changes, and buys you time to choose deliberately:
|
||||
|
||||
```bash
|
||||
pip install "mcp<2"
|
||||
```
|
||||
|
||||
## Two Upgrade Paths
|
||||
|
||||
From here, both directions are reasonable, and which is less work depends on which API you already write.
|
||||
|
||||
**`MCPServer`, the SDK's high-level server**, is a capable, well-designed API and the direct continuation of the SDK's own line. Because it was rebuilt rather than renamed, expect real work: a new class and import, a different decorator call style, and protocol types imported from the standalone `mcp_types` package with snake_case field names.
|
||||
|
||||
**FastMCP** is the import change at the top of this page. It is short for a specific, historical reason: FastMCP 1.0 *is* early FastMCP — it was contributed into the `mcp` package, and the standalone project kept developing that same high-level API. The surface you already write against is the surface FastMCP still offers. FastMCP 4 is itself built on MCP SDK v2, so both paths land you on the same modern protocol layer; FastMCP absorbs the adaptation internally rather than asking your code to do it.
|
||||
|
||||
The claim is narrower than it may sound. It holds for FastMCP 1.0 servers specifically, because of shared lineage — not because one library is better than the other. Both projects are moving the same direction on the same protocol.
|
||||
|
||||
If you have already moved to SDK v2 and write against `MCPServer` today, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2). If your server uses the low-level `Server` class rather than the high-level one, see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1).
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
```
|
||||
|
||||
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](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
|
||||
FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` and `mcp.types` are both gone — anything you imported from those two modules needs a new home, and the sections below cover both. Update your import, run your server, and if your tools work, you're done.
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from FastMCP 1.0 (bundled in v1 of the `mcp` package) to standalone FastMCP 4.
|
||||
|
||||
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
|
||||
|
||||
For most servers the entire upgrade is the first item. Work through the rest looking for signals, and report only what you actually find.
|
||||
|
||||
THE IMPORT (every server needs this)
|
||||
- `from mcp.server.fastmcp import FastMCP` → `from fastmcp import FastMCP`
|
||||
- `from mcp.server.fastmcp import Context`
|
||||
- `from mcp.server.fastmcp import Image`
|
||||
|
||||
CONSTRUCTOR ARGUMENTS THAT MOVED (all raise TypeError)
|
||||
- moved to run()/http_app(), and FastMCP names them in the error: host, port, log_level, debug, sse_path, message_path, streamable_http_path, json_response, stateless_http
|
||||
- moved but rejected with only a generic "unexpected keyword argument", so flag these explicitly: `event_store=` (→ `http_app(event_store=...)`; dropping it silently disables streamable-HTTP resumability), `mount_path=` (→ `http_app(path=...)`), `transport=` (→ `run(transport=...)`), `transport_security=` (→ host/origin settings on `http_app()`), `warn_on_duplicate_tools/_resources/_prompts=` (→ one `on_duplicate=`), `dependencies=` (→ a fastmcp.json file)
|
||||
- `name`, `instructions`, `website_url`, `icons`, `tools`, `lifespan` carry over unchanged
|
||||
- note when reporting: FastMCP names the streamable HTTP transport "http", not "streamable-http"
|
||||
|
||||
CONTEXT METHODS WITH CHANGED SIGNATURES (compile fine, fail at runtime)
|
||||
- `ctx.log(level, data)` → `ctx.log(message, level=...)`, message first
|
||||
- `ctx.info(data)` / `debug` / `warning` / `error` → take a str message, not arbitrary JSON-serializable data
|
||||
- `ctx.elicit(..., schema=Model)` → `response_type=Model`
|
||||
- `ctx.read_resource(uri)` → returns a `ResourceResult`; read `.contents` rather than iterating the return value
|
||||
- `ctx.report_progress`, `ctx.request_id`, `ctx.client_id` are unchanged
|
||||
|
||||
AUTHENTICATION (the one case where the single import change is NOT enough)
|
||||
- `token_verifier=` and `auth_server_provider=` — both raise TypeError on FastMCP 4
|
||||
- `auth=AuthSettings(...)` — the keyword survives but the value does not: FastMCP's `auth=` takes a FastMCP `AuthProvider`, not the SDK settings object
|
||||
Report these as a real migration, not a rename: FastMCP consolidates all three into one provider, and ships `JWTVerifier` for tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider without Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, and others. Look up the right one at https://gofastmcp.com/servers/auth/authentication rather than guessing.
|
||||
|
||||
PROMPT RETURN VALUES
|
||||
- prompt functions returning `PromptMessage`, or `TextContent`-wrapped content
|
||||
- prompt functions returning raw dicts with "role"/"content" keys — FastMCP 1.0 coerced these silently, standalone FastMCP does not
|
||||
|
||||
OTHER mcp.* IMPORTS
|
||||
- anything from `mcp.types` — the module does not exist in the SDK v2 that FastMCP 4 builds on; protocol types moved to `mcp_types` with camelCase fields renamed to snake_case
|
||||
- `from mcp.server.stdio import stdio_server` and any transport boilerplate around it
|
||||
- `mcp.types.TextContent` / `ImageContent` used to wrap tool return values — FastMCP has friendlier equivalents, so prefer those over a mechanical `mcp_types` swap
|
||||
|
||||
DECORATOR RETURN VALUES
|
||||
- any code reading `.name`, `.description`, or other component attributes off a `@mcp.tool` / `@mcp.resource` / `@mcp.prompt` decorated function. Decorators return the original function now.
|
||||
|
||||
For each item found, show the original line, name what changed, and give the corrected code from the guide. If the only change needed is the import, say so plainly rather than manufacturing work.
|
||||
</Prompt>
|
||||
|
||||
## What Might Need Updating
|
||||
|
||||
Most servers need nothing beyond the import change. Skim the sections below to see if any apply.
|
||||
|
||||
### Constructor Settings
|
||||
|
||||
If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed:
|
||||
|
||||
```python test="skip"
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Before
|
||||
mcp = FastMCP("my-server", host="0.0.0.0", port=8080)
|
||||
mcp.run()
|
||||
|
||||
# After
|
||||
mcp = FastMCP("my-server")
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8080)
|
||||
```
|
||||
|
||||
Nine arguments move this way, and each raises a `TypeError` naming its own replacement, so you can also just run the server and follow the errors: `host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, and `stateless_http`.
|
||||
|
||||
A second group is rejected with only a generic "unexpected keyword argument" and no hint, which makes these the ones worth reading in advance:
|
||||
|
||||
| SDK v1 `FastMCP(...)` | FastMCP 4 |
|
||||
|---|---|
|
||||
| `event_store=` | `mcp.http_app(event_store=...)` |
|
||||
| `mount_path=` | `mcp.http_app(path=...)` |
|
||||
| `transport=` | `mcp.run(transport=...)` |
|
||||
| `transport_security=` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` on `http_app()` |
|
||||
| `warn_on_duplicate_tools=`, `_resources=`, `_prompts=` | a single `on_duplicate=` |
|
||||
| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
|
||||
| `auth_server_provider=`, `token_verifier=` | a single `auth=` provider — see [Authentication](#authentication) below |
|
||||
|
||||
Dropping `event_store=` rather than moving it is the one to watch: it silently disables streamable-HTTP resumability, so a client that reconnects loses the events it missed instead of replaying them.
|
||||
|
||||
`name`, `instructions`, `website_url`, `icons`, `tools`, and `lifespan` carry over to the constructor unchanged.
|
||||
|
||||
### Authentication
|
||||
|
||||
This is the one case where the import change alone won't do. FastMCP 1.0 exposed the SDK's auth plumbing as three separate constructor arguments — `token_verifier=`, `auth_server_provider=`, and `auth=AuthSettings(...)`. The first two raise `TypeError` on FastMCP 4, and while `auth=` survives as a keyword, its value doesn't: FastMCP expects one of its own `AuthProvider` objects rather than the SDK's settings object.
|
||||
|
||||
The replacement is a single provider carrying the whole configuration, chosen by what you're actually doing:
|
||||
|
||||
| What you were doing | FastMCP provider |
|
||||
|---|---|
|
||||
| Validating JWTs you already issue | `JWTVerifier` |
|
||||
| Delegating to an external authorization server | `RemoteAuthProvider` |
|
||||
| Wrapping a provider without Dynamic Client Registration | `OAuthProxy` |
|
||||
| GitHub, Google, Auth0, Keycloak, WorkOS, … | the matching named provider |
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import JWTVerifier
|
||||
|
||||
mcp = FastMCP("my-server", auth=JWTVerifier(jwks_uri="https://example.com/.well-known/jwks.json"))
|
||||
```
|
||||
|
||||
See [Authentication](/servers/auth/authentication) for the full set and their configuration.
|
||||
|
||||
### Context Methods
|
||||
|
||||
`from fastmcp import Context` gets you the injected context object, but four of its methods took a different shape in FastMCP 1.0, and a bare import swap leaves calls that compile and then fail:
|
||||
|
||||
| SDK v1 | FastMCP 4 |
|
||||
|---|---|
|
||||
| `ctx.log(level, data)` | `ctx.log(message, level=...)` — message is first now |
|
||||
| `ctx.info(data)` and its `debug`/`warning`/`error` siblings | take a `str` message, where v1 accepted any JSON-serializable value |
|
||||
| `ctx.elicit(message, schema=Model)` | `ctx.elicit(message, response_type=Model)` |
|
||||
| `ctx.read_resource(uri)` | returns a `ResourceResult`; the payload is under `.contents` rather than being iterable directly |
|
||||
|
||||
`ctx.report_progress()`, `ctx.request_id`, and `ctx.client_id` are unchanged.
|
||||
|
||||
### Prompts
|
||||
|
||||
If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, upgrade them to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
@mcp.prompt
|
||||
def review(code: str) -> str:
|
||||
"""Review code for issues"""
|
||||
return f"Please review this code:\n\n{code}"
|
||||
```
|
||||
|
||||
Multi-turn prompts return a list of messages. `Message` takes the text positionally and defaults to the user role, so only the assistant turns need a `role`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
@mcp.prompt
|
||||
def debug(error: str) -> list[Message]:
|
||||
"""Start a debugging session"""
|
||||
return [
|
||||
Message(f"I'm seeing this error:\n\n{error}"),
|
||||
Message("I'll help debug that. Can you share the relevant code?", role="assistant"),
|
||||
]
|
||||
```
|
||||
|
||||
### Other `mcp.*` Imports
|
||||
|
||||
FastMCP 4 builds on MCP SDK v2, where the `mcp.types` module no longer exists. Protocol types moved to a standalone `mcp_types` package, and the field names were renamed from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). Update `from mcp.types import X` to `from mcp_types import X`. For everything else SDK v2 changed, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), which covers the same protocol rebuild from the FastMCP side.
|
||||
|
||||
Where FastMCP provides its own API for the same thing, it's worth switching over rather than importing the protocol type:
|
||||
|
||||
| MCP SDK v1 | FastMCP equivalent |
|
||||
|---|---|
|
||||
| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool |
|
||||
| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` |
|
||||
| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |
|
||||
| `mcp.server.fastmcp.Context` | `from fastmcp import Context` |
|
||||
| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |
|
||||
|
||||
For protocol types without a FastMCP equivalent, import them from `mcp_types` directly.
|
||||
|
||||
### Decorated Functions
|
||||
|
||||
In FastMCP 1.0, `@mcp.tool` replaced your function with a `FunctionTool` object. Now decorators return your original function unchanged, so decorated functions stay callable for testing, reuse, and composition:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("greeter")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# This works now — the function is still a regular function
|
||||
assert greet("World") == "Hello, World!"
|
||||
```
|
||||
|
||||
Code that reads `.name`, `.description`, or other component attributes off the decorated result needs updating. This is uncommon — most servers never touch the tool object. When you do need the component itself, reach it through the server with `await mcp.get_tool("greet")`.
|
||||
|
||||
## Verifying the Upgrade
|
||||
|
||||
Run your server the way you always have. To confirm every component came across, inspect the server with the FastMCP CLI:
|
||||
|
||||
```bash
|
||||
fastmcp inspect my_server.py
|
||||
```
|
||||
|
||||
The output lists every tool, resource, template, and prompt your server exposes, so a component that failed to register shows up here rather than at the first client call.
|
||||
|
||||
## Looking Ahead
|
||||
|
||||
The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change. The SDK v1 to v2 transition is the clearest example so far: an entire protocol layer was rewritten underneath FastMCP 4, and the servers on this page cross it with one line.
|
||||
325
docs/getting-started/upgrading/from-mcp-sdk-v2.mdx
Normal file
325
docs/getting-started/upgrading/from-mcp-sdk-v2.mdx
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
---
|
||||
title: Upgrading from MCP SDK v2
|
||||
sidebarTitle: "From MCP SDK v2"
|
||||
description: Move a server built on the MCP Python SDK v2's MCPServer class to FastMCP
|
||||
icon: up
|
||||
---
|
||||
|
||||
If your server starts with `from mcp.server.mcpserver import MCPServer`, you're using the high-level server API introduced in v2 of the `mcp` package. Moving to FastMCP is a mechanical migration: the two APIs share a lineage, so most of your code carries over with a rename.
|
||||
|
||||
```python
|
||||
# Before
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
server = MCPServer("my-server")
|
||||
|
||||
@server.tool()
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone by name"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# After
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("my-server")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone by name"""
|
||||
return f"Hello, {name}!"
|
||||
```
|
||||
|
||||
That resemblance is not a coincidence. `MCPServer` is the SDK's successor to FastMCP 1.0, the high-level server that shipped inside SDK v1; FastMCP is the standalone framework that grew from the same starting point. Both derive the protocol layer from your function signatures — type hints become JSON Schema, docstrings become descriptions, return values are serialized for you. What separates them is scope: `MCPServer` is the SDK's ergonomic surface over the protocol, while FastMCP builds on that same SDK v2 and adds the machinery a server needs in production — composition, middleware, proxying, authentication providers, tool transformation, a client, and a testing story.
|
||||
|
||||
<Note>
|
||||
Building on the low-level `Server` class instead? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2). Still on SDK v1's `mcp.server.fastmcp.FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1).
|
||||
</Note>
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are migrating an MCP server from the MCP Python SDK v2's high-level `MCPServer` class (`mcp.server.mcpserver`) to FastMCP 4. The two APIs are close relatives, so most of this is mechanical renaming.
|
||||
|
||||
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2 — it carries the full mapping table and before-and-after code for everything below. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
|
||||
|
||||
Then work through the provided code looking for each of these.
|
||||
|
||||
IMPORTS AND CONSTRUCTION
|
||||
- `MCPServer`, and `Context`, `Image`, `Audio`, `Message` imported from `mcp.server.mcpserver`
|
||||
- `mcp_types` imports — these are UNCHANGED. FastMCP 4 builds on the same SDK v2, so leave them alone and say so.
|
||||
|
||||
DECORATORS
|
||||
- `@server.tool()`, `@server.prompt()` — FastMCP takes a bare `@mcp.tool` / `@mcp.prompt` (and still accepts the called form)
|
||||
- `@server.resource(...)`, `@server.completion()`, `@server.custom_route(...)`
|
||||
|
||||
TRANSPORT
|
||||
- `run(transport="streamable-http")` — FastMCP names this transport "http"
|
||||
- `streamable_http_app()`, `sse_app()`
|
||||
|
||||
CONSTRUCTOR ARGUMENTS THAT DO NOT CARRY OVER
|
||||
- `debug=`, `log_level=`
|
||||
- `warn_on_duplicate_tools=` / `_resources=` / `_prompts=`
|
||||
- `dependencies=`
|
||||
- `title=`, `description=`
|
||||
- `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` — FastMCP consolidates all three into one `auth=` provider
|
||||
- `cache_hints=`
|
||||
- `extensions=`
|
||||
- `tools=[...]` (rare — the SDK's `Tool` type is not exported): FastMCP takes plain callables, so pass the underlying functions
|
||||
These raise TypeError, most naming their replacement. `name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` carry over unchanged.
|
||||
|
||||
CONTEXT — these ten properties do NOT exist on FastMCP's Context and raise AttributeError if you only swap the import:
|
||||
- `ctx.mcp_server` → `ctx.fastmcp`
|
||||
- `ctx.headers` → `get_http_headers()` from `fastmcp.server.dependencies` (a function, not a property)
|
||||
- `ctx.protocol_version` → `ctx.request_context.protocol_version`
|
||||
- `ctx.client_capabilities` → read it off `ctx.session` / `ctx.request_context`
|
||||
- `ctx.notify_tools_changed()`, `notify_resources_changed()`, `notify_prompts_changed()`, `notify_resource_updated()` → `ctx.send_notification(...)` with the matching `mcp_types` notification. FastMCP emits the list-changed ones for you when components change visibility through `ctx.enable_components` / `ctx.disable_components`.
|
||||
- `ctx.elicit_url` → not the same thing as `ctx.elicit` (that one is form elicitation, with a different signature and wire behavior). The URL flow survives on the raw session as `ctx.session.elicit_url(...)` — use that rather than deleting an OAuth or payment handoff.
|
||||
- `ctx.close_standalone_sse_stream` → no public FastMCP equivalent, and NOT on `ctx.request_context`. Flag it for the user.
|
||||
These four exist on both but with DIFFERENT signatures, so a bare import swap compiles and then fails at runtime:
|
||||
- `ctx.log(level, data)` → `ctx.log(message, level=...)` — the first positional argument is now the message, not the level
|
||||
- `ctx.info(data)` / `debug` / `warning` / `error` → these take `message` as a string, where the SDK accepted any JSON-serializable `data`
|
||||
- `ctx.elicit(message, schema=Model)` → `ctx.elicit(message, response_type=Model)` — the keyword was renamed
|
||||
- `ctx.read_resource(uri)` → still takes a URI, but returns a `ResourceResult` whose payload is under `.contents`, where the SDK returned an iterable of content objects directly. Code that iterates or indexes the return value needs updating.
|
||||
|
||||
Genuinely unchanged: `report_progress`, `request_id`, `client_id`, `input_responses`, `request_state`, `session`, and `request_context`.
|
||||
|
||||
RESOLVERS — the one part that is not a rename, so check for it first
|
||||
- any `Annotated[T, Resolve(fn)]` parameter, and the resolvers behind it
|
||||
- resolvers returning `Elicit[...]`, `Sample`, or `ListRoots`
|
||||
FastMCP has no resolver injection, but the underlying requests survive in a different shape: on a modern connection `Elicit`, `Sample`, and `ListRoots` all ride the guard pattern, where the tool returns an `InputRequiredResult` and the client answers on the next call. Do not tell the user these capabilities are simply unavailable. Flag every resolver with the guide's per-capability reasoning (server-side LLM call is usually better than guard-routed sampling; roots are often simplest as ordinary tool arguments) rather than picking a rewrite yourself. Also note that a resolved parameter is hidden from the tool's input schema, so replacing it with an ordinary argument changes the schema clients see.
|
||||
|
||||
For each item found, show the original code, name what changed, and give the FastMCP equivalent from the guide. Call out anything you could not find a documented replacement for instead of inventing one.
|
||||
</Prompt>
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
```
|
||||
|
||||
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](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
|
||||
FastMCP 4 depends on the MCP SDK v2, so nothing you already import from `mcp_types` moves. That is the practical benefit of migrating at this version rather than an earlier one: you and FastMCP are on the same protocol layer, with the same snake_case field names and the same type package, so the migration touches only the server API.
|
||||
|
||||
## The Mechanical Part
|
||||
|
||||
Most of the work is renaming. This table covers the surfaces a typical `MCPServer` server touches:
|
||||
|
||||
| MCP SDK v2 | FastMCP |
|
||||
|---|---|
|
||||
| `from mcp.server.mcpserver import MCPServer` | `from fastmcp import FastMCP` |
|
||||
| `from mcp.server.mcpserver import Context` | `from fastmcp import Context` |
|
||||
| `from mcp.server.mcpserver import Image, Audio` | `from fastmcp.utilities.types import Image, Audio` |
|
||||
| `from mcp.server.mcpserver.prompts.base import Message` | `from fastmcp.prompts import Message` |
|
||||
| `@server.tool()` | `@mcp.tool` |
|
||||
| `@server.prompt()` | `@mcp.prompt` |
|
||||
| `@server.resource("uri://x")` | `@mcp.resource("uri://x")` |
|
||||
| `@server.completion()` | `@mcp.completion` |
|
||||
| `@server.custom_route(path, methods)` | `@mcp.custom_route(path, methods)` |
|
||||
| `server.run(transport="streamable-http")` | `mcp.run(transport="http")` |
|
||||
| `server.streamable_http_app()` | `mcp.http_app()` |
|
||||
| `server.sse_app()` | `mcp.http_app(transport="sse")` |
|
||||
| `ctx.mcp_server` | `ctx.fastmcp` |
|
||||
| `ctx.headers` | `get_http_headers()` from `fastmcp.server.dependencies` |
|
||||
| `ctx.protocol_version` | `ctx.request_context.protocol_version` |
|
||||
| `ctx.client_capabilities` | read it off `ctx.session` |
|
||||
| `from mcp_types import X` | unchanged |
|
||||
|
||||
Two of these are worth a sentence each. The decorators lose their parentheses: `MCPServer` required `@server.tool()` and raised a `TypeError` telling you so if you wrote `@server.tool`, while FastMCP accepts both forms, so `@mcp.tool` is the idiomatic spelling and `@mcp.tool()` keeps working if you'd rather not touch every line. And the streamable HTTP transport is named `"http"` in FastMCP rather than `"streamable-http"` — the transport is the same, and `mcp.run()` still defaults to stdio.
|
||||
|
||||
Here is a complete server before and after. Nothing in the logic changes:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Before
|
||||
import json
|
||||
from mcp.server.mcpserver import MCPServer, Context
|
||||
|
||||
server = MCPServer("demo")
|
||||
|
||||
@server.tool()
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone by name"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@server.tool()
|
||||
async def process(items: list[str], ctx: Context) -> str:
|
||||
"""Process a batch of items"""
|
||||
for i, item in enumerate(items):
|
||||
await ctx.report_progress(i, len(items))
|
||||
return f"Processed {len(items)} items"
|
||||
|
||||
@server.resource("config://app", mime_type="application/json")
|
||||
def app_config() -> str:
|
||||
"""Application configuration"""
|
||||
return json.dumps({"debug": False})
|
||||
|
||||
@server.resource("users://{user_id}/profile")
|
||||
def profile(user_id: str) -> str:
|
||||
"""User profile by ID"""
|
||||
return json.dumps({"id": user_id})
|
||||
|
||||
@server.prompt()
|
||||
def summarize(text: str) -> str:
|
||||
"""Summarize text"""
|
||||
return f"Summarize:\n\n{text}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
server.run(transport="streamable-http")
|
||||
```
|
||||
|
||||
```python After
|
||||
import json
|
||||
from fastmcp import FastMCP, Context
|
||||
|
||||
mcp = FastMCP("demo")
|
||||
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone by name"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@mcp.tool
|
||||
async def process(items: list[str], ctx: Context) -> str:
|
||||
"""Process a batch of items"""
|
||||
for i, item in enumerate(items):
|
||||
await ctx.report_progress(i, len(items))
|
||||
return f"Processed {len(items)} items"
|
||||
|
||||
@mcp.resource("config://app", mime_type="application/json")
|
||||
def app_config() -> str:
|
||||
"""Application configuration"""
|
||||
return json.dumps({"debug": False})
|
||||
|
||||
@mcp.resource("users://{user_id}/profile")
|
||||
def profile(user_id: str) -> str:
|
||||
"""User profile by ID"""
|
||||
return json.dumps({"id": user_id})
|
||||
|
||||
@mcp.prompt
|
||||
def summarize(text: str) -> str:
|
||||
"""Summarize text"""
|
||||
return f"Summarize:\n\n{text}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="http")
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Constructor Arguments
|
||||
|
||||
`FastMCP()` describes your server's identity and behavior; how it gets deployed is decided when you serve it. Several `MCPServer` constructor arguments move accordingly, and each raises a `TypeError` naming its replacement rather than being silently ignored.
|
||||
|
||||
`name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` all mean what they meant before. The rest map like this:
|
||||
|
||||
| `MCPServer(...)` | FastMCP |
|
||||
|---|---|
|
||||
| `debug=True` | `FASTMCP_DEBUG` environment variable |
|
||||
| `log_level="DEBUG"` | `run_http_async(log_level=...)` or `FASTMCP_LOG_LEVEL` |
|
||||
| `warn_on_duplicate_tools`, `_resources`, `_prompts` | a single `on_duplicate=` |
|
||||
| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
|
||||
| `title=`, `description=` | `instructions=` |
|
||||
| `tools=[Tool, ...]` | `tools=[callable, ...]`, or FastMCP's own `Tool` |
|
||||
| `resources=[Resource, ...]` | no constructor keyword — register with `@mcp.resource` or `mcp.add_resource()` |
|
||||
| `subscriptions=<SubscriptionBus>` | no equivalent — see below |
|
||||
| `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` | a single `auth=` provider |
|
||||
| `cache_hints={...}` | `cache_ttl=`, `cache_scope=` |
|
||||
| `extensions=[...]` | `mcp.add_extension(...)` |
|
||||
|
||||
Authentication is the largest of these, and it consolidates rather than moves. `MCPServer` exposes the SDK's raw auth plumbing — a token verifier, an authorization-server provider, and an `AuthSettings` object, configured separately. FastMCP takes one `auth=` provider that carries the whole configuration, and ships providers for the common cases: `JWTVerifier` for validating tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider that lacks Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, WorkOS, and others. See [Authentication](/servers/auth/authentication).
|
||||
|
||||
Two rows are worth reading before you delete the argument. `resources=` has no constructor equivalent, so pre-built `Resource` objects need registering through `@mcp.resource` or `mcp.add_resource()` instead — dropping the keyword silently drops the resources with it. And `subscriptions=`, which an `MCPServer` uses to plug in an external pub/sub bus so resource-update notifications reach clients across replicas, has no FastMCP equivalent at all. A multi-replica deployment that relies on it should confirm it can live without cross-replica subscription fan-out before migrating, because a mechanical rename removes that behavior without any error to warn you.
|
||||
|
||||
### Serving HTTP
|
||||
|
||||
Renaming `streamable_http_app()` to `http_app()` is only mechanical for a call with no arguments. The keywords were renamed and regrouped, so an existing call carries arguments `http_app()` does not accept:
|
||||
|
||||
| SDK v2 | FastMCP |
|
||||
|---|---|
|
||||
| `streamable_http_app(streamable_http_path=...)` | `http_app(path=...)` |
|
||||
| `sse_app(sse_path=...)` | `http_app(path=..., transport="sse")` |
|
||||
| `sse_app(message_path=...)` | no equivalent |
|
||||
| `transport_security=TransportSecuritySettings(...)` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` |
|
||||
| `host=...` | pass to `mcp.run(host=...)` instead |
|
||||
|
||||
`json_response`, `stateless_http`, `event_store`, and `retry_interval` keep their names. See [Deploying HTTP servers](/deployment/http) for the host and origin settings.
|
||||
|
||||
### Stricter Arguments
|
||||
|
||||
One behavior change survives the rename and is worth knowing before you migrate. `MCPServer` binds the arguments it recognizes and ignores the rest, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on every generated schema and enforces it, so the same call fails:
|
||||
|
||||
```python test="skip"
|
||||
# Against MCPServer: succeeds, "extra" ignored.
|
||||
# Against FastMCP: raises, "extra" is not a parameter of greet().
|
||||
await client.call_tool("greet", {"name": "World", "extra": "surprise"})
|
||||
```
|
||||
|
||||
For most servers this is an improvement that costs nothing — a caller sending keys your tool never reads was already a bug. It matters if a client in your fleet passes extra metadata alongside real arguments, since those calls start failing the moment you migrate. Accept the extras explicitly as optional parameters if you need to keep them working.
|
||||
|
||||
## Asking for Input
|
||||
|
||||
This is the one part of the migration that is not a rename, so read it before you start if your tools use resolvers.
|
||||
|
||||
`MCPServer` asks the client for things through dependency-injection resolvers. A tool parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` before the tool body, and the resolver can return a request marker — `Elicit[T]` to ask the user, `Sample` to borrow the client's model, `ListRoots` to fetch its roots — which the framework turns into the right wire interaction for whichever protocol era the connection negotiated:
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel
|
||||
from mcp.server.mcpserver import MCPServer, Resolve, Elicit
|
||||
|
||||
server = MCPServer("booking")
|
||||
|
||||
|
||||
class Destination(BaseModel):
|
||||
destination: str
|
||||
|
||||
|
||||
def ask_destination() -> Elicit[Destination]:
|
||||
return Elicit("Where would you like to fly?", Destination)
|
||||
|
||||
|
||||
@server.tool()
|
||||
def book_flight(dest: Annotated[Destination, Resolve(ask_destination)]) -> str:
|
||||
"""Book a flight"""
|
||||
return f"Booked to {dest.destination}"
|
||||
```
|
||||
|
||||
FastMCP has no equivalent annotation, and it makes the protocol era explicit instead of hiding it. Which replacement you want depends on which era your clients speak.
|
||||
|
||||
On **handshake-era connections** (≤ 2025-11-25), a running tool asks the user directly with `ctx.elicit()`, and the call blocks until the answer arrives. Where the resolver returned a value or aborted the call, `ctx.elicit()` hands you the outcome to branch on, so declining and cancelling become cases your tool answers for itself:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP, Context
|
||||
|
||||
mcp = FastMCP("booking")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def book_flight(ctx: Context) -> str:
|
||||
"""Book a flight"""
|
||||
result = await ctx.elicit("Where would you like to fly?", response_type=str)
|
||||
if result.action == "accept":
|
||||
return f"Booked to {result.data}"
|
||||
return "Booking cancelled"
|
||||
```
|
||||
|
||||
On the **modern protocol** (2026-07-28), server-initiated requests are gone from the wire, so a tool asks by *returning* a description of what it needs. The client answers and calls the tool again with the answer attached, and the tool re-runs from the top. This is the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), and it reads the answers off `ctx.input_responses`.
|
||||
|
||||
The two are era-gated in both directions: `ctx.elicit()` raises on a modern connection, and a guard result raises on a handshake one. A server that must serve both branches on `ctx.request_context.protocol_version`. See [Elicitation](/servers/elicitation#which-approach-to-use) for both shapes side by side.
|
||||
|
||||
Resolvers that return `Sample` or `ListRoots` have no *injected* equivalent — FastMCP has no `ctx.sample()` or `ctx.list_roots()` — but the underlying request survives, so this is a change of shape rather than a loss of capability. On a modern connection both ride the same guard pattern as elicitation: the tool returns an `InputRequiredResult` describing the sampling or roots request, and the client answers on the next call.
|
||||
|
||||
Which shape you want differs by capability. For **roots**, the guard route is the natural replacement, since one round buys the whole answer — and taking the paths as ordinary tool arguments is simpler still whenever the caller can supply them. For **generation**, prefer [calling an LLM from your server](/servers/sampling) with your own API key: your tool then behaves identically for every client, including the many that never implemented sampling, and you avoid paying a full request-response cycle per generation step. Reach for the guard route when using the *caller's* model is specifically the point.
|
||||
|
||||
One schema detail is easy to miss during the rewrite. A resolved parameter never appears in the tool's input schema — `book_flight` above advertises no arguments at all. When you replace a resolver with an explicit tool argument, the schema the client sees gains a field, which is usually what you want but is a visible change to your tool's contract.
|
||||
|
||||
## What You Gain
|
||||
|
||||
The migration is worth doing for what sits on the other side of it. FastMCP is a framework rather than a protocol surface, and these are the capabilities that most often motivate the move:
|
||||
|
||||
[Server composition](/servers/composition) mounts one server inside another, so a large surface splits into modules that are developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control. The [OpenAPI integration](/integrations/openapi) generates a whole server from an existing API specification. [Tool transformation](/servers/transforms/transforms) rewrites the tools a server exposes — renaming, hiding, and reshaping arguments — without touching the code that defines them.
|
||||
|
||||
FastMCP also ships a [client](/clients/client), which `MCPServer` has no counterpart for. It speaks every transport, drives both protocol eras, and connects to a server object in-process — so [testing](/servers/testing) a server means calling its tools in the same Python process, with no subprocess and no network.
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
---
|
||||
title: Upgrading from the MCP SDK
|
||||
sidebarTitle: "From MCP SDK"
|
||||
description: Upgrade from FastMCP in the MCP Python SDK to the standalone FastMCP framework
|
||||
icon: up
|
||||
---
|
||||
|
||||
If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.**
|
||||
|
||||
```python
|
||||
# Before
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# After
|
||||
from fastmcp import FastMCP
|
||||
```
|
||||
|
||||
That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is.
|
||||
|
||||
<Tip>
|
||||
**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes.
|
||||
</Tip>
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install --upgrade fastmcp
|
||||
# or
|
||||
uv add fastmcp
|
||||
```
|
||||
|
||||
FastMCP includes the `mcp` package as a dependency, so you don't lose access to anything. Update your import, run your server, and if your tools work, you're done.
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from FastMCP 1.0 (bundled in the `mcp` package v1) to standalone FastMCP 4. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
|
||||
STEP 1 — IMPORT (required for all servers):
|
||||
Change "from mcp.server.fastmcp import FastMCP" to "from fastmcp import FastMCP".
|
||||
|
||||
STEP 2 — CONSTRUCTOR KWARGS (only if FastMCP() receives transport settings):
|
||||
FastMCP() no longer accepts: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http.
|
||||
Fix: pass these to run() instead.
|
||||
Before: `mcp = FastMCP("server", host="0.0.0.0", port=8080); mcp.run()`
|
||||
After: `mcp = FastMCP("server"); mcp.run(transport="http", host="0.0.0.0", port=8080)`
|
||||
|
||||
STEP 3 — PROMPTS (only if using PromptMessage directly or returning dicts):
|
||||
mcp.types.PromptMessage is replaced by fastmcp.prompts.Message.
|
||||
Before: `PromptMessage(role="user", content=TextContent(type="text", text="Hello"))`
|
||||
After: `Message("Hello")` — role defaults to "user", accepts plain strings.
|
||||
Also: if prompts return raw dicts like `{"role": "user", "content": "..."}`, these must become Message objects or plain strings.
|
||||
The MCP SDK's FastMCP 1.0 silently coerced dicts; standalone FastMCP requires typed returns.
|
||||
|
||||
STEP 4 — OTHER MCP IMPORTS (only if importing from mcp.* directly):
|
||||
FastMCP now builds on MCP SDK v2, which removed the `mcp.types` module — protocol types live in the standalone `mcp_types` package. Update any `from mcp.types import X` to `from mcp_types import X`. Prefer FastMCP's own APIs where equivalents exist:
|
||||
- mcp_types.TextContent for tool returns → just return plain Python values (str, int, dict, etc.)
|
||||
- mcp_types.ImageContent → fastmcp.utilities.types.Image
|
||||
- from mcp.server.stdio import stdio_server → not needed, mcp.run() handles transport
|
||||
|
||||
STEP 5 — DECORATORS (only if treating decorated functions as objects):
|
||||
@mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name or .description on the decorated result needs updating. Set FASTMCP_DECORATOR_MODE=object temporarily to restore v1 behavior (this compat setting is itself deprecated).
|
||||
|
||||
For each issue found, show the original line, explain what changed, and provide the corrected code.
|
||||
</Prompt>
|
||||
|
||||
## What Might Need Updating
|
||||
|
||||
Most servers need nothing beyond the import change. Skim the sections below to see if any apply.
|
||||
|
||||
### Constructor Settings
|
||||
|
||||
If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed:
|
||||
|
||||
```python
|
||||
# Before
|
||||
mcp = FastMCP("my-server", host="0.0.0.0", port=8080)
|
||||
mcp.run()
|
||||
|
||||
# After
|
||||
mcp = FastMCP("my-server")
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8080)
|
||||
```
|
||||
|
||||
If you pass the old kwargs, you'll get a clear `TypeError` with a migration hint.
|
||||
|
||||
### Prompts
|
||||
|
||||
If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, you'll need to upgrade to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. The MCP SDK's bundled FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("prompts")
|
||||
|
||||
@mcp.prompt
|
||||
def review(code: str) -> str:
|
||||
"""Review code for issues"""
|
||||
return f"Please review this code:\n\n{code}"
|
||||
```
|
||||
|
||||
For multi-turn prompts:
|
||||
|
||||
```python
|
||||
from fastmcp.prompts import Message
|
||||
|
||||
@mcp.prompt
|
||||
def debug(error: str) -> list[Message]:
|
||||
"""Start a debugging session"""
|
||||
return [
|
||||
Message(f"I'm seeing this error:\n\n{error}"),
|
||||
Message("I'll help debug that. Can you share the relevant code?", role="assistant"),
|
||||
]
|
||||
```
|
||||
|
||||
### Other `mcp.*` Imports
|
||||
|
||||
FastMCP now builds on MCP SDK v2. The `mcp.types` module no longer exists — protocol types moved to a standalone `mcp_types` package, and the field names were renamed from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). Update `from mcp.types import X` to `from mcp_types import X`. For the full picture, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3).
|
||||
|
||||
Where FastMCP provides its own API for the same thing, it's worth switching over:
|
||||
|
||||
| mcp Package | FastMCP Equivalent |
|
||||
|---|---|
|
||||
| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool |
|
||||
| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` |
|
||||
| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |
|
||||
| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |
|
||||
|
||||
For protocol types without a FastMCP equivalent, import them from `mcp_types` directly.
|
||||
|
||||
### Decorated Functions
|
||||
|
||||
In FastMCP 1.0, `@mcp.tool` returned a `FunctionTool` object. Now decorators return your original function unchanged — so decorated functions stay callable for testing, reuse, and composition:
|
||||
|
||||
```python
|
||||
@mcp.tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# This works now — the function is still a regular function
|
||||
assert greet("World") == "Hello, World!"
|
||||
```
|
||||
|
||||
If you have code that accesses `.name`, `.description`, or other attributes on the decorated result, that will need updating. This is uncommon — most servers don't interact with the tool object directly. If you need the old behavior temporarily, set `FASTMCP_DECORATOR_MODE=object` to restore it (this compatibility setting is itself deprecated and will be removed in a future release).
|
||||
|
||||
## Verify the Upgrade
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pip install --upgrade fastmcp
|
||||
|
||||
# Check version
|
||||
fastmcp version
|
||||
|
||||
# Run your server
|
||||
python my_server.py
|
||||
```
|
||||
|
||||
You can also inspect your server's registered components with the FastMCP CLI:
|
||||
|
||||
```bash
|
||||
fastmcp inspect my_server.py
|
||||
```
|
||||
|
||||
## Looking Ahead
|
||||
|
||||
The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change.
|
||||
|
|
@ -27,7 +27,7 @@ Elicitation reaches the user two different ways, depending on the protocol era t
|
|||
- **On handshake-era connections (≤ 2025-11-25)**, a running tool calls [`ctx.elicit()`](#requesting-input-on-handshake-connections). The tool pauses mid-execution, the server sends a request over the session back-channel, and the tool resumes with the answer. This is the original elicitation API and the rest of this page's first half covers it in full.
|
||||
- **On the modern protocol (2026-07-28)**, that back-channel is gone — server-initiated requests were removed from the wire (SEP-2577), so a tool cannot issue a request mid-execution and block on the answer. Instead a tool asks for input by *returning* a description of what it needs; each round completes normally and the client issues a new call with the answer attached. This is the [guard pattern](#elicitation-on-the-modern-protocol), covered in the second half.
|
||||
|
||||
The era gate is strict: `ctx.elicit()` only works on handshake connections, and the guard pattern only works on modern ones. A tool that returns a guard result on a handshake connection — or calls `ctx.elicit()` on a modern one — raises a clear era error rather than failing obscurely. A server that serves both eras may need both paths; branch on `ctx.protocol_version` to pick the right one. `fastmcp.Client` drives whichever the connection negotiated automatically.
|
||||
The era gate is strict: `ctx.elicit()` only works on handshake connections, and the guard pattern only works on modern ones. A tool that returns a guard result on a handshake connection — or calls `ctx.elicit()` on a modern one — raises a clear era error rather than failing obscurely. A server that serves both eras may need both paths; branch on `ctx.request_context.protocol_version` to pick the right one. `fastmcp.Client` drives whichever the connection negotiated automatically.
|
||||
|
||||
## Requesting input on handshake connections
|
||||
|
||||
|
|
@ -539,7 +539,7 @@ connection negotiated '2025-11-25'. Use ctx.elicit() for server-initiated input
|
|||
on handshake-era connections.
|
||||
```
|
||||
|
||||
If you need to support both eras, branch on `ctx.protocol_version`: return an `InputRequiredResult` on modern connections and fall back to [`ctx.elicit()`](#requesting-input-on-handshake-connections) on handshake-era ones.
|
||||
If you need to support both eras, branch on `ctx.request_context.protocol_version`: return an `InputRequiredResult` on modern connections and fall back to [`ctx.elicit()`](#requesting-input-on-handshake-connections) on handshake-era ones.
|
||||
|
||||
### Prompts and resources
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue