Skip to content
Draft
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
622aed5
Resolve the mcp package's exports lazily (PEP 562)
maxisbey Jul 29, 2026
2128b57
Resolve submodules of mcp.client, mcp.server, mcp.shared and mcp.os o…
maxisbey Jul 29, 2026
1c1b7f8
Stop mcp.client from importing the server stack
maxisbey Jul 29, 2026
1e00f42
Load httpx2 with the HTTP transports only
maxisbey Jul 29, 2026
5b25303
Keep typing.get_type_hints(Client) resolvable at runtime
maxisbey Jul 29, 2026
fcaf297
Make server-side imports pay-for-what-you-use
maxisbey Jul 29, 2026
3ac881f
Test HttpResource.read() instead of excluding it from coverage
maxisbey Jul 29, 2026
0237ed9
Load per-version wire packages lazily from the methods surface maps
maxisbey Jul 29, 2026
2a7fb0c
Keep PrimitiveSchemaDefinition reachable on mcp.server.elicitation
maxisbey Jul 29, 2026
ff6c36e
Add an import-cost ratchet for the SDK's entry points
maxisbey Jul 29, 2026
2a13e6d
Keep the Client type-hints test green on Python 3.10
maxisbey Jul 29, 2026
1f3fbc7
Use one lazy-attribute helper for the packages, invisible to type che…
maxisbey Jul 29, 2026
cdc78fc
Keep the public server signatures resolvable by typing.get_type_hints
maxisbey Jul 29, 2026
9ef39b6
Load the OAuth provider stack and cryptography with their first user,…
maxisbey Jul 29, 2026
695b1ab
Keep the wire packages statically discoverable to bundlers
maxisbey Jul 29, 2026
b8bc17d
Defer building the generated wire models and emit them in dependency …
maxisbey Jul 29, 2026
08f4385
Defer building the monolith protocol models and routing adapters
maxisbey Jul 29, 2026
45bb6c9
Defer building the SDK's own eager pydantic models and adapters
maxisbey Jul 29, 2026
1facb57
Document the import-cost contract and the deferred-work model
maxisbey Jul 29, 2026
d515c85
Cover the deferred-signature edge cases: instance access and defer_bu…
maxisbey Jul 29, 2026
8c9f2a2
Serialize the deferred first build of the SDK's pydantic models
maxisbey Jul 29, 2026
33471c7
Add mcp.warm(): opt-in prewarming for the deferred validators
maxisbey Jul 29, 2026
7fae576
Document the deferred-work bills and correct the import-cost bullets
maxisbey Jul 29, 2026
53a46e6
Construct DEFAULT_CLIENT_INFO without building the Implementation model
maxisbey Jul 29, 2026
986d3b9
Import the internal deferred_model decorator under a private name
maxisbey Jul 29, 2026
e02caca
Fail generation if a non-class statement sits between generated classes
maxisbey Jul 29, 2026
fc4f4e6
Cover the decorator's refusal paths and the runtime-only lazy-init br…
maxisbey Jul 29, 2026
b592f09
Generate a deferred model's JSON schema under the rebuild lock too
maxisbey Jul 29, 2026
6da7ba4
Spell warm()'s every-version flag as all_versions and return a frozen…
maxisbey Jul 29, 2026
008c74e
Document the prewarm recipe and the rebuild lock's terms
maxisbey Jul 29, 2026
cb7c68c
Resolve attribute chains through the four leaf sub-packages too
maxisbey Jul 29, 2026
2716d09
Pin the decorator's refusal messages and describe two wire-base tests
maxisbey Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
maxisbey committed Jul 29, 2026
commit 1c1b7f83c17778d46c6fa21daeb989eb5ee89728
45 changes: 37 additions & 8 deletions src/mcp/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import hashlib
import logging
import sys
import uuid
from collections.abc import Awaitable, Callable, Mapping, Sequence
from contextlib import AbstractAsyncContextManager, AsyncExitStack
from dataclasses import KW_ONLY, dataclass, field
from typing import Any, Literal, TypeVar, cast
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast

import anyio
import anyio.lowlevel
Expand Down Expand Up @@ -40,10 +41,9 @@
ServerCapabilities,
)
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS
from typing_extensions import deprecated
from typing_extensions import TypeIs, deprecated

from mcp.client._input_required import DEFAULT_INPUT_REQUIRED_MAX_ROUNDS, run_input_required_driver
from mcp.client._memory import InMemoryTransport
from mcp.client._probe import negotiate_auto
from mcp.client._transport import Transport
from mcp.client.caching import CacheConfig, CacheMode, ClientResponseCache, InMemoryResponseCacheStore
Expand All @@ -61,16 +61,20 @@
from mcp.client.streamable_http import streamable_http_client
from mcp.client.subscriptions import ServerEvent, Subscription
from mcp.client.subscriptions import listen as _listen
from mcp.server import Server
from mcp.server.mcpserver import MCPServer
from mcp.server.runner import modern_on_request
from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair
from mcp.shared.dispatcher import Dispatcher, ProgressFnT
from mcp.shared.exceptions import MCPDeprecationWarning, MCPError
from mcp.shared.extension import validate_extension_identifier
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
from mcp.shared.subscriptions import event_to_notification

if TYPE_CHECKING:
# Typing-only: the server stack is never imported by the client at runtime.
# An in-process `Server`/`MCPServer` argument already implies the caller
# imported it (see `_is_lowlevel_server`); URL/`Transport` clients pay nothing.
from mcp.server import Server
from mcp.server.mcpserver import MCPServer

logger = logging.getLogger(__name__)

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


def _loaded_class(module: str, name: str) -> type | None:
"""Return class `name` from `module` if that module has already been imported, else `None`.

A `Server`/`MCPServer` instance can only exist once its defining module has run, so
looking the class up in `sys.modules` (never importing it) gives an exact `isinstance`
test while keeping the whole server stack out of a transport-only client's imports.
"""
return getattr(sys.modules.get(module), name, None)


def _is_mcpserver(obj: object) -> TypeIs[MCPServer]:
cls = _loaded_class("mcp.server.mcpserver.server", "MCPServer")
return cls is not None and isinstance(obj, cls)


def _is_lowlevel_server(obj: object) -> TypeIs[Server[Any]]:
cls = _loaded_class("mcp.server.lowlevel.server", "Server")
return cls is not None and isinstance(obj, cls)


def _connect_inproc(server: Server[Any]) -> _Connector:
"""Connector for an in-process ``Server``: legacy mode drives the stream loop via
``InMemoryTransport``; any other mode drives the modern per-request path through a
``DirectDispatcher`` peer pair (no streams, no JSON-RPC framing, no initialize handshake)."""
# Function-level imports (documented exception): this connector is only built for a live
# in-process `Server`, whose stack the caller has therefore already imported; binding these
# here rather than at module top keeps `import mcp.client` free of the server stack.
from mcp.client._memory import InMemoryTransport
from mcp.server.runner import modern_on_request

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

srv = self.server
if isinstance(srv, MCPServer):
if _is_mcpserver(srv):
srv = srv._lowlevel_server # pyright: ignore[reportPrivateUsage]
if isinstance(srv, Server):
if _is_lowlevel_server(srv):
self._connect = _connect_inproc(srv)
elif isinstance(srv, str):
self._connect = _connect_transport(streamable_http_client(srv))
Expand Down