cd /news/developer-tools/fastmcp-3-4-migration-the-breaking-c… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-122697] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

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.

read7 min views1 publishedSep 7, 2026

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.

── more in #developer-tools 4 stories Β· sorted by recency
simonwillison.net Β· Β· #developer-tools
llm 0.32.1
── more on @fastmcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/fastmcp-3-4-migratio…] indexed:0 read:7min 2026-09-07 Β· β€”