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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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: 3 additions & 1 deletion docs/migration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,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 DownExpand Up@@ -672,6 +672,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:
Expand Down
11 changes: 6 additions & 5 deletions scripts/docs/gen_ref_pages.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,11 +31,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 numberDiff line numberDiff 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."
)

raiseModuleNotFoundError(_MESSAGE, name=__name__)

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.

🟣 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

55 changes: 55 additions & 0 deletions tests/server/test_fastmcp.py
Original file line numberDiff line numberDiff 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