Skip to main content

The MCP servers stopped starting, and our own install command was the cause

· 2 min read
Anchor maintainers

If python server.py gave you ModuleNotFoundError: No module named 'mcp.server.fastmcp', this was us.

All three Anchor MCP servers — model-fleet, anchor-prompts, and project-orchestrator — opened with:

from mcp.server.fastmcp import FastMCP

MCP Python SDK 2.0 removed that module and renamed FastMCP to MCPServer. Our pyproject.toml files declared mcp[cli]>=1.2.0 with no upper bound, so a fresh install resolved 2.x and every server died at import. Worse, the install command printed in each README and module docstring was the unbounded pip install "mcp[cli]" — we were handing people the command that broke them.

The fix

The decorator surface (tool, prompt, resource) and run() are unchanged across the rename, so this needed two names, not a rewrite:

try: # SDK 2.x
from mcp.server import MCPServer as MCPServerClass
except ImportError: # SDK 1.x
from mcp.server.fastmcp import FastMCP as MCPServerClass

The dependency is now bounded >=1.2.0,<3. A fresh install gets 2.x and works; an operator pinned to 1.x keeps working. Both were verified by importing all three servers against 1.29.0 and 2.0.0.

Why it took so long to notice

Two things hid it, and both are worth knowing about independently.

Nothing in CI installed the SDK. No test ever imported a server, so no test could fail.

The tests that should have covered it skipped. They used pytest.importorskip("mcp.server.fastmcp"), and since CI never had the SDK, that skip was permanent rather than situational — a test that never runs looks exactly like a test that passes.

What replaces them

tests/test_mcp_servers_import.py imports each server against the actually installed SDK, in a subprocess run from outside the repo so the directory shadowing cannot fool it, and asserts the child resolved a real site-packages mcp before trusting the result. It also fails if any server declares an unbounded SDK range again, or reaches for the removed module outside a guarded fallback. requirements-dev.txt now installs the SDK, so CI exercises it.

We kept a separate set of tests that stub the SDK — those cover the delegate gate without needing an install. They are useful and they could never have caught this: a stub satisfies the import no matter how broken the real path is. Both kinds are needed, for different jobs.