DEV Community

wolfejam.dev
wolfejam.dev Subscriber

Posted on

FastMCP 3 4 migration: the breaking changes that compile

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. httpxhttpx2: 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
    ...
Enter fullscreen mode Exit fullscreen mode

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_initialize handshake — 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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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, isErroris_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_mode defaults to "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 defaults to "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 defaults to 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.


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:

  1. bump pydantic (and FastAPI, if you use it),
  2. grep for except httpx. and migrate the ones around FastMCP calls,
  3. 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
Enter fullscreen mode Exit fullscreen mode

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 the 2026-07-28 sessionless shift sits.

Top comments (0)