Skip to content

Commit 1c1b7f8

Browse files
committed
Stop mcp.client from importing the server stack
Client only needs Server/MCPServer/InMemoryTransport/modern_on_request when it is handed a live in-process server, and a Server instance can only exist once the user has imported the server stack. Detect that case via sys.modules instead of eager imports, keep the server types for static checking under TYPE_CHECKING, and import the in-memory bridge inside _connect_inproc.
1 parent 2128b57 commit 1c1b7f8

1 file changed

Lines changed: 37 additions & 8 deletions

File tree

‎src/mcp/client/client.py‎

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
import hashlib
66
import logging
7+
import sys
78
import uuid
89
from collections.abc import Awaitable, Callable, Mapping, Sequence
910
from contextlib import AbstractAsyncContextManager, AsyncExitStack
1011
from dataclasses import KW_ONLY, dataclass, field
11-
from typing import Any, Literal, TypeVar, cast
12+
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
1213

1314
import anyio
1415
import anyio.lowlevel
@@ -40,10 +41,9 @@
4041
ServerCapabilities,
4142
)
4243
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS
43-
from typing_extensions import deprecated
44+
from typing_extensions import TypeIs, deprecated
4445

4546
from mcp.client._input_required import DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, run_input_required_driver
46-
from mcp.client._memory import InMemoryTransport
4747
from mcp.client._probe import negotiate_auto
4848
from mcp.client._transport import Transport
4949
from mcp.client.caching import CacheConfig, CacheMode, ClientResponseCache, InMemoryResponseCacheStore
@@ -61,16 +61,20 @@
6161
from mcp.client.streamable_http import streamable_http_client
6262
from mcp.client.subscriptions import ServerEvent, Subscription
6363
from mcp.client.subscriptions import listen as _listen
64-
from mcp.server import Server
65-
from mcp.server.mcpserver import MCPServer
66-
from mcp.server.runner import modern_on_request
6764
from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair
6865
from mcp.shared.dispatcher import Dispatcher, ProgressFnT
6966
from mcp.shared.exceptions import MCPDeprecationWarning, MCPError
7067
from mcp.shared.extension import validate_extension_identifier
7168
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
7269
from mcp.shared.subscriptions import event_to_notification
7370

71+
if TYPE_CHECKING:
72+
# Typing-only: the server stack is never imported by the client at runtime.
73+
# An in-process `Server`/`MCPServer` argument already implies the caller
74+
# imported it (see `_is_lowlevel_server`); URL/`Transport` clients pay nothing.
75+
from mcp.server import Server
76+
from mcp.server.mcpserver import MCPServer
77+
7478
logger = logging.getLogger(__name__)
7579

7680
ConnectMode = Literal["legacy", "auto"] | str
@@ -99,10 +103,35 @@ async def connect(exit_stack: AsyncExitStack, _mode: ConnectMode, _raise_excepti
99103
return connect
100104

101105

106+
def _loaded_class(module: str, name: str) -> type | None:
107+
"""Return class `name` from `module` if that module has already been imported, else `None`.
108+
109+
A `Server`/`MCPServer` instance can only exist once its defining module has run, so
110+
looking the class up in `sys.modules` (never importing it) gives an exact `isinstance`
111+
test while keeping the whole server stack out of a transport-only client's imports.
112+
"""
113+
return getattr(sys.modules.get(module), name, None)
114+
115+
116+
def _is_mcpserver(obj: object) -> TypeIs[MCPServer]:
117+
cls = _loaded_class("mcp.server.mcpserver.server", "MCPServer")
118+
return cls is not None and isinstance(obj, cls)
119+
120+
121+
def _is_lowlevel_server(obj: object) -> TypeIs[Server[Any]]:
122+
cls = _loaded_class("mcp.server.lowlevel.server", "Server")
123+
return cls is not None and isinstance(obj, cls)
124+
125+
102126
def _connect_inproc(server: Server[Any]) -> _Connector:
103127
"""Connector for an in-process ``Server``: legacy mode drives the stream loop via
104128
``InMemoryTransport``; any other mode drives the modern per-request path through a
105129
``DirectDispatcher`` peer pair (no streams, no JSON-RPC framing, no initialize handshake)."""
130+
# Function-level imports (documented exception): this connector is only built for a live
131+
# in-process `Server`, whose stack the caller has therefore already imported; binding these
132+
# here rather than at module top keeps `import mcp.client` free of the server stack.
133+
from mcp.client._memory import InMemoryTransport
134+
from mcp.server.runner import modern_on_request
106135

107136
async def connect(exit_stack: AsyncExitStack, mode: ConnectMode, raise_exceptions: bool) -> Dispatcher[Any]:
108137
if mode == "legacy":
@@ -388,9 +417,9 @@ def __post_init__(self) -> None:
388417
self._folded_extensions = _fold_extensions(self.extensions)
389418

390419
srv = self.server
391-
if isinstance(srv, MCPServer):
420+
if _is_mcpserver(srv):
392421
srv = srv._lowlevel_server # pyright: ignore[reportPrivateUsage]
393-
if isinstance(srv, Server):
422+
if _is_lowlevel_server(srv):
394423
self._connect = _connect_inproc(srv)
395424
elif isinstance(srv, str):
396425
self._connect = _connect_transport(streamable_http_client(srv))

0 commit comments

Comments
 (0)