[v2.0.x] Point imports of mcp.server.fastmcp at the migration guide - #3393
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. (cherry picked from commit b189452)
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. (cherry picked from commit 3330346)
There was a problem hiding this comment.
Beyond the inline finding, I also checked whether shipping the tombstone as a real module file breaks spec-based introspection — importlib.util.find_spec("mcp.server.fastmcp") now returns a ModuleSpec where it previously returned None, but this is an inherent, low-impact tradeoff of the tombstone approach (find_spec does not execute the module) and was ruled out as a bug.
Extended reasoning...
A finding on docs/migration.md's new claim about except ImportError fallbacks is posted inline, so approval is off the table. Separately, two candidate issues about the stub file's visibility to importlib.util.find_spec were investigated and ruled out: find_spec locating the module without executing it means code probing for the v1 module via spec lookup sees it as present, but that is a known cost of any tombstone module rather than a defect in this change, and the common dual-major pattern (try-import with a ModuleNotFoundError fallback, covered by the new tests in tests/server/test_fastmcp.py) works as intended.
| - `ToolError`, `ResourceError` — from `mcp.server.mcpserver.exceptions` | ||
| - `MCPServerError` (renamed from `FastMCPError`) — from `mcp.server.mcpserver.exceptions` | ||
|
|
||
| Importing `mcp.server.fastmcp`, or anything below it, raises `ModuleNotFoundError` (newer 2.x releases include a link to this section in its message), so existing `except ImportError` or `except ModuleNotFoundError` fallbacks around the v1 import keep working. |
There was a problem hiding this comment.
🟡 This new claim is false for the from mcp.server import fastmcp spelling (valid in v1, whose mcp/server/__init__.py imported .fastmcp): CPython's _handle_fromlist swallows a fromlist submodule's ModuleNotFoundError exactly when exc.name matches the dotted path and the module is absent from sys.modules — both true for the shim — so the user gets a plain ImportError: cannot import name 'fastmcp' from 'mcp.server' with no migration pointer, and an except ModuleNotFoundError fallback does NOT catch it (behavior itself is unchanged from base; the new doc sentence and the shim docstring in src/mcp/server/fastmcp.py are what overclaim). Scope the claim to import mcp.server.fastmcp / from mcp.server.fastmcp import ..., or note that only except ImportError covers every spelling.
Extended reasoning...
Path: from mcp.server import fastmcp compiles to import('mcp.server', fromlist=['fastmcp']). mcp.server (v2) has no fastmcp attribute, so _handle_fromlist runs import_('mcp.server.fastmcp'); the shim at src/mcp/server/fastmcp.py:16 raises ModuleNotFoundError(name='mcp.server.fastmcp'), and the failed exec removes the entry from sys.modules. _handle_fromlist's backwards-compat guard — if exc.name == from_ and sys.modules.get(from_, _ERR_MSG_PREFIX) is not None: continue — matches on both conditions (name set to name on purpose, module absent so .get returns the non-None sentinel), so the shim's exception is silently discarded. IMPORT_FROM then raises ImportError('cannot import name fastmcp from mcp.server'), which is not a ModuleNotFoundError and carries no migration pointer. Consequence: a v1-compat guard try: from mcp.server import fastmcp / except ModuleNotFoundError: crashes despite migration.md:670's assurance that such fallbacks 'keep working', and the whole point of the shim (the pointer message) never surfaces for this spelling. Same final exception as base 2
Verification: nit — The mechanism is real and the new doc sentence is false for one valid v1 spelling, but runtime behavior is unchanged from the base branch. from mcp.server import fastmcp (valid in v1, whose mcp/server/__init__.py did from .fastmcp import FastMCP, binding the submodule attribute) compiles to __import__('mcp.server', fromlist=('fastmcp',)). v2's src/mcp/server/__init__.py binds no `f
📚 Documentation preview
|
There was a problem hiding this comment.
1 issue found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/migration.md">
<violation number="1" location="docs/migration.md:670">
P2: Scope this fallback claim to `import mcp.server.fastmcp` and `from mcp.server.fastmcp import ...`, or tell users to catch `ImportError`: `from mcp.server import fastmcp` suppresses the shim's `ModuleNotFoundError` and raises `ImportError` instead.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| - `ToolError`, `ResourceError` — from `mcp.server.mcpserver.exceptions` | ||
| - `MCPServerError` (renamed from `FastMCPError`) — from `mcp.server.mcpserver.exceptions` | ||
|
|
||
| Importing `mcp.server.fastmcp`, or anything below it, raises `ModuleNotFoundError` (newer 2.x releases include a link to this section in its message), so existing `except ImportError` or `except ModuleNotFoundError` fallbacks around the v1 import keep working. |
There was a problem hiding this comment.
P2: Scope this fallback claim to import mcp.server.fastmcp and from mcp.server.fastmcp import ..., or tell users to catch ImportError: from mcp.server import fastmcp suppresses the shim's ModuleNotFoundError and raises ImportError instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/migration.md, line 670:
<comment>Scope this fallback claim to `import mcp.server.fastmcp` and `from mcp.server.fastmcp import ...`, or tell users to catch `ImportError`: `from mcp.server import fastmcp` suppresses the shim's `ModuleNotFoundError` and raises `ImportError` instead.</comment>
<file context>
@@ -667,6 +667,8 @@ All submodules under `mcp.server.fastmcp.*` are now under `mcp.server.mcpserver.
- `ToolError`, `ResourceError` — from `mcp.server.mcpserver.exceptions`
- `MCPServerError` (renamed from `FastMCPError`) — from `mcp.server.mcpserver.exceptions`
+Importing `mcp.server.fastmcp`, or anything below it, raises `ModuleNotFoundError` (newer 2.x releases include a link to this section in its message), so existing `except ImportError` or `except ModuleNotFoundError` fallbacks around the v1 import keep working.
+
### What is unchanged on `MCPServer`
</file context>
| Importing `mcp.server.fastmcp`, or anything below it, raises `ModuleNotFoundError` (newer 2.x releases include a link to this section in its message), so existing `except ImportError` or `except ModuleNotFoundError` fallbacks around the v1 import keep working. | |
| +Importing `mcp.server.fastmcp`, or anything below it, raises `ModuleNotFoundError` (newer 2.x releases include a link to this section in its message), so fallbacks around `import mcp.server.fastmcp` or `from mcp.server.fastmcp import ...` keep working. |
Backport of #3388 onto
v2.0.0so it can ship as 2.0.1.v2.0.xwas cut at thev2.0.0tag; this PR is the only thing on top of it.Three cherry-picks (
-x, all applied cleanly):publish-pypi.ymlruns from the tagged commit, and the 2.0.0-era workflow would rebuild with floating hatchling and hit the same Metadata 2.5 upload rejection v2.1.0 did.src/mcp/server/fastmcp.pyraisingModuleNotFoundErrorwith the pointer, the API-reference exclusion, tests, and the twodocs/migration.mdclarity edits.Motivation and Context
See #3388. A 2.0.1 reaches installs pinned to
mcp==2.0.*/<2.1; everyone unpinned gets it from the next release offmain.How Has This Been Tested?
On this branch:
./scripts/test(5585 passed, 100% coverage,strict-no-coverclean), pyright, ruff,scripts/docs/build_config.py(nofastmcppage), and the import by hand.uv-dynamic-versioningresolves the branch as2.0.1.devN, so av2.0.1tag here yields 2.0.1.Breaking Changes
None.
Types of changes
Checklist
help wanted, or I'm a maintainer)Additional context
Release:
gh release create v2.0.1 --target <merge sha on v2.0.x> --latest=falseso 2.1.0 stays "Latest".deploy-docs.ymlonly fires frommain, so the docs site is unaffected by this branch. If you'd rather keep #3380 as its own commit onv2.0.x, rebase-merge instead of squash.AI Disclaimer