FastMCP 4 is GA. If you have an MCP server or client on fastmcp 3.x, you'll
upgrade soon. Most of it is painless — FastMCP(...), @mcp.tool, and
mcp.run(transport=...) are all unchanged. The parts that aren't painless are the
parts that don't announce themselves.
These are field notes on top of the official
Upgrading from FastMCP 3
guide — the items that bit hardest when I moved one MCP server and two clients,
in the order they bit.
1. pip install -U fastmcp can leave you half-broken
FastMCP 4 is split into extras. The fastmcp package is now a thin meta-package
that depends on fastmcp-slim[client,server]; fastmcp-slim carries the actual
code, and its extras are client, server, mcp, anthropic, apps, azure,
code-mode, gemini, openai.
On a fresh install this is invisible — pip install fastmcp pulls
fastmcp-slim[client,server] and everything works.
I upgraded in place with pip install -U fastmcp over fastmcp 3.2.x, and pip
did not re-resolve those base extras. Result: an importable shell with nothing in
it.
>>> import fastmcp
>>> fastmcp.__file__ is None
True
>>> dir(fastmcp)
[]
>>> from fastmcp import Client
ImportError: cannot import name 'Client' from 'fastmcp' (unknown location)
This looks exactly like a broken release. It isn't — it's the 4.x extras split not
getting re-resolved on an in-place upgrade. (FastMCP separately documents a
different pip file-manifest issue on the 3.2 → 3.3 hop and notes uv is
unaffected by that one; this is a distinct problem, and I hit it with pip -U —
I didn't test uv pip install -U.)
The fix, either way:
python -m pip uninstall -y fastmcp fastmcp-slim
python -m pip install fastmcp # or fastmcp==4.0.x to pin the version you tested
Or just recreate the venv. It cost me a false-alarm debugging session — twice,
because the symptom (ModuleNotFoundError on a submodule that's genuinely in the
wheel) is so convincing.
Also: fastmcp in 4.x no longer exposes __version__. If you assert on it
anywhere, switch to importlib.metadata.version("fastmcp").
2. httpx → httpx2: your except clauses go quiet
FastMCP 4 dropped httpx for httpx2 (a next-gen fork) internally. So a FastMCP
client call that used to raise httpx.ConnectError now raises
httpx2.ConnectError.
The trap: httpx is still transitively installed in most environments, so this
keeps importing and type-checking:
try:
async with Client(StreamableHttpTransport(url)) as c:
result = await c.call_tool("do_thing", args)
except httpx.ConnectError: # never matches on FastMCP 4
...
It just silently stops catching. Grep for except httpx. and check whether each
one wraps a FastMCP Client / transport call — if it does, migrate it to
httpx2 (or catch FastMCP's own fastmcp.exceptions.ToolError, which is usually
what you actually want). Your own direct httpx calls are unaffected as long as
you keep httpx as a dependency.
Same silent class, elsewhere: anything you hand into FastMCP that's built on
httpx — a custom httpx_client_factory, an httpx.AsyncClient passed to a
transport, an httpx.Auth — now needs to be httpx2. The official guide lists
this right next to the except trap.
One more downstream effect: TLS verification now uses the OS trust store via
truststore (honouring SSL_CERT_FILE / SSL_CERT_DIR) instead of bundled
certifi — corporate-CA setups may verify differently. HTTP log records also move
from httpx / httpcore.* to httpx2 / httpcore2.* — update logging filters.
3. Client now defaults to mode="auto"
In 4.x, Client(...) defaults to mode="auto" and negotiates the modern
2026-07-28 protocol era. That era is sessionless, and it changes runtime
behaviour even though your code compiles fine:
-
No
on_initializehandshake — middleware / init hooks tied to it never run. -
ctx.set_state()doesn't persist to the next call. -
ctx.elicit()raises — the modern era has no server-initiated back-channel.
If your client only does plain reads and writes (call_tool, read_resource),
you're fine — that's the common case and it needs no change. If it relies on
session state, an init hook, or elicitation, pin it back:
Client(server, mode="legacy")
StreamableHttpTransport also dropped sse_read_timeout= — pass timeout= on the
Client instead.
4. Removed ctx methods
These are gone and raise AttributeError:
ctx.sample()ctx.sample_step()ctx.list_roots()
If your server's job was to borrow the caller's model via ctx.sample() (or
FastMCP(sampling_handler=...), also removed), you either call an LLM directly
from the server now or stay on 3.x. ctx.elicit() still exists but requires a
response_type argument and raises on modern connections — rewrite it as a guard
tool that returns an "input required" result, or branch on
ctx.request_context.protocol_version.
Background tasks moved to an extension. @mcp.tool(task=True) no longer runs
anything by itself — install fastmcp[tasks] and register
mcp.add_extension(TasksExtension()), or startup raises. Drop task= from
@mcp.resource / @mcp.prompt (tools only).
5. Version floors
# hard requirement — resolution fails without it
pydantic = ">=2.12"
# only if you use the server's FastAPI extra
starlette = ">=1.0.1" # → FastAPI >= 0.133.0 (first version admitting Starlette 1.x)
Pin style unchanged: an app pins the exact version it tested
(fastmcp==4.0.x); a library floors at fastmcp>=4.0.0 in its own
dependencies and tests against the current release.
6. Import moves (quick reference)
| 3.x | 4.x |
|---|---|
from fastmcp.tools.tool import Tool, ToolResult |
from fastmcp.tools import Tool, ToolResult |
from fastmcp.resources.resource import Resource |
from fastmcp.resources import Resource |
TextContent, Tool protocol types from fastmcp.types
|
from mcp.types import ... (fastmcp.types now holds only FastMCP-defined types) |
mcp.as_proxy(sub) |
create_proxy(sub) from fastmcp.server
|
mcp.import_server(sub) |
mcp.mount(sub) (live composition, not a snapshot) |
mcp.add_tool_transformation(name, cfg) |
mcp.add_transform(ToolTransform({name: cfg})) |
CachableToolResult (old typo) |
CacheableToolResult — no compat alias |
McpError(ErrorData(code=..., message=...)) |
McpError(code=..., message=...) |
SDK v2 also renamed model fields camelCase → snake_case (inputSchema →
input_schema, isError → is_error). Old reads are auto-bridged and emit a
FastMCPDeprecationWarning. The bridge is
fastmcp.settings.mcp_camelcase_compat (env FASTMCP_MCP_CAMELCASE_COMPAT),
bool, default true. Set it false once — that turns every remaining camelCase
read into a hard error, so you can find and clear them before the bridge is
removed.
7. New defaults from the settings page
gofastmcp.com/more/settings lists every
setting — each has a fastmcp.settings.<name> attribute and a FASTMCP_<NAME>
environment variable. Three defaults changed behaviour in 4.x and don't get a
line in the upgrade guide:
-
telemetry_modedefaults to"native"— FastMCP 4 auto-instruments OpenTelemetry spans for MCP calls. If you don't want that,FASTMCP_TELEMETRY_MODE=off(orpropagation_only). -
check_for_updatesdefaults to"stable"— the CLI checks PyPI for a newer FastMCP on startup. SetFASTMCP_CHECK_FOR_UPDATES=offin CI and containers. -
client_raise_first_exceptiongroup_errordefaults totrue— a client error surfaces as the first underlying exception, not theExceptionGroup. That's whyexcept ToolError:still works; if you were catching withexcept*, revisit.
Also worth a look while you're there: stateless_http (new-transport-per-request,
the sessionless/Cloud-Run knob), http_host_origin_protection (new, opt-in Host/
Origin validation for Streamable HTTP), and mask_error_details (default false
— error text is passed through unless you raise an explicit ToolError /
ResourceError / PromptError).
Keep FASTMCP_DEPRECATION_WARNINGS=true (the default) for the whole migration —
it's how you find the rest of this list in your own code.
The good news: the minimal server barely changes
If your server is only @mcp.tool-decorated functions plus
mcp.run(transport="stdio") or mcp.run(transport="streamable-http"), there is
no code change. The constructor, the decorator, and the transport call are all
the same. You:
- bump
pydantic(and FastAPI, if you use it), - grep for
except httpx.and migrate the ones around FastMCP calls, - run your tests.
That's it.
What I actually changed
| Shape | Change |
|---|---|
An MCP server — @mcp.tool + mcp.run("stdio" / "streamable-http")
|
dependency floor only — zero code |
Two MCP clients — Client + StreamableHttpTransport + except ToolError
|
dependency floor only — verified mode="auto" is fine for plain reads / writes |
No API changes in either. The real cost was the pip install -U false alarm
(twice) and one test that hard-coded a version string in an assertion.
The checklist
[ ] Recreate the venv (or `pip3 uninstall fastmcp fastmcp-slim` first) — don't `-U` over 3.x
[ ] pydantic >= 2.12 (+ FastAPI >= 0.133.0 if you use the server's FastAPI extra)
[ ] grep `except httpx.` — migrate the ones wrapping FastMCP Client/transport calls to httpx2
[ ] grep `httpx_client_factory` / `httpx.AsyncClient` / `httpx.Auth` handed to FastMCP — same, → httpx2
[ ] grep `ctx.sample` / `ctx.sample_step` / `ctx.list_roots` — removed (and `FastMCP(sampling_handler=)`)
[ ] grep `ctx.elicit` — needs response_type + fails on modern connections
[ ] grep `@mcp.tool(task=True)` — now needs fastmcp[tasks] + TasksExtension()
[ ] grep `Client(` — needs mode="legacy" only if it relies on session state / on_initialize / elicit
[ ] grep `sse_read_timeout` — moved to Client(timeout=...)
[ ] grep imports: fastmcp.tools.tool, fastmcp.resources.resource, fastmcp.types, mcp.as_proxy, import_server
[ ] grep `fastmcp.__version__` — gone; use importlib.metadata.version("fastmcp")
[ ] set `fastmcp.settings.mcp_camelcase_compat = False` once — clear the camelCase deprecation warnings
[ ] CI: `FASTMCP_CHECK_FOR_UPDATES=off`; decide on `FASTMCP_TELEMETRY_MODE` (default is `native` = OTel on)
[ ] keep `FASTMCP_DEPRECATION_WARNINGS=true` (default) for the whole migration
[ ] run the test suite
If you're just @mcp.tool + mcp.run, the whole list is "bump two floors and
check your httpx catches." Everything else is for the code that does more.
More reading
- Upgrading from FastMCP 3 — the official guide this checklist rides on.
-
FastMCP settings — every
fastmcp.settings.*/FASTMCP_*knob, including the §7 defaults. -
The MCP landscape — how MCP servers are built (the official SDKs, FastMCP) and how they run (
stdio, Streamable HTTP), and where the2026-07-28sessionless shift sits.
Top comments (0)