Skip to content

Commit 29d33eb

Browse files
committed
auto mode: legacy-only discover advertisement falls back to initialize
A server that answers server/discover but advertises no modern version in supportedVersions (go-sdk's default stateful streamable server does this) previously made adopt() raise 'No mutually supported modern protocol version'. On the old schema this path was unreachable by accident: the reply lacked the then-required body serverInfo, failed validation, and fell into the unparseable-result fallback. Making serverInfo optional exposed the raise. Treat the reply as what it is, an explicit legacy advertisement, and run the initialize handshake, matching the -32022-with-legacy-versions branch directly above and the behavior of both the typescript and go clients. adopt() itself stays strict for direct prior_discover use.
1 parent 34ab5e4 commit 29d33eb

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

src/mcp/client/_probe.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
``supported`` list. The streamable-HTTP transport already maps HTTP-layer
1010
4xx rejections (no JSON-RPC body) into ``MCPError`` codes, so those reach
1111
the same path. Any non-``MCPError`` exception (network/connection errors,
12-
anyio cancellation, the ``RuntimeError`` from ``adopt()`` on no-mutual)
13-
propagates to the caller; an outage or in-process bug is never an era verdict.
12+
anyio cancellation) propagates to the caller; an outage or in-process bug
13+
is never an era verdict.
14+
15+
A successful ``DiscoverResult`` whose ``supportedVersions`` shares no modern
16+
version with this client is treated the same way: the server speaks discover
17+
but advertises only handshake-era versions, which is a legacy advertisement,
18+
not an incompatibility.
1419
1520
The fallback handshake itself can be answered with ``-32022`` — e.g. a probe
1621
that timed out client-side but succeeded on a slow-starting server locked the
@@ -89,13 +94,21 @@ async def negotiate_auto(session: ClientSession) -> None:
8994
version = mutual[-1]
9095
continue
9196
return
92-
# any other exception (httpx2.TransportError, ConnectionError, anyio errors,
93-
# RuntimeError from adopt) → propagate
97+
# any other exception (httpx2.TransportError, ConnectionError,
98+
# anyio errors) → propagate
9499
try:
95100
result = types.DiscoverResult.model_validate(raw)
96101
except ValidationError:
97102
await session.initialize() # unparseable result → not modern evidence
98103
return
104+
if not any(v in result.supported_versions for v in MODERN_PROTOCOL_VERSIONS):
105+
# A discover-answering server that advertises no modern version
106+
# (go-sdk's stateful streamable default does this) is an explicit
107+
# legacy advertisement: fall back like the -32022 branch above
108+
# instead of letting `adopt()` raise. The ts and go clients fall
109+
# back here too.
110+
await session.initialize()
111+
return
99112
session.adopt(result)
100113
return
101114
raise AssertionError("unreachable") # pragma: no cover — loop body always returns or raises

tests/client/test_probe.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ async def test_an_unparseable_discover_result_falls_back_to_initialize() -> None
121121
assert session.adopted is None
122122

123123

124+
async def test_a_discover_result_advertising_only_legacy_versions_falls_back_to_initialize() -> None:
125+
"""A server that answers the probe but lists no modern version has made an
126+
explicit legacy advertisement (go-sdk's default stateful streamable server
127+
does this), so the policy runs the handshake instead of raising on adopt."""
128+
session = _StubSession(_discover_dict(list(HANDSHAKE_PROTOCOL_VERSIONS)))
129+
await _negotiate(session)
130+
assert session.initialized
131+
assert session.adopted is None
132+
133+
124134
# --- the denylist: every JSON-RPC error code falls back ---
125135

126136

0 commit comments

Comments
 (0)