Files
kefu/wechat_rpa/.venv/Lib/site-packages/mcp/client/_probe.py
T
2026-09-21 10:34:06 +08:00

115 lines
5.3 KiB
Python

"""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