From 52cb1a810e678944d86aa093eb276f2f0071d52e Mon Sep 17 00:00:00 2001 From: Max <224885523+maxisbey@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:57:07 +0100 Subject: [PATCH 1/3] Build releases with the pinned hatchling and a publish action that accepts Metadata 2.5 (#3380) (cherry picked from commit 4d6f87e8d2df8b161bd279cecdb76c988d74bc8a) --- .github/workflows/publish-pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 41b127f923..946ac3db6c 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -21,7 +21,7 @@ jobs: uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: false - version: 0.9.5 + version: 0.12.5 - name: Set up Python 3.12 run: uv python install 3.12 @@ -57,7 +57,7 @@ jobs: path: dist/ - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: # Lets a re-run after a partially failed upload publish the remaining # files instead of erroring on the ones already on PyPI. From 49c6266402a49289bec6439bd5a41fdda1513054 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:24:41 +0000 Subject: [PATCH 2/3] 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. (cherry picked from commit b189452ee58d618118324c5c3fab678cb9a64f71) --- scripts/docs/gen_ref_pages.py | 11 +++---- src/mcp/server/fastmcp.py | 16 ++++++++++ tests/server/test_fastmcp.py | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 src/mcp/server/fastmcp.py create mode 100644 tests/server/test_fastmcp.py diff --git a/scripts/docs/gen_ref_pages.py b/scripts/docs/gen_ref_pages.py index 26916e8c39..c406766415 100644 --- a/scripts/docs/gen_ref_pages.py +++ b/scripts/docs/gen_ref_pages.py @@ -30,11 +30,12 @@ # it from `src/` would emit the unimportable `mcp-types.mcp_types.*`. PACKAGES = (ROOT / "src" / "mcp", ROOT / "src" / "mcp-types" / "mcp_types") -# Alias packages that mirror another package's namespaces (`mcp.types` mirrors -# `mcp_types`, `mcp.types.version` mirrors `mcp_types.version`): the mirrored -# package's pages are the canonical rendering, so an alias, and every module -# under it, earns no page of its own. -EXCLUDED = frozenset({"mcp.types"}) +# Module paths that get no page, and neither does anything under them: alias +# packages that mirror another package's namespaces (`mcp.types` mirrors +# `mcp_types`), whose canonical rendering is the mirrored package's pages; and +# removed v1 import paths (`mcp.server.fastmcp`) that only raise a pointer to +# the migration guide and carry no API. +EXCLUDED = frozenset({"mcp.types", "mcp.server.fastmcp"}) _KIND_SECTIONS = { griffe.Kind.MODULE: "Modules", diff --git a/src/mcp/server/fastmcp.py b/src/mcp/server/fastmcp.py new file mode 100644 index 0000000000..7dd4646dc0 --- /dev/null +++ b/src/mcp/server/fastmcp.py @@ -0,0 +1,16 @@ +"""Removed in mcp 2: `FastMCP` is now `mcp.server.mcpserver.MCPServer`. + +This module has no API. Importing it, or anything below it, raises +`ModuleNotFoundError` with a message that points at the migration guide. It +exists only because the bare "No module named 'mcp.server.fastmcp'" gave v1 +code no hint that the installed SDK is a different major version. +""" + +_MESSAGE = ( + "No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer " + "(from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at " + "https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver " + "or pin 'mcp<2' to keep running v1 code." +) + +raise ModuleNotFoundError(_MESSAGE, name=__name__) diff --git a/tests/server/test_fastmcp.py b/tests/server/test_fastmcp.py new file mode 100644 index 0000000000..b15c519148 --- /dev/null +++ b/tests/server/test_fastmcp.py @@ -0,0 +1,55 @@ +"""The removed v1 import path `mcp.server.fastmcp` fails with a pointer to the migration guide.""" + +import importlib +import sys + +import pytest +from inline_snapshot import snapshot + +import mcp.server +from mcp.server.mcpserver import MCPServer + + +def test_importing_fastmcp_raises_module_not_found_that_points_at_the_migration_guide() -> None: + """SDK-defined: the v1 path fails with the same exception type and `.name` as a module + that genuinely does not exist, but the message names the replacement and the guide.""" + with pytest.raises(ModuleNotFoundError) as exc_info: + importlib.import_module("mcp.server.fastmcp") + + assert exc_info.value.name == "mcp.server.fastmcp" + assert str(exc_info.value) == snapshot( + "No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer " + "(from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at " + "https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver " + "or pin 'mcp<2' to keep running v1 code." + ) + # A module that raises while executing is never cached, so nothing is left behind. + assert "mcp.server.fastmcp" not in sys.modules + assert not hasattr(mcp.server, "fastmcp") + + +def test_importing_a_fastmcp_submodule_raises_the_parent_pointer() -> None: + """SDK-defined: a deep v1 path executes `mcp.server.fastmcp` first, so it fails with that + module's message and `.name` rather than a bare error for the leaf.""" + with pytest.raises(ModuleNotFoundError) as parent: + importlib.import_module("mcp.server.fastmcp") + with pytest.raises(ModuleNotFoundError) as exc_info: + importlib.import_module("mcp.server.fastmcp.utilities.types") + + assert exc_info.value.name == "mcp.server.fastmcp" + assert str(exc_info.value) == str(parent.value) + + +def test_v1_first_import_shim_falls_back_to_mcpserver() -> None: + """SDK-defined: projects that support both majors try the v1 import and fall back on + `ModuleNotFoundError` (the narrowest guard seen in the wild), which is why the pointer is + raised as exactly that type and not as a bare `ImportError` or after a warning.""" + fell_back = False + try: + server_class: type = importlib.import_module("mcp.server.fastmcp").FastMCP + except ModuleNotFoundError: + fell_back = True + server_class = MCPServer + + assert fell_back + assert server_class is MCPServer From df20ae02268e288ad4e748c1f78c08b5f599bd03 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:25:16 +0000 Subject: [PATCH 3/3] Describe the old import path's pointer in the migration guide 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 333034695f2c8d4e3c17e6f4dc8795acb1cb74a0) --- docs/migration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/migration.md b/docs/migration.md index 931d470d1a..232170b8d9 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -12,7 +12,7 @@ Every section heading below names the API it affects, so searching this page for | Change | First symptom | Section | |---|---|---| -| `FastMCP` renamed to `MCPServer` | `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` | [`FastMCP` renamed](#fastmcp-renamed-to-mcpserver) | +| `FastMCP` renamed to `MCPServer` | `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` (newer 2.x releases follow it with a pointer to this guide) | [`FastMCP` renamed](#fastmcp-renamed-to-mcpserver) | | Fields renamed from camelCase to snake_case | `AttributeError: 'Tool' object has no attribute 'inputSchema'` | [snake_case fields](#field-names-changed-from-camelcase-to-snake_case) | | `mcp.types` names removed | `ImportError: cannot import name 'Content' from 'mcp.types'` | [Removed types](#removed-type-aliases-and-classes) | | `McpError` renamed to `MCPError` | `ImportError: cannot import name 'McpError' from 'mcp'` | [`McpError` renamed](#mcperror-renamed-to-mcperror) | @@ -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` Beyond the changes covered in this section, the everyday `FastMCP` surface carries over to `MCPServer` as-is: