1919from mcp .server .mcpserver import MCPServer
2020from mcp .server .runner import modern_on_request
2121from mcp .shared .direct_dispatcher import create_direct_dispatcher_pair
22- from mcp .shared .dispatcher import ProgressFnT
23- from mcp .shared .exceptions import MCPDeprecationWarning
22+ from mcp .shared .dispatcher import Dispatcher , ProgressFnT
23+ from mcp .shared .exceptions import MCPDeprecationWarning , MCPError
24+ from mcp .shared .version import HANDSHAKE_PROTOCOL_VERSIONS , MODERN_PROTOCOL_VERSIONS
2425from mcp .types import (
26+ METHOD_NOT_FOUND ,
27+ REQUEST_TIMEOUT ,
2528 CallToolResult ,
2629 CompleteResult ,
2730 EmptyResult ,
2831 GetPromptResult ,
2932 Implementation ,
30- InitializeResult ,
3133 ListPromptsResult ,
3234 ListResourcesResult ,
3335 ListResourceTemplatesResult ,
3840 ReadResourceResult ,
3941 RequestParamsMeta ,
4042 ResourceTemplateReference ,
43+ ServerCapabilities ,
4144)
4245
46+ ConnectMode = Literal ["legacy" , "auto" ] | str
47+ """``mode=`` value: ``"legacy"`` (initialize handshake), ``"auto"`` (discover, fall back to
48+ initialize), or a modern protocol-version string (adopt directly). The ``str`` arm is for
49+ forward-compat; ``Client.__post_init__`` rejects anything outside that set at construction."""
50+
4351
4452def _synthesize_discover (protocol_version : str ) -> types .DiscoverResult :
4553 return types .DiscoverResult (
@@ -119,10 +127,10 @@ async def main():
119127 client_info : Implementation | None = None
120128 """Client implementation info to send to server."""
121129
122- mode : Literal [ "legacy" , "auto" ] | str = "legacy"
130+ mode : ConnectMode = "legacy"
123131 """'legacy' performs the initialize handshake. 'auto' probes server/discover and falls back to initialize()
124- on legacy servers. A protocol-version string (e.g. '2026-07-28') adopts that version directly without a
125- handshake — supply prior_discover to reuse a known DiscoverResult, or omit it to synthesize a minimal one."""
132+ on legacy servers. A modern protocol-version string (e.g. '2026-07-28') adopts that version directly without
133+ a handshake — supply prior_discover to reuse a known DiscoverResult, or omit it to synthesize a minimal one."""
126134
127135 prior_discover : types .DiscoverResult | None = None
128136 """A previously-obtained DiscoverResult to install via .adopt() when mode is a version pin.
@@ -146,60 +154,70 @@ def __post_init__(self) -> None:
146154 else :
147155 self ._transport = self .server
148156
157+ if self .mode not in ("legacy" , "auto" ) and self .mode not in MODERN_PROTOCOL_VERSIONS :
158+ hint = (
159+ f" ({ self .mode !r} is a handshake-era version — use mode='legacy')"
160+ if self .mode in HANDSHAKE_PROTOCOL_VERSIONS
161+ else ""
162+ )
163+ raise ValueError (
164+ f"mode must be 'legacy', 'auto', or one of { list (MODERN_PROTOCOL_VERSIONS )} ; got { self .mode !r} { hint } "
165+ )
166+
167+ async def _build_session (self , exit_stack : AsyncExitStack ) -> ClientSession :
168+ """Set up the dispatcher/transport and return an un-entered ClientSession."""
169+ dispatcher : Dispatcher [Any ] | None
170+ if self ._inproc_server is not None and self .mode != "legacy" :
171+ # Modern in-process path: drive the server through a DirectDispatcher peer-pair
172+ # with one `serve_one` per request — no streams, no initialize handshake.
173+ lifespan_state = await exit_stack .enter_async_context (self ._inproc_server .lifespan (self ._inproc_server ))
174+ client_disp , server_disp = create_direct_dispatcher_pair ()
175+ tg = await exit_stack .enter_async_context (anyio .create_task_group ())
176+ exit_stack .callback (server_disp .close )
177+ on_request = modern_on_request (self ._inproc_server , lifespan_state , raise_exceptions = self .raise_exceptions )
178+ await tg .start (server_disp .run , on_request , _drop_notify )
179+ dispatcher = client_disp
180+ read_stream = write_stream = None
181+ else :
182+ if self ._inproc_server is not None :
183+ transport : Transport = InMemoryTransport (self ._inproc_server , raise_exceptions = self .raise_exceptions )
184+ else :
185+ assert self ._transport is not None
186+ transport = self ._transport
187+ read_stream , write_stream = await exit_stack .enter_async_context (transport )
188+ dispatcher = None
189+ return ClientSession (
190+ read_stream = read_stream ,
191+ write_stream = write_stream ,
192+ dispatcher = dispatcher ,
193+ read_timeout_seconds = self .read_timeout_seconds ,
194+ sampling_callback = self .sampling_callback ,
195+ list_roots_callback = self .list_roots_callback ,
196+ logging_callback = self .logging_callback ,
197+ message_handler = self .message_handler ,
198+ client_info = self .client_info ,
199+ elicitation_callback = self .elicitation_callback ,
200+ )
201+
149202 async def __aenter__ (self ) -> Client :
150203 """Enter the async context manager."""
151204 if self ._session is not None :
152205 raise RuntimeError ("Client is already entered; cannot reenter" )
153206
154207 async with AsyncExitStack () as exit_stack :
155- if self ._inproc_server is not None and self .mode != "legacy" :
156- # Modern in-process path: drive the server through a DirectDispatcher peer-pair
157- # with one `serve_one` per request — no streams, no initialize handshake.
158- lifespan_state = await exit_stack .enter_async_context (self ._inproc_server .lifespan (self ._inproc_server ))
159- client_disp , server_disp = create_direct_dispatcher_pair ()
160- tg = await exit_stack .enter_async_context (anyio .create_task_group ())
161- exit_stack .callback (server_disp .close )
162- on_request = modern_on_request (
163- self ._inproc_server , lifespan_state , raise_exceptions = self .raise_exceptions
164- )
165- await tg .start (server_disp .run , on_request , _drop_notify )
166- session = ClientSession (
167- dispatcher = client_disp ,
168- read_timeout_seconds = self .read_timeout_seconds ,
169- sampling_callback = self .sampling_callback ,
170- list_roots_callback = self .list_roots_callback ,
171- logging_callback = self .logging_callback ,
172- message_handler = self .message_handler ,
173- client_info = self .client_info ,
174- elicitation_callback = self .elicitation_callback ,
175- )
176- else :
177- if self ._inproc_server is not None :
178- transport : Transport = InMemoryTransport (
179- self ._inproc_server , raise_exceptions = self .raise_exceptions
180- )
181- else :
182- assert self ._transport is not None
183- transport = self ._transport
184- read_stream , write_stream = await exit_stack .enter_async_context (transport )
185- session = ClientSession (
186- read_stream = read_stream ,
187- write_stream = write_stream ,
188- read_timeout_seconds = self .read_timeout_seconds ,
189- sampling_callback = self .sampling_callback ,
190- list_roots_callback = self .list_roots_callback ,
191- logging_callback = self .logging_callback ,
192- message_handler = self .message_handler ,
193- client_info = self .client_info ,
194- elicitation_callback = self .elicitation_callback ,
195- )
196-
208+ session = await self ._build_session (exit_stack )
197209 self ._session = await exit_stack .enter_async_context (session )
198210
199211 if self .mode == "legacy" :
200212 await self ._session .initialize ()
201213 elif self .mode == "auto" :
202- await self ._session .discover ()
214+ try :
215+ await self ._session .discover ()
216+ except MCPError as e :
217+ if e .code in (METHOD_NOT_FOUND , REQUEST_TIMEOUT ):
218+ await self ._session .initialize ()
219+ else :
220+ raise
203221 else :
204222 self ._session .adopt (self .prior_discover or _synthesize_discover (self .mode ))
205223
@@ -227,16 +245,30 @@ def session(self) -> ClientSession:
227245 return self ._session
228246
229247 @property
230- def initialize_result (self ) -> InitializeResult :
231- """The server's InitializeResult.
248+ def protocol_version (self ) -> str :
249+ """Negotiated protocol version (set by initialize/discover/adopt during ``__aenter__``)."""
250+ version = self .session .protocol_version
251+ assert version is not None
252+ return version
232253
233- Contains server_info, capabilities, instructions, and the negotiated protocol_version.
234- Raises RuntimeError if accessed outside the context manager.
235- """
236- result = self .session .initialize_result
237- if result is None : # pragma: no cover
238- raise RuntimeError ("Client must be used within an async context manager" )
239- return result
254+ @property
255+ def server_info (self ) -> Implementation :
256+ """Server name/version (set by initialize/discover/adopt during ``__aenter__``)."""
257+ info = self .session .server_info
258+ assert info is not None
259+ return info
260+
261+ @property
262+ def server_capabilities (self ) -> ServerCapabilities :
263+ """Server capabilities (set by initialize/discover/adopt during ``__aenter__``)."""
264+ caps = self .session .server_capabilities
265+ assert caps is not None
266+ return caps
267+
268+ @property
269+ def instructions (self ) -> str | None :
270+ """Server-provided instructions text, if any."""
271+ return self .session .instructions
240272
241273 async def send_ping (self , * , meta : RequestParamsMeta | None = None ) -> EmptyResult :
242274 """Send a ping request to the server."""
0 commit comments