On July 28, 2026, thousands of MCP servers broke without a single line of their own code changing. Anyone who ran a fresh pip install or uv sync after that date against an unbounded mcp>=1.x dependency silently resolved to the brand-new v2.0.0 of the official Python SDK — and their server died at import time with ModuleNotFoundError: No module named 'mcp.server.fastmcp'. The fix, for most servers, is a one-line version pin followed by a small, mechanical port. But the incident is worth a full post because of what it exposes: your MCP server's SDK is its own supply chain with major-bump discipline, and if your CI doesn't install from scratch against the latest resolver output on every run, you will learn about the next breaking major from your users, not your pipeline.
The triage, up front so nobody has to scroll: if your server imports FastMCP from mcp.server.fastmcp, pin mcp[cli]>=1.28,<2 today to stop the bleeding, then port from mcp.server.fastmcp import FastMCP to from mcp.server import MCPServer and lift the ceiling to mcp>=2,<3. Everything below is the receipt — what changed, why it bit so many projects at once, the four porting gotchas beyond the rename, and the CI guard that catches the next one.
The failure signature: a dead server and a lying client
The server-side symptom is unambiguous. Any environment that resolves the mcp package to 2.x while the code still imports the v1 path fails immediately:
from mcp.server.fastmcp import FastMCP
# ModuleNotFoundError: No module named 'mcp.server.fastmcp'What made this incident nastier than a normal breaking change is that the people who saw it first were usually not the server authors — they were the server's users, and what they saw was misleading. MCP clients surfaced the dead server only as a generic connection closed: initialize response failure with no tools loaded. If you operate a self-hosted MCP server and your agent client suddenly reports zero tools after a redeploy, check the server logs for that ModuleNotFoundError before you debug the client, the transport, or your network. The psquare-mcp project documented exactly this misdirection when it triaged its own crash.
The blast radius was wide because the failure mode was latent in every repo with an unbounded lower-bound pin. A partial roll call from public issue trackers and fix commits: obsidian-web-mcp (broken for everyone cloning its Home Assistant wrapper after release day), freecad-mcp, screener-mcp, mcp-ynab, token-saver, omniswarm, rhinomcp, apple_eventkit_mcp, clikernel, the tanglebrain delegate extra, devtime-ei, and mikrotik-mcp, whose Docker image crashed on startup. None of these projects changed anything. Their resolvers just started answering a different question than the one their code was written against.
What 2.0 actually changed: a spec-day major, not a drive-by rename
Version 2.0.0 shipped on July 28, 2026 alongside the 2026-07-28 revision of the Model Context Protocol specification — the same day, the same version number, deliberately. This was not a cosmetic rename; it was the SDK generation that implements the new spec wire format while keeping backward compatibility for 2025-era clients. That is precisely why there is no deprecation shim: v2 is a new line, and v1.x moves to maintenance mode receiving critical bug fixes and security patches only. The upstream README's recommended posture for unmigrated packages is an explicit >=1.28,<2 pin — a ceiling, stated plainly, not folklore.
The rename itself is mechanical:
| v1.x | v2.x |
|---|---|
from mcp.server.fastmcp import FastMCP | from mcp.server import MCPServer |
mcp.server.fastmcp module path | mcp.server.mcpserver module path |
mcp = FastMCP("demo") | mcp = MCPServer("demo") |
The decorator API — @mcp.tool() and friends — is otherwise unchanged, which is why most ports are small. But four projects in the migration wave hit real gotchas past the rename, so here they are in one place:
McpErrorbecameMCPError(mcp.shared.exceptions.McpError→mcp.shared.exceptions.MCPError). If your server raises protocol errors, the except clauses need the same rename or they will silently stop catching.- Protocol model fields went snake_case.
inputSchemabecomesinput_schemafor Python access. Any code that reads tool schemas or constructs protocol models with camelCase kwargs breaks. hostandportleft the constructor. They moved toMCPServer.run(). Passing them to the constructor the v1 way fails — one migration explicitly deleted itsinit_mcp()workaround because the new run signature made it unnecessary.- The dependency floor moved. v2 drops
pydantic-settingsalong with theMCP_*environment variables (which, candidly, never took effect in v1 either) and replaceshttpx/httpx-ssewithhttpx2>=2.5. Timeouts are now float seconds. If your server reads timeouts as integers or relies onMCP_*env config, audit both.
None of this is unreasonable for a major version. The unreasonable part was on our side: thousands of servers declared mcp>=1.0 with no upper bound, which is a standing instruction to the resolver to install a future breaking major sight unseen.
The five-minute triage: pin the ceiling, ship the patch release
If your server is broken right now, do the boring thing first. Cap the dependency below 2.0 in pyproject.toml:
dependencies = ["mcp[cli]>=1.28,<2"]That is the upstream-recommended pin for packages not yet migrated, and it is exactly what mcp-sequel, oebb-mcp-server, omniswarm, and half the roll call above shipped as their stopgap. Cut a patch release so the fixed bound is what installers actually resolve — several projects bumped their own version purely to push the pinned requirement out through the publish chain. Verify the way screener-mcp did: resolve clean and import both entry points (mcp==1.29.0 era: from mcp.server.fastmcp import FastMCP imports cleanly; unpinned resolves 2.0.0 and fails).
Two environment-specific notes worth stealing from the incident reports. First, if your users install via uvx with --with, publish the workaround form too: uvx --with "mcp[cli]<2" your-server unbreaks them before your patch release lands. Second, if you ship a container, pin in every manifest that resolves — mikrotik-mcp had to cap the bound in both pyproject.toml and its requirements file because the Docker build resolved independently.
The real port: twenty lines, both generations, one regression test
With the bleeding stopped, migrate. The core diff is the import and the constructor:
# before (v1.x)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("deploy")# after (v2.x)
from mcp.server import MCPServer
mcp = MCPServer("deploy")Then work the four gotchas from the previous section (MCPError, snake_case fields, host/port into run(), the httpx2 floor), following the official v1-to-v2 migration guide, and declare mcp>=2,<3 — note the new ceiling; today's unbounded mcp>=2 is tomorrow's identical incident. The ecosystem has already converged on this shape: changelogs across half a dozen servers read mcp>=2.0.0,<3.0.0, and the 2.x line itself is stable and moving (2.1.0/2.1.1 shipped August 25, 2026).
If you need to support both SDK generations during a transition — because your users pin either side — steal the pattern Espressif's esp-idf and DevTime both landed on: prefer MCPServer, fall back to FastMCP:
try:
from mcp.server import MCPServer as _Server
except ImportError: # mcp v1.x
from mcp.server.fastmcp import FastMCP as _Server
mcp = _Server("deploy")Verify the shim against both majors in CI, the way DevTime does, and add the regression test that makes a future rename loud instead of silent. mikrotik-mcp's version-bound test — asserting the declared upper bound still matches the SDK the code imports against — is five lines that would have turned this entire incident class into a red CI job instead of a user report. A bound without a test guarding it is a comment with extra steps.
The systemic fix: your CI must install from scratch against latest
Here is the detail from the incident that should worry you most. oebb-mcp-server's CI still looked green after 2.0.0 shipped — because its last run predated the release and resolved the old SDK. The sibling project with the identical dependency spec went red on an unrelated dependency PR that happened to re-resolve. Green CI meant "passes against the packages we resolved last month," not "passes against what a user installs today." Every project whose lockfile or cache survived release day had a passing badge on a broken server.
The fix belongs in CI, not in a postmortem:
- One job that installs from scratch against latest on every run — no lockfile, no cache — so a fresh-resolver breakage fails the build the day upstream ships it, not the day a user reports it. omniswarm's timeline is the cautionary version: its v1.2.0 CI ran July 22, six days before 2.0.0, and resolved fine.
- A version-bound regression test that fails when the declared ceiling and the imported API disagree, so the next rename breaks the build instead of the install.
- A ceiling on every major-track dependency, including the ones in extras and Docker manifests, not just the base requirement. tanglebrain broke through its
delegateextra; mikrotik-mcp broke through its container's separate resolve.
This is the actual lesson of the FastMCP rename. The rename was announced, versioned, and documented — a model major, really. The damage came from thousands of servers implicitly opting into it via unbounded pins, validated by CI that never re-resolved. A self-hosted deploy MCP server is infrastructure your agents program against; it deserves the same dependency discipline as the platform underneath it — pinned ceilings, fresh-resolve CI, and a test that screams before your users do.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



