Point imports of mcp.server.fastmcp at the migration guide - #3388
Conversation
v1 code running against mcp 2 fails with a bare "No module named 'mcp.server.fastmcp'", which reads like a broken install and gives no hint that the package was renamed in a new major version. Add a plain module at the old path whose only statement raises ModuleNotFoundError with a message that keeps the canonical prefix, names the replacement import, links the migration guide, and mentions pinning mcp<2. The exception type and its `name` attribute match what a genuinely missing module produces, so existing `except ImportError`, `except ModuleNotFoundError`, and `exc.name` fallbacks keep working and nothing is re-exported or warned about. The module is a file rather than a package so tools that walk packages do not execute it, and it is excluded from the generated API reference since it carries no API.
The first-symptom row keeps the verbatim 2.0/2.1 text people search for while staying true once the message carries a pointer, and the FastMCP section notes that the old path raises ModuleNotFoundError and that dual-version import fallbacks continue to work.
📚 Documentation preview
|
There was a problem hiding this comment.
Beyond the inline finding, I also checked that the new tombstone doesn't break v1-detection patterns: importlib.util.find_spec("mcp.server.fastmcp") now returns a spec instead of None, but any code that follows that check with an actual import still hits the pointer message, and the dominant try/except-ImportError dual-major guard is unaffected (ModuleNotFoundError subclasses ImportError, and the new tests cover it). Also confirmed the module raising at import leaves no sys.modules residue, and the gen_ref_pages.py exclusion just keeps the tombstone out of the API docs.
Extended reasoning...
The change adds a tombstone module at the removed v1 path mcp/server/fastmcp.py that raises ModuleNotFoundError (with the correct name=) pointing at the migration guide, plus docs and doc-generation exclusions and thorough tests. The one inline finding (the equally common v1 spelling from mcp.server import FastMCP still gets the bare error) is a coverage-scope question worth a maintainer's eye, so I am not approving over it. Separately, I examined whether making mcp.server.fastmcp discoverable again (find_spec returns a spec, pkgutil lists it) could misfire spec-based v1 detection and ruled it out: such detection is unusual for this path, and any subsequent real import still raises the pointer, while the try/except import guard the change explicitly targets keeps working since ModuleNotFoundError subclasses ImportError and the module is never left cached after raising.
| "or pin 'mcp<2' to keep running v1 code." | ||
| ) | ||
|
|
||
| raise ModuleNotFoundError(_MESSAGE, name=__name__) |
There was a problem hiding this comment.
🟣 Pre-existing: the migration-pointer tombstone covers only the mcp.server.fastmcp module path, but v1 also re-exported the class from the package itself (v1's mcp/server/__init__.py had from .fastmcp import FastMCP), so the equally common v1 spelling from mcp.server import FastMCP still fails with the bare ImportError: cannot import name 'FastMCP' from 'mcp.server' and never sees the new pointer this PR adds. A module-level __getattr__ in src/mcp/server/__init__.py raising the same guidance for FastMCP (mirroring the _MESSAGE in src/mcp/server/fastmcp.py) would close the gap; docs/migration.md line 675 and the line-20 symptom row also only describe the ModuleNotFoundError path.
Extended reasoning...
A v1 user whose server does from mcp.server import FastMCP (a valid, exported v1 import path) upgrades to a 2.x release containing this change. Instead of the improved message pointing at MCPServer and the migration guide, they still get the uninformative ImportError: cannot import name 'FastMCP' from 'mcp.server' — exactly the confusing experience this PR was written to eliminate — because the tombstone only intercepts imports of the mcp.server.fastmcp module, not the FastMCP attribute of mcp.server.
Verification: pre-existing — src/mcp/server/init.py defines no FastMCP and no module-level __getattr__ (its imports are only CacheHint, ServerRequestContext, NotificationOptions, Server, MCPServer, InitializationOptions), so from mcp.server import FastMCP — a valid v1 spelling, since v1's mcp/server/__init__.py re-exported FastMCP via from .fastmcp import FastMCP and listed it in __all__ — raise
v1 code running against mcp 2 fails with a bare
ModuleNotFoundError: No module named 'mcp.server.fastmcp', which reads like a broken install. This adds a 16-linesrc/mcp/server/fastmcp.pywhose only statement raises the same exception with a message that says what happened:Not #3189: nothing is aliased or re-exported and no warning is emitted. The old path still fails to import; it just says why.
Motivation and Context
Since 2.0 shipped the bare string has been quoted in roughly a thousand downstream GitHub issues and PRs (one here, #3309), and new v1-shaped code keeps being written. It raises
ModuleNotFoundErrorwithname=set, rather thanImportError, because dual-version shims in the wild catch that class specifically or checkexc.name; those keep working unchanged. It's a module file rather than a package so one file covers everymcp.server.fastmcp.*path, and it's excluded from the API reference so it stays undocumented and deletable in any release.How Has This Been Tested?
New
tests/server/test_fastmcp.py(message snapshot,.name, nothing left insys.modules, deep submodule path, v1-first fallback idiom);./scripts/test, pyright, docs build; and by hand viapython -c,mcp run, and a built wheel.Breaking Changes
None. Exception type and
.nameare unchanged; only the message text differs, andimportlib.util.find_spec("mcp.server.fastmcp")now returns a spec instead ofNone.Types of changes
Checklist
help wanted, or I'm a maintainer)Additional context
Deliberately out of scope: the other removed v1 module paths (near-zero reports),
from mcp.server import FastMCPandMcpError(would need per-symbol__getattr__, cf. c27f95c).AI Disclaimer