Skip to content

Commit 426fb79

Browse files
committed
Make mcp.types a permanent alias, not a deprecation shim
A deprecation is a permanent commitment (a v3 removal) spent on a transient goal (nudging the v1->v2 migration wave), and the second break would cost every remaining user again for no functional gain: the module is a star re-export that cannot drift. `import mcp.types as types` is also baked into years of tutorials and generated code, and is a genuinely clean types-only namespace for SDK users, while `mcp_types` remains the spelling that installs without the SDK stack. So drop the MCPDeprecationWarning and every "removed in v3" phrase. Removed names now raise AttributeError from `__getattr__` as PEP 562 requires, keeping hasattr() and getattr(name, default) on their fallback paths; the tradeoff is that `from mcp.types import <removed>` falls back to CPython's generic ImportError, which is still fail-fast. Fix two doc claims that still said mcp.types no longer exists.
1 parent 16a9d56 commit 426fb79

6 files changed

Lines changed: 67 additions & 80 deletions

File tree

docs/migration.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Every section heading below names the API it affects, so searching this page for
1414
|---|---|---|
1515
| `FastMCP` renamed to `MCPServer` | `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` | [`FastMCP` renamed](#fastmcp-renamed-to-mcpserver) |
1616
| 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) |
17-
| `mcp.types` moved to the `mcp-types` package | `MCPDeprecationWarning: mcp.types is deprecated; import from mcp_types.` | [`mcp.types` moved](#mcptypes-moved-to-the-mcp-types-package) |
17+
| `mcp.types` names removed | `AttributeError: mcp.types.Content was removed in v2; use ContentBlock.` | [Removed types](#removed-type-aliases-and-classes) |
1818
| `McpError` renamed to `MCPError` | `ImportError: cannot import name 'McpError' from 'mcp'` | [`McpError` renamed](#mcperror-renamed-to-mcperror) |
1919
| Resource URIs are `str`, not `AnyUrl` | `AttributeError: 'str' object has no attribute 'host'` | [URI type](#resource-uri-type-changed-from-anyurl-to-str) |
2020
| `streamablehttp_client` removed | `ImportError: cannot import name 'streamablehttp_client'` | [`streamablehttp_client`](#streamablehttp_client-removed) |
@@ -170,18 +170,19 @@ nothing on PyPI to pin to, keep the unpinned form.
170170

171171
The protocol wire types now live in a standalone distribution, `mcp-types`, imported as
172172
`mcp_types`. Its only runtime dependencies are `pydantic` and `typing-extensions`, so code
173-
that just needs to (de)serialize MCP traffic can install it without the full SDK. The `mcp` package depends on `mcp-types` and
174-
continues to re-export the type names at the top level, so `from mcp import Tool` is
175-
unchanged. `mcp.shared.version` was removed. The package's API reference is at
173+
that just needs to (de)serialize MCP traffic can install it without the full SDK. The `mcp`
174+
package depends on `mcp-types` and continues to re-export the type names at the top level, so
175+
`from mcp import Tool` is unchanged. The package's API reference is at
176176
[`mcp_types`](api/mcp_types/index.md).
177177

178-
`import mcp.types` still works in v2, as a compatibility shim that mirrors `mcp_types` and
179-
emits an `MCPDeprecationWarning` on first import; it will be removed in v3, so switch to
180-
`mcp_types` now. The shim only restores the import: the field renames below still apply, so
181-
code that imports through it can go on to fail on camelCase attribute access. The 23 names
182-
that no longer exist (listed under
183-
[Removed type aliases and classes](#removed-type-aliases-and-classes)) raise an
184-
`ImportError` from the shim that names their replacement.
178+
`import mcp.types` and `from mcp.types import ...` keep working: `mcp.types` is a permanent
179+
alias that mirrors `mcp_types` exactly (every name is the same object), so v1's most common
180+
import line needs no change if you already depend on `mcp`. Only `mcp.shared.version` was
181+
removed; import from `mcp_types.version` instead. Reading a name that no longer exists (listed
182+
under [Removed type aliases and classes](#removed-type-aliases-and-classes)) as
183+
`mcp.types.Content` raises an `AttributeError` that names its replacement; a
184+
`from mcp.types import Content` of one raises a plain `ImportError` (Python discards the hint on
185+
that path, but it still fails fast).
185186

186187
**Why:** keeping the wire types in their own package lets tooling and lightweight clients
187188
depend on the protocol schema without pulling in `httpx2`, `starlette`, `uvicorn`, and the
@@ -197,11 +198,13 @@ from mcp.shared.version import LATEST_PROTOCOL_VERSION
197198
**After (v2):**
198199

199200
```python
201+
# Unchanged if you depend on the SDK:
202+
from mcp.types import Tool, Resource
203+
from mcp import Tool, Resource
204+
205+
# Or the standalone package, which installs without the SDK's transport stack:
200206
from mcp_types import Tool, Resource
201207
from mcp_types.version import LATEST_PROTOCOL_VERSION
202-
203-
# Names `mcp` already re-exported at the top level are unchanged:
204-
from mcp import Tool, Resource
205208
```
206209

207210
### Removed type aliases and classes
@@ -1711,7 +1714,7 @@ except MCPError as e:
17111714
raise
17121715
```
17131716

1714-
`e.error.code` also still works; `e.code` is the v2 convenience property. `mcp.types` no longer exists, so the constant comes from `mcp_types`. The example uses the high-level `Client`; `ClientSession.call_tool()` raises the same `MCPError`.
1717+
`e.error.code` also still works; `e.code` is the v2 convenience property. The constant is importable from `mcp_types` or, equivalently, `mcp.types`. The example uses the high-level `Client`; `ClientSession.call_tool()` raises the same `MCPError`.
17151718

17161719
### `ClientSession` now runs on `JSONRPCDispatcher`; `BaseSession` removed
17171720

docs/whats-new.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Underneath, the v1 `BaseSession` receive loop was replaced by a dispatcher engin
117117

118118
### The wire types moved to `mcp-types`, and every field is snake_case
119119

120-
The protocol types now live in their own distribution, `mcp-types`, imported as `mcp_types`. It depends on nothing but pydantic and typing-extensions, so a gateway, a proxy, or a code generator can consume MCP's wire shapes without installing an HTTP stack. `mcp` depends on it at an exact version and re-exports the common names, so `from mcp import Tool` still works; `import mcp.types` does not.
120+
The protocol types now live in their own distribution, `mcp-types`, imported as `mcp_types`. It depends on nothing but pydantic and typing-extensions, so a gateway, a proxy, or a code generator can consume MCP's wire shapes without installing an HTTP stack. `mcp` depends on it at an exact version, and both v1 spellings keep working: `from mcp import Tool` for the common names, and `import mcp.types` as a permanent alias that mirrors `mcp_types` exactly.
121121

122122
On those types, every Python attribute is now snake_case: `result.is_error`, `tool.input_schema`, `listing.next_cursor`. The JSON on the wire is camelCase, exactly as before; only the attribute spelling changed. Two stricter defaults ride along: unknown fields are ignored instead of round-tripped (put extras in `_meta`), and both sides validate traffic against the protocol version they negotiated. See the **[Migration Guide](migration.md#field-names-changed-from-camelcase-to-snake_case)** for the rename table.
123123

@@ -146,7 +146,7 @@ Each of these is a section in the **[Migration Guide](migration.md)**:
146146

147147
* The **WebSocket transport**, both sides, and the `mcp[ws]` extra. It was never part of the MCP specification.
148148
* The **experimental Tasks** API (`mcp.*.experimental`). 2026-07-28 moves tasks out of the core protocol and into an official extension ([SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663)), which this SDK does not implement yet.
149-
* `mcp.types`, `mcp.shared.version`, `mcp.shared.progress`, and `mcp.shared.session` (with the `RequestResponder` stub v1 `message_handler` annotations imported) as import paths.
149+
* `mcp.shared.version`, `mcp.shared.progress`, and `mcp.shared.session` (with the `RequestResponder` stub v1 `message_handler` annotations imported) as import paths. (`mcp.types` is *not* removed: it remains as a permanent alias for the standalone `mcp_types` package.)
150150
* The deprecated `streamablehttp_client` spelling, and the `get_session_id` callback from `streamable_http_client` (which now yields exactly two streams).
151151
* `McpError`, renamed **`MCPError`** with a direct `(code, message, data)` constructor.
152152
* `MCPServer.get_context()`, `mount_path=`, and the lowlevel `Server`'s decorator methods, ContextVar, and handler dicts.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ max-complexity = 24 # Default is 10
217217

218218
[tool.ruff.lint.per-file-ignores]
219219
"__init__.py" = ["F401"]
220-
# Deprecated compat shim that mirrors the whole mcp_types namespace by design.
220+
# mcp.types is an alias that mirrors the whole mcp_types namespace by design.
221221
"src/mcp/types.py" = ["F403"]
222222
# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators).
223223
"src/mcp-types/mcp_types/v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"]

scripts/docs/gen_ref_pages.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
# it from `src/` would emit the unimportable `mcp-types.mcp_types.*`.
3131
PACKAGES = (ROOT / "src" / "mcp", ROOT / "src" / "mcp-types" / "mcp_types")
3232

33-
# Deprecated compatibility shims: they mirror another module's namespace and
34-
# are documented in the migration guide, so they earn no API page of their own.
33+
# Alias modules that mirror another module's namespace (`mcp.types` mirrors
34+
# `mcp_types`): the mirrored package's pages are the canonical rendering, so
35+
# the alias earns no page of its own.
3536
EXCLUDED = frozenset({"mcp.types"})
3637

3738
_KIND_SECTIONS = {

src/mcp/types.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,23 @@
1-
"""Deprecated: the protocol types moved to the standalone `mcp_types` package.
1+
"""The MCP protocol wire types, as the `mcp.types` namespace.
22
3-
`import mcp.types` still works in v2 as a compatibility shim, but it emits an
4-
`MCPDeprecationWarning` and will be removed in v3. Import from `mcp_types` instead:
3+
This module mirrors the standalone `mcp_types` package exactly (every name is the
4+
same object), so SDK users can keep the familiar v1 spelling:
55
6-
from mcp_types import Tool, TextContent
6+
import mcp.types as types
77
8-
Field names also changed from camelCase to snake_case (`tool.input_schema`, not
9-
`tool.inputSchema`), so an import that succeeds through this shim can still fail
10-
later on attribute access. See the migration guide, "Types and wire format".
8+
types.TextContent(type="text", text="hi")
9+
10+
Depend on and import `mcp_types` directly instead when you only need to
11+
(de)serialize MCP traffic and don't want the SDK's transport stack: its only
12+
runtime dependencies are `pydantic` and `typing-extensions`.
1113
"""
1214

13-
# A wildcard mirror of the mcp_types namespace is the whole point of this shim.
15+
# A wildcard mirror of the mcp_types namespace is the whole point of this module.
1416
# pyright: reportWildcardImportFromLibrary=false
1517

16-
import warnings
17-
1818
from mcp_types import *
1919
from mcp_types import __all__ as __all__
2020

21-
from mcp.shared.exceptions import MCPDeprecationWarning
22-
23-
warnings.warn(
24-
"mcp.types is deprecated; import from mcp_types. Fields are now snake_case; see the migration guide.",
25-
MCPDeprecationWarning,
26-
stacklevel=2,
27-
)
28-
2921
# Names v1's mcp.types exposed that no longer exist. A bare "cannot import name"
3022
# leaves people grepping; naming the replacement finishes the migration step.
3123
_REMOVED = {
@@ -56,9 +48,9 @@
5648

5749
def __getattr__(name: str) -> object:
5850
if (hint := _REMOVED.get(name)) is not None:
59-
# ImportError, not AttributeError: `from mcp.types import X` swallows an
60-
# AttributeError raised here and reports a generic "cannot import name",
61-
# discarding the hint; an ImportError propagates through both the
62-
# from-import and plain attribute-access paths with the message intact.
63-
raise ImportError(f"mcp.types.{name} was removed in v2; {hint}. See the migration guide.")
51+
# AttributeError as PEP 562 requires, so hasattr() and getattr(..., default)
52+
# still take their fallback path. The cost: `from mcp.types import Content`
53+
# discards this message for CPython's generic "cannot import name" (still
54+
# fail-fast); attribute access, the far more common v1 spelling, keeps it.
55+
raise AttributeError(f"mcp.types.{name} was removed in v2; {hint}. See the migration guide.")
6456
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

tests/test_types.py

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import importlib
2-
import sys
3-
from types import ModuleType
41
from typing import Any
52

63
import mcp_types
@@ -43,7 +40,7 @@
4340
from pydantic import ValidationError
4441

4542
import mcp
46-
from mcp import MCPDeprecationWarning
43+
import mcp.types
4744

4845

4946
@pytest.mark.anyio
@@ -454,42 +451,36 @@ def test_input_required_result_requires_at_least_one_of_input_requests_or_reques
454451
assert InputRequiredResult(request_state="s").input_requests is None
455452

456453

457-
def _import_deprecated_mcp_types_shim() -> ModuleType:
458-
"""Re-import the `mcp.types` compatibility shim so its import-time warning fires again."""
459-
sys.modules.pop("mcp.types", None)
460-
with pytest.warns(MCPDeprecationWarning) as record:
461-
module = importlib.import_module("mcp.types")
462-
assert str(record[0].message) == snapshot(
463-
"mcp.types is deprecated; import from mcp_types. Fields are now snake_case; see the migration guide."
464-
)
465-
return module
466-
467-
468-
def test_deprecated_mcp_types_import_warns_and_mirrors_mcp_types():
469-
"""SDK-defined: the v1 `mcp.types` module is a warning shim that mirrors `mcp_types` exactly."""
470-
module = _import_deprecated_mcp_types_shim()
471-
assert module.__all__ == mcp_types.__all__
472-
assert module.Tool is mcp_types.Tool
473-
# Importing the submodule binds it on the package, so v1's `from mcp import types`
474-
# idiom resolves to the same shim module.
475-
assert getattr(mcp, "types") is module
476-
477-
478-
def test_deprecated_mcp_types_names_the_replacement_for_a_name_removed_in_v2():
479-
"""SDK-defined: a v1 name that no longer exists raises an error naming its v2 replacement."""
480-
module = _import_deprecated_mcp_types_shim()
481-
# ImportError rather than AttributeError so `from mcp.types import Content` keeps this
482-
# message: the from-import path replaces an AttributeError with a generic one.
483-
with pytest.raises(ImportError) as exc_info:
484-
getattr(module, "Content")
454+
def test_mcp_types_namespace_mirrors_mcp_types_exactly():
455+
"""SDK-defined: `mcp.types` is a permanent alias whose every name is the `mcp_types` object."""
456+
assert mcp.types.__all__ == mcp_types.__all__
457+
assert all(getattr(mcp.types, name) is getattr(mcp_types, name) for name in mcp_types.__all__)
458+
459+
460+
def test_mcp_types_attribute_access_names_the_replacement_for_a_removed_name():
461+
"""SDK-defined: reading a v1 name that no longer exists names its v2 replacement."""
462+
with pytest.raises(AttributeError) as exc_info:
463+
getattr(mcp.types, "Content")
485464
assert str(exc_info.value) == snapshot(
486465
"mcp.types.Content was removed in v2; use ContentBlock. See the migration guide."
487466
)
467+
# AttributeError (not ImportError) keeps the introspection contract for probing code.
468+
assert not hasattr(mcp.types, "Content")
469+
assert getattr(mcp.types, "Content", None) is None
470+
471+
472+
def test_mcp_types_from_import_of_a_removed_name_still_fails_fast():
473+
"""SDK-defined: `from mcp.types import <removed>` raises rather than importing a stale name.
474+
475+
CPython builds this ImportError itself and discards the module's hint text, so only the
476+
exception type is asserted; the hint is on the attribute-access path above.
477+
"""
478+
with pytest.raises(ImportError):
479+
exec("from mcp.types import Content")
488480

489481

490-
def test_deprecated_mcp_types_unknown_attribute_raises_attribute_error():
491-
"""SDK-defined: an unknown attribute is a normal AttributeError, so `hasattr` still works."""
492-
module = _import_deprecated_mcp_types_shim()
482+
def test_mcp_types_unknown_attribute_raises_attribute_error():
483+
"""SDK-defined: an unknown attribute is a normal AttributeError, so `hasattr` returns False."""
493484
with pytest.raises(AttributeError):
494-
getattr(module, "not_a_protocol_type")
495-
assert not hasattr(module, "not_a_protocol_type")
485+
getattr(mcp.types, "not_a_protocol_type")
486+
assert not hasattr(mcp.types, "not_a_protocol_type")

0 commit comments

Comments
 (0)