FastMCP 3 4 migration: the breaking changes that compile Fa | Coderz Club

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, an

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, an

By Coderz Club · 2026-09-07 · Tags: ai

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) 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_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") 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 #

View this page on Coderz Club