Skip to content

Commit 622aed5

Browse files
committed
Resolve the mcp package's exports lazily (PEP 562)
`import mcp` used to import the protocol types plus the entire client and server stacks (in-memory transport, lowlevel server, HTTP transports, otel, auth) just to bind the names in `mcp.__all__`. Each export is now resolved from its home module by a module-level `__getattr__` on first access and cached in the package namespace; the old eager import block is mirrored name-for-name under `TYPE_CHECKING` so type checkers and the docs generator see the same bindings. `__all__`, `dir(mcp)`, `from mcp import *`, object identity and the `mcp.types` submodule binding are unchanged. A bare `import mcp` no longer loads pydantic, mcp_types or any mcp submodule; each entry point pays only for what it uses.
1 parent 6f69a37 commit 622aed5

2 files changed

Lines changed: 263 additions & 71 deletions

File tree

‎src/mcp/__init__.py‎

Lines changed: 212 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,84 @@
1-
from mcp_types import (
2-
CallToolRequest,
3-
ClientCapabilities,
4-
ClientNotification,
5-
ClientRequest,
6-
ClientResult,
7-
CompleteRequest,
8-
CreateMessageRequest,
9-
CreateMessageResult,
10-
CreateMessageResultWithTools,
11-
ErrorData,
12-
GetPromptRequest,
13-
GetPromptResult,
14-
Implementation,
15-
IncludeContext,
16-
InitializedNotification,
17-
InitializeRequest,
18-
InitializeResult,
19-
JSONRPCError,
20-
JSONRPCRequest,
21-
JSONRPCResponse,
22-
ListPromptsRequest,
23-
ListPromptsResult,
24-
ListResourcesRequest,
25-
ListResourcesResult,
26-
ListToolsResult,
27-
LoggingLevel,
28-
LoggingMessageNotification,
29-
Notification,
30-
PingRequest,
31-
ProgressNotification,
32-
PromptsCapability,
33-
ReadResourceRequest,
34-
ReadResourceResult,
35-
Resource,
36-
ResourcesCapability,
37-
ResourceUpdatedNotification,
38-
RootsCapability,
39-
SamplingCapability,
40-
SamplingContent,
41-
SamplingContextCapability,
42-
SamplingMessage,
43-
SamplingMessageContentBlock,
44-
SamplingToolsCapability,
45-
ServerCapabilities,
46-
ServerNotification,
47-
ServerRequest,
48-
ServerResult,
49-
SetLevelRequest,
50-
StopReason,
51-
SubscribeRequest,
52-
Tool,
53-
ToolChoice,
54-
ToolResultContent,
55-
ToolsCapability,
56-
ToolUseContent,
57-
UnsubscribeRequest,
58-
)
59-
from mcp_types import Role as SamplingRole
1+
from importlib import import_module
2+
from typing import TYPE_CHECKING
3+
4+
# Every name below is resolved LAZILY on first access via the module
5+
# `__getattr__` at the bottom of this file (PEP 562), so a bare `import mcp` no
6+
# longer imports the protocol types nor the client and server stacks. Type
7+
# checkers still see the real bindings through these `TYPE_CHECKING` imports,
8+
# which mirror the runtime lazy map name for name.
9+
if TYPE_CHECKING:
10+
from mcp_types import (
11+
CallToolRequest,
12+
ClientCapabilities,
13+
ClientNotification,
14+
ClientRequest,
15+
ClientResult,
16+
CompleteRequest,
17+
CreateMessageRequest,
18+
CreateMessageResult,
19+
CreateMessageResultWithTools,
20+
ErrorData,
21+
GetPromptRequest,
22+
GetPromptResult,
23+
Implementation,
24+
IncludeContext,
25+
InitializedNotification,
26+
InitializeRequest,
27+
InitializeResult,
28+
JSONRPCError,
29+
JSONRPCRequest,
30+
JSONRPCResponse,
31+
ListPromptsRequest,
32+
ListPromptsResult,
33+
ListResourcesRequest,
34+
ListResourcesResult,
35+
ListToolsResult,
36+
LoggingLevel,
37+
LoggingMessageNotification,
38+
Notification,
39+
PingRequest,
40+
ProgressNotification,
41+
PromptsCapability,
42+
ReadResourceRequest,
43+
ReadResourceResult,
44+
Resource,
45+
ResourcesCapability,
46+
ResourceUpdatedNotification,
47+
RootsCapability,
48+
SamplingCapability,
49+
SamplingContent,
50+
SamplingContextCapability,
51+
SamplingMessage,
52+
SamplingMessageContentBlock,
53+
SamplingToolsCapability,
54+
ServerCapabilities,
55+
ServerNotification,
56+
ServerRequest,
57+
ServerResult,
58+
SetLevelRequest,
59+
StopReason,
60+
SubscribeRequest,
61+
Tool,
62+
ToolChoice,
63+
ToolResultContent,
64+
ToolsCapability,
65+
ToolUseContent,
66+
UnsubscribeRequest,
67+
)
68+
from mcp_types import Role as SamplingRole
6069

61-
# Bind the `mcp.types` submodule on the package, as v1's `from .types import
62-
# ...` did, so `import mcp` followed by `mcp.types.Tool` keeps working.
63-
from . import types as types
64-
from .client._input_required import InputRequiredRoundsExceededError
65-
from .client.client import Client
66-
from .client.session import ClientSession
67-
from .client.session_group import ClientSessionGroup
68-
from .client.stdio import StdioServerParameters, stdio_client
69-
from .server.session import ServerSession
70-
from .server.stdio import stdio_server
71-
from .shared.exceptions import MCPDeprecationWarning, MCPError, UrlElicitationRequiredError
72-
from .shared.uri_template import InvalidUriTemplate, UriTemplate
70+
# Bind the `mcp.types` submodule on the package, as v1's `from .types import
71+
# ...` did, so `import mcp` followed by `mcp.types.Tool` keeps working.
72+
from . import types as types
73+
from .client._input_required import InputRequiredRoundsExceededError
74+
from .client.client import Client
75+
from .client.session import ClientSession
76+
from .client.session_group import ClientSessionGroup
77+
from .client.stdio import StdioServerParameters, stdio_client
78+
from .server.session import ServerSession
79+
from .server.stdio import stdio_server
80+
from .shared.exceptions import MCPDeprecationWarning, MCPError, UrlElicitationRequiredError
81+
from .shared.uri_template import InvalidUriTemplate, UriTemplate
7382

7483
__all__ = [
7584
"CallToolRequest",
@@ -143,3 +152,135 @@
143152
"stdio_client",
144153
"stdio_server",
145154
]
155+
156+
# ---------------------------------------------------------------------------
157+
# Lazy attributes (PEP 562).
158+
#
159+
# `import mcp` used to pull in the protocol types (`mcp_types`, ~500 pydantic
160+
# models) plus the entire client and server stacks (in-memory transport ->
161+
# lowlevel server -> transports/otel/auth) only to bind the names above.
162+
# Every name is still here - `from mcp import Client`, `mcp.Tool`,
163+
# `from mcp import *`, `dir(mcp)` all behave exactly as before and resolve to the
164+
# very same objects - but each is imported from its home module on first access
165+
# and then cached in this namespace, so `import mcp` alone is nearly free and
166+
# each entry point pays only for what it actually uses.
167+
# ---------------------------------------------------------------------------
168+
169+
_MCP_TYPES_NAMES = (
170+
"CallToolRequest",
171+
"ClientCapabilities",
172+
"ClientNotification",
173+
"ClientRequest",
174+
"ClientResult",
175+
"CompleteRequest",
176+
"CreateMessageRequest",
177+
"CreateMessageResult",
178+
"CreateMessageResultWithTools",
179+
"ErrorData",
180+
"GetPromptRequest",
181+
"GetPromptResult",
182+
"Implementation",
183+
"IncludeContext",
184+
"InitializedNotification",
185+
"InitializeRequest",
186+
"InitializeResult",
187+
"JSONRPCError",
188+
"JSONRPCRequest",
189+
"JSONRPCResponse",
190+
"ListPromptsRequest",
191+
"ListPromptsResult",
192+
"ListResourcesRequest",
193+
"ListResourcesResult",
194+
"ListToolsResult",
195+
"LoggingLevel",
196+
"LoggingMessageNotification",
197+
"Notification",
198+
"PingRequest",
199+
"ProgressNotification",
200+
"PromptsCapability",
201+
"ReadResourceRequest",
202+
"ReadResourceResult",
203+
"Resource",
204+
"ResourcesCapability",
205+
"ResourceUpdatedNotification",
206+
"RootsCapability",
207+
"SamplingCapability",
208+
"SamplingContent",
209+
"SamplingContextCapability",
210+
"SamplingMessage",
211+
"SamplingMessageContentBlock",
212+
"SamplingToolsCapability",
213+
"ServerCapabilities",
214+
"ServerNotification",
215+
"ServerRequest",
216+
"ServerResult",
217+
"SetLevelRequest",
218+
"StopReason",
219+
"SubscribeRequest",
220+
"Tool",
221+
"ToolChoice",
222+
"ToolResultContent",
223+
"ToolsCapability",
224+
"ToolUseContent",
225+
"UnsubscribeRequest",
226+
)
227+
228+
# name -> (home module, attribute in it); mirrors the TYPE_CHECKING imports
229+
# above name for name. (No module-level annotations here: on 3.14+ they would
230+
# add `__annotate__`/`__conditional_annotations__` to the namespace and show up
231+
# in `dir(mcp)`.)
232+
_LAZY_ATTRS = {
233+
**{name: ("mcp_types", name) for name in _MCP_TYPES_NAMES},
234+
"SamplingRole": ("mcp_types", "Role"),
235+
"InputRequiredRoundsExceededError": ("mcp.client._input_required", "InputRequiredRoundsExceededError"),
236+
"Client": ("mcp.client.client", "Client"),
237+
"ClientSession": ("mcp.client.session", "ClientSession"),
238+
"ClientSessionGroup": ("mcp.client.session_group", "ClientSessionGroup"),
239+
"StdioServerParameters": ("mcp.client.stdio", "StdioServerParameters"),
240+
"stdio_client": ("mcp.client.stdio", "stdio_client"),
241+
"ServerSession": ("mcp.server.session", "ServerSession"),
242+
"stdio_server": ("mcp.server.stdio", "stdio_server"),
243+
"MCPDeprecationWarning": ("mcp.shared.exceptions", "MCPDeprecationWarning"),
244+
"MCPError": ("mcp.shared.exceptions", "MCPError"),
245+
"UrlElicitationRequiredError": ("mcp.shared.exceptions", "UrlElicitationRequiredError"),
246+
"InvalidUriTemplate": ("mcp.shared.uri_template", "InvalidUriTemplate"),
247+
"UriTemplate": ("mcp.shared.uri_template", "UriTemplate"),
248+
}
249+
250+
# Submodules a bare `import mcp` used to bind on the package as a side effect
251+
# of the eager imports (`mcp.types` is documented: v1's `mcp.types.Tool` idiom;
252+
# `client`/`server`/`shared`/`os` were merely incidental). Each is imported on
253+
# first touch; the import system then binds it here, so it is cached as well.
254+
_LAZY_SUBMODULES = frozenset({"types", "client", "os", "server", "shared"})
255+
256+
# Names the lazy machinery itself adds to this namespace; kept out of
257+
# `dir(mcp)` so it reports exactly what it did when the imports were eager.
258+
_LAZY_MACHINERY = frozenset(
259+
{
260+
"TYPE_CHECKING",
261+
"import_module",
262+
"_LAZY_ATTRS",
263+
"_LAZY_MACHINERY",
264+
"_LAZY_SUBMODULES",
265+
"_MCP_TYPES_NAMES",
266+
"__getattr__",
267+
"__dir__",
268+
}
269+
)
270+
271+
272+
def __getattr__(name: str) -> object:
273+
lazy = _LAZY_ATTRS.get(name)
274+
if lazy is not None:
275+
module_name, attr = lazy
276+
value = getattr(import_module(module_name), attr)
277+
# Cache on the module: every later access is a plain namespace lookup.
278+
globals()[name] = value
279+
return value
280+
if name in _LAZY_SUBMODULES:
281+
return import_module(f"{__name__}.{name}")
282+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
283+
284+
285+
def __dir__() -> list[str]:
286+
return sorted((globals().keys() - _LAZY_MACHINERY) | _LAZY_ATTRS.keys() | _LAZY_SUBMODULES)

‎tests/test_init.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""The `mcp` package namespace: lazy exports (PEP 562) that stay identical to the eager originals."""
2+
3+
import subprocess
4+
import sys
5+
6+
import pytest
7+
from inline_snapshot import snapshot
8+
9+
import mcp
10+
import mcp.shared.exceptions
11+
12+
13+
def test_exported_name_is_the_home_module_object_and_is_cached_on_the_package():
14+
"""SDK-defined: `mcp.MCPError` is `mcp.shared.exceptions.MCPError`, cached after first access."""
15+
assert mcp.MCPError is mcp.shared.exceptions.MCPError
16+
assert vars(mcp)["MCPError"] is mcp.shared.exceptions.MCPError
17+
18+
19+
def test_dir_lists_every_export_and_the_bound_submodules():
20+
"""SDK-defined: `dir(mcp)` reports all of `__all__` plus the submodules a bare import binds."""
21+
listing = set(dir(mcp))
22+
assert set(mcp.__all__) <= listing
23+
assert {"client", "os", "server", "shared", "types"} <= listing
24+
25+
26+
def test_unknown_attribute_raises_attribute_error():
27+
"""SDK-defined: a name that is neither an export nor a submodule raises AttributeError."""
28+
with pytest.raises(AttributeError):
29+
getattr(mcp, "no_such_name")
30+
31+
32+
def test_bare_import_mcp_does_not_load_the_types_or_client_or_server_stacks():
33+
"""SDK-defined: `import mcp` is lazy - the protocol types and the client/server stacks
34+
load on first use, not on package import.
35+
36+
A fresh interpreter is required to observe `sys.modules` after a bare import: this
37+
test process imported all of them long ago.
38+
"""
39+
probe = (
40+
"import mcp, sys\n"
41+
"print(sorted(m for m in sys.modules if m.split('.')[0] in {'mcp', 'mcp_types', 'pydantic'}))\n"
42+
)
43+
result = subprocess.run(
44+
[sys.executable, "-c", probe],
45+
capture_output=True,
46+
text=True,
47+
check=False,
48+
timeout=20,
49+
)
50+
assert result.returncode == 0, result.stderr
51+
assert result.stdout == snapshot("['mcp']\n")

0 commit comments

Comments
 (0)