Skip to content

Commit b189452

Browse files
committed
Point imports of mcp.server.fastmcp at the migration guide
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.
1 parent 4d6f87e commit b189452

3 files changed

Lines changed: 77 additions & 5 deletions

File tree

scripts/docs/gen_ref_pages.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@
3131
# it from `src/` would emit the unimportable `mcp-types.mcp_types.*`.
3232
PACKAGES = (ROOT / "src" / "mcp", ROOT / "src" / "mcp-types" / "mcp_types")
3333

34-
# Alias packages that mirror another package's namespaces (`mcp.types` mirrors
35-
# `mcp_types`, `mcp.types.version` mirrors `mcp_types.version`): the mirrored
36-
# package's pages are the canonical rendering, so an alias, and every module
37-
# under it, earns no page of its own.
38-
EXCLUDED = frozenset({"mcp.types"})
34+
# Module paths that get no page, and neither does anything under them: alias
35+
# packages that mirror another package's namespaces (`mcp.types` mirrors
36+
# `mcp_types`), whose canonical rendering is the mirrored package's pages; and
37+
# removed v1 import paths (`mcp.server.fastmcp`) that only raise a pointer to
38+
# the migration guide and carry no API.
39+
EXCLUDED = frozenset({"mcp.types", "mcp.server.fastmcp"})
3940

4041
_KIND_SECTIONS = {
4142
griffe.Kind.MODULE: "Modules",

src/mcp/server/fastmcp.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Removed in mcp 2: `FastMCP` is now `mcp.server.mcpserver.MCPServer`.
2+
3+
This module has no API. Importing it, or anything below it, raises
4+
`ModuleNotFoundError` with a message that points at the migration guide. It
5+
exists only because the bare "No module named 'mcp.server.fastmcp'" gave v1
6+
code no hint that the installed SDK is a different major version.
7+
"""
8+
9+
_MESSAGE = (
10+
"No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer "
11+
"(from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at "
12+
"https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver "
13+
"or pin 'mcp<2' to keep running v1 code."
14+
)
15+
16+
raise ModuleNotFoundError(_MESSAGE, name=__name__)

tests/server/test_fastmcp.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""The removed v1 import path `mcp.server.fastmcp` fails with a pointer to the migration guide."""
2+
3+
import importlib
4+
import sys
5+
6+
import pytest
7+
from inline_snapshot import snapshot
8+
9+
import mcp.server
10+
from mcp.server.mcpserver import MCPServer
11+
12+
13+
def test_importing_fastmcp_raises_module_not_found_that_points_at_the_migration_guide() -> None:
14+
"""SDK-defined: the v1 path fails with the same exception type and `.name` as a module
15+
that genuinely does not exist, but the message names the replacement and the guide."""
16+
with pytest.raises(ModuleNotFoundError) as exc_info:
17+
importlib.import_module("mcp.server.fastmcp")
18+
19+
assert exc_info.value.name == "mcp.server.fastmcp"
20+
assert str(exc_info.value) == snapshot(
21+
"No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer "
22+
"(from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at "
23+
"https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver "
24+
"or pin 'mcp<2' to keep running v1 code."
25+
)
26+
# A module that raises while executing is never cached, so nothing is left behind.
27+
assert "mcp.server.fastmcp" not in sys.modules
28+
assert not hasattr(mcp.server, "fastmcp")
29+
30+
31+
def test_importing_a_fastmcp_submodule_raises_the_parent_pointer() -> None:
32+
"""SDK-defined: a deep v1 path executes `mcp.server.fastmcp` first, so it fails with that
33+
module's message and `.name` rather than a bare error for the leaf."""
34+
with pytest.raises(ModuleNotFoundError) as parent:
35+
importlib.import_module("mcp.server.fastmcp")
36+
with pytest.raises(ModuleNotFoundError) as exc_info:
37+
importlib.import_module("mcp.server.fastmcp.utilities.types")
38+
39+
assert exc_info.value.name == "mcp.server.fastmcp"
40+
assert str(exc_info.value) == str(parent.value)
41+
42+
43+
def test_v1_first_import_shim_falls_back_to_mcpserver() -> None:
44+
"""SDK-defined: projects that support both majors try the v1 import and fall back on
45+
`ModuleNotFoundError` (the narrowest guard seen in the wild), which is why the pointer is
46+
raised as exactly that type and not as a bare `ImportError` or after a warning."""
47+
fell_back = False
48+
try:
49+
server_class: type = importlib.import_module("mcp.server.fastmcp").FastMCP
50+
except ModuleNotFoundError:
51+
fell_back = True
52+
server_class = MCPServer
53+
54+
assert fell_back
55+
assert server_class is MCPServer

0 commit comments

Comments
 (0)