"""Connect-time era negotiation for ``mode='auto'``. The ``server/discover`` probe is sent at the newest modern version. Anything that is not positive evidence the peer is a modern MCP server falls back to the legacy ``initialize`` handshake — a *denylist* (only the disjoint-modern case raises) rather than an allowlist of fallback codes. Every ``MCPError`` falls back except ``-32022`` with a disjoint modern-only ``supported`` list. The streamable-HTTP transport already maps HTTP-layer 4xx rejections (no JSON-RPC body) into ``MCPError`` codes, so those reach the same path. Any non-``MCPError`` exception (network/connection errors, anyio cancellation) propagates to the caller; an outage or in-process bug is never an era verdict. A successful ``DiscoverResult`` whose ``supportedVersions`` shares no modern version with this client is treated the same way: the server speaks discover but advertises only handshake-era versions, which is a legacy advertisement, not an incompatibility. The fallback handshake itself can be answered with ``-32022`` — e.g. a probe that timed out client-side but succeeded on a slow-starting server locked the connection modern before the pipelined ``initialize`` arrived. That code is itself positive modern evidence (it names the server's versions), so it triggers one re-probe at a mutual version instead of failing the connect. """ from __future__ import annotations from typing import Any import mcp_types as types from mcp_types import UNSUPPORTED_PROTOCOL_VERSION from mcp_types.version import ( HANDSHAKE_PROTOCOL_VERSIONS, LATEST_MODERN_VERSION, MODERN_PROTOCOL_VERSIONS, ) from pydantic import ValidationError from mcp.client.session import ClientSession from mcp.shared.exceptions import MCPError def _parse_supported(data: Any) -> list[str] | None: """Pull ``data.supported`` off a -32022 error, or ``None`` if not actionable.""" try: return types.UnsupportedProtocolVersionErrorData.model_validate(data).supported except ValidationError: return None async def negotiate_auto(session: ClientSession) -> None: """Drive the ``mode='auto'`` connect-time policy on ``session``. Probes ``server/discover`` once (twice if the server names a mutual modern version via -32022), then either ``adopt()``s the result or falls back to ``initialize()``. Idempotent only in the sense that one of ``session.discover_result`` / ``session.initialize_result`` is set on return. Raises: MCPError: The server is modern-only and shares no version with this client (-32022 with a disjoint ``supported`` list), or the fallback handshake failed and one corrective re-probe did too. Exception: Any transport/network error from the probe propagates as-is. """ version = LATEST_MODERN_VERSION for attempt in range(2): try: raw = await session.send_discover(version) except MCPError as e: if e.code == UNSUPPORTED_PROTOCOL_VERSION: supported = _parse_supported(e.error.data) mutual = [v for v in MODERN_PROTOCOL_VERSIONS if v in (supported or ())] if mutual and attempt == 0: version = mutual[-1] continue if supported is not None and not any(v in HANDSHAKE_PROTOCOL_VERSIONS for v in supported): raise # server is modern-only and disjoint — real incompatibility try: await session.initialize() # every other rpc-error → legacy (the denylist) except MCPError as handshake_exc: if handshake_exc.code != UNSUPPORTED_PROTOCOL_VERSION or attempt != 0: raise # -32022 from the handshake is itself modern evidence: a probe # that timed out client-side but succeeded on the server locked # the connection modern before this initialize arrived. Re-probe # once at a version the server names; the era is already # settled, so the second probe answers without the slow start. supported = _parse_supported(handshake_exc.error.data) mutual = [v for v in MODERN_PROTOCOL_VERSIONS if v in (supported or ())] if not mutual: raise version = mutual[-1] continue return # any other exception (httpx2.TransportError, ConnectionError, # anyio errors) → propagate try: result = types.DiscoverResult.model_validate(raw) except ValidationError: await session.initialize() # unparseable result → not modern evidence return if not any(v in result.supported_versions for v in MODERN_PROTOCOL_VERSIONS): # A discover-answering server that advertises no modern version # (go-sdk's stateful streamable default does this) is an explicit # legacy advertisement: fall back like the -32022 branch above # instead of letting `adopt()` raise. The ts and go clients fall # back here too. await session.initialize() return session.adopt(result) return raise AssertionError("unreachable") # pragma: no cover — loop body always returns or raises