Skip to content

Commit 5b25303

Browse files
committed
Keep typing.get_type_hints(Client) resolvable at runtime
Making the server stack a typing-only import in the client module left the `Client.server` annotation naming `Server` / `MCPServer`, which `typing.get_type_hints` can then no longer evaluate: string annotations are evaluated against the module dict, so a module-level `__getattr__` would not be consulted either. Spell the annotation through the `mcp.server` namespace instead. Evaluating the hints then walks the lazy `mcp` package, which imports `mcp.server` at that moment only; `import mcp.client` stays free of the server stack and the evaluated hint is the same union of classes as before. The package's TYPE_CHECKING mirror now also lists the subpackages that its `__getattr__` resolves on first access, so type checkers see the same chains.
1 parent 1e00f42 commit 5b25303

3 files changed

Lines changed: 25 additions & 2 deletions

File tree

‎src/mcp/__init__.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@
6868
from mcp_types import Role as SamplingRole
6969

7070
# 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.
71+
# ...` did, so `import mcp` followed by `mcp.types.Tool` keeps working. The
72+
# other subpackages are mirrored too: at runtime `__getattr__` imports them
73+
# on first attribute access (`_LAZY_SUBMODULES`), so `mcp.server.Server`
74+
# style chains resolve, and type checkers must see the same members.
75+
from . import client as client
76+
from . import os as os
77+
from . import server as server
78+
from . import shared as shared
7279
from . import types as types
7380
from .client._input_required import InputRequiredRoundsExceededError
7481
from .client.client import Client

‎src/mcp/client/client.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS
4444
from typing_extensions import TypeIs, deprecated
4545

46+
import mcp
4647
from mcp.client._input_required import DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, run_input_required_driver
4748
from mcp.client._probe import negotiate_auto
4849
from mcp.client._transport import Transport
@@ -321,7 +322,11 @@ async def main():
321322
```
322323
"""
323324

324-
server: Server[Any] | MCPServer | Transport | str
325+
# Spelled through the `mcp.server` namespace (not the TYPE_CHECKING-only
326+
# `Server`/`MCPServer` names above) so `typing.get_type_hints(Client)`
327+
# still resolves at runtime: the lazy `mcp` package imports `mcp.server`
328+
# only when the annotation is actually evaluated.
329+
server: mcp.server.Server[Any] | mcp.server.MCPServer | Transport | str
325330
"""The MCP server to connect to.
326331
327332
If the server is a `Server` or `MCPServer` instance, it will be connected in-process.

‎tests/client/test_client.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import subprocess
88
import sys
9+
import typing
910
from collections.abc import AsyncIterator, Iterator
1011
from contextlib import asynccontextmanager, contextmanager
1112
from unittest.mock import patch
@@ -446,6 +447,16 @@ def test_client_entry_points_load_the_server_stack_and_http_transports_only_when
446447
assert after_url_client == ["httpx2"]
447448

448449

450+
def test_client_annotations_still_evaluate_at_runtime():
451+
"""SDK-defined: `typing.get_type_hints(Client)` resolves even though the server stack is
452+
a typing-only import in the client module; the `server` annotation is qualified through
453+
the lazy `mcp.server` namespace, which imports it only when the hints are evaluated."""
454+
hints = typing.get_type_hints(Client)
455+
server_targets = typing.get_args(hints["server"])
456+
assert MCPServer in server_targets
457+
assert str in server_targets
458+
459+
449460
async def test_client_uses_transport_directly(app: MCPServer):
450461
transport = InMemoryTransport(app)
451462
async with Client(transport, mode="legacy") as client:

0 commit comments

Comments
 (0)