Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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.


### What is unchanged on `MCPServer`

Beyond the changes covered in this section, the everyday `FastMCP` surface carries over to `MCPServer` as-is:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions src/mcp/server/fastmcp.py
Original file line number Diff line number Diff line change
@@ -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__)
55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line number Diff line number Diff line change
@@ -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
Loading