|
4 | 4 |
|
5 | 5 | import hashlib |
6 | 6 | import logging |
| 7 | +import sys |
7 | 8 | import uuid |
8 | 9 | from collections.abc import Awaitable, Callable, Mapping, Sequence |
9 | 10 | from contextlib import AbstractAsyncContextManager, AsyncExitStack |
10 | 11 | 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 |
12 | 13 |
|
13 | 14 | import anyio |
14 | 15 | import anyio.lowlevel |
|
40 | 41 | ServerCapabilities, |
41 | 42 | ) |
42 | 43 | from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS |
43 | | -from typing_extensions import deprecated |
| 44 | +from typing_extensions import TypeIs, deprecated |
44 | 45 |
|
45 | 46 | from mcp.client._input_required import DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, run_input_required_driver |
46 | | -from mcp.client._memory import InMemoryTransport |
47 | 47 | from mcp.client._probe import negotiate_auto |
48 | 48 | from mcp.client._transport import Transport |
49 | 49 | from mcp.client.caching import CacheConfig, CacheMode, ClientResponseCache, InMemoryResponseCacheStore |
|
61 | 61 | from mcp.client.streamable_http import streamable_http_client |
62 | 62 | from mcp.client.subscriptions import ServerEvent, Subscription |
63 | 63 | 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 |
67 | 64 | from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair |
68 | 65 | from mcp.shared.dispatcher import Dispatcher, ProgressFnT |
69 | 66 | from mcp.shared.exceptions import MCPDeprecationWarning, MCPError |
70 | 67 | from mcp.shared.extension import validate_extension_identifier |
71 | 68 | from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher |
72 | 69 | from mcp.shared.subscriptions import event_to_notification |
73 | 70 |
|
| 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 | + |
74 | 78 | logger = logging.getLogger(__name__) |
75 | 79 |
|
76 | 80 | ConnectMode = Literal["legacy", "auto"] | str |
@@ -99,10 +103,35 @@ async def connect(exit_stack: AsyncExitStack, _mode: ConnectMode, _raise_excepti |
99 | 103 | return connect |
100 | 104 |
|
101 | 105 |
|
| 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 | + |
102 | 126 | def _connect_inproc(server: Server[Any]) -> _Connector: |
103 | 127 | """Connector for an in-process ``Server``: legacy mode drives the stream loop via |
104 | 128 | ``InMemoryTransport``; any other mode drives the modern per-request path through a |
105 | 129 | ``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 |
106 | 135 |
|
107 | 136 | async def connect(exit_stack: AsyncExitStack, mode: ConnectMode, raise_exceptions: bool) -> Dispatcher[Any]: |
108 | 137 | if mode == "legacy": |
@@ -388,9 +417,9 @@ def __post_init__(self) -> None: |
388 | 417 | self._folded_extensions = _fold_extensions(self.extensions) |
389 | 418 |
|
390 | 419 | srv = self.server |
391 | | - if isinstance(srv, MCPServer): |
| 420 | + if _is_mcpserver(srv): |
392 | 421 | srv = srv._lowlevel_server # pyright: ignore[reportPrivateUsage] |
393 | | - if isinstance(srv, Server): |
| 422 | + if _is_lowlevel_server(srv): |
394 | 423 | self._connect = _connect_inproc(srv) |
395 | 424 | elif isinstance(srv, str): |
396 | 425 | self._connect = _connect_transport(streamable_http_client(srv)) |
|
0 commit comments