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.
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").
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.
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:
on_initialize handshakectx.set_state() doesn't persistctx.elicit() raises
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.
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).
pydantic = ">=2.12"
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.
| 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 fromfastmcp.types |
from mcp.types import ... (fastmcp.types now holds only FastMCP-defined types) |
mcp.as_proxy(sub) |
create_proxy(sub) fromfastmcp.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.
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_mode``"native" β FastMCP 4 auto-instruments
OpenTelemetry spans for MCP calls. If you don't want that,
FASTMCP_TELEMETRY_MODE=off (or propagation_only). check_for_updates``"stable" β the CLI checks PyPI for a newer
FastMCP on startup. Set FASTMCP_CHECK_FOR_UPDATES=off in CI and containers.client_raise_first_exceptiongroup_error`` true β a client
error surfaces as the first underlying exception, not the ExceptionGroup.
That's why except ToolError: still works; if you were catching with except*,
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.
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:
pydantic (and FastAPI, if you use it),except httpx. and migrate the ones around FastMCP calls,
That's it.
| 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.
[ ] 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.
fastmcp.settings.* / FASTMCP_* knob, including the Β§7 defaults.stdio, Streamable HTTP), and where the 2026-07-28 sessionless shift sits.