FastMCP 3 4 migration: the breaking changes that compile A developer migrating an MCP server and two clients from FastMCP 3 to FastMCP 4 documented several breaking changes that compile but fail at runtime. Key issues include a pip in-place upgrade leaving an importable shell with no code due to the new extras split, silent exception handling failures from the httpx to httpx2 switch, and new default behaviors in Client mode that alter runtime semantics. 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 https://gofastmcp.com/getting-started/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. python 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 handshake ctx.set state doesn't persist ctx.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 . 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. | 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. gofastmcp.com/more/settings https://gofastmcp.com/more/settings lists every setting — each has a fastmcp.settings.