2424 INVALID_REQUEST ,
2525 METHOD_NOT_FOUND ,
2626 PARSE_ERROR ,
27+ REQUEST_TIMEOUT ,
2728 UNSUPPORTED_PROTOCOL_VERSION ,
2829 Implementation ,
2930 ServerCapabilities ,
@@ -45,12 +46,16 @@ class _StubSession:
4546 """Minimal stand-in for `ClientSession` exposing only what `negotiate_auto` touches.
4647
4748 `send_discover` plays back a script (raise an exception, or return a dict);
48- `initialize` and `adopt` just record that they were called.
49+ `initialize` raises the next entry of an optional `handshake` exception
50+ script (succeeding once it is exhausted) and records its calls; `adopt`
51+ just records.
4952 """
5053
51- def __init__ (self , * script : dict [str , Any ] | Exception ) -> None :
54+ def __init__ (self , * script : dict [str , Any ] | Exception , handshake : list [ Exception ] | None = None ) -> None :
5255 self ._script : list [dict [str , Any ] | Exception ] = list (script )
56+ self ._handshake : list [Exception ] = list (handshake or [])
5357 self .probed_at : list [str ] = []
58+ self .initialize_calls : int = 0
5459 self .initialized : bool = False
5560 self .adopted : types .DiscoverResult | None = None
5661
@@ -62,6 +67,9 @@ async def send_discover(self, version: str) -> dict[str, Any]:
6267 return step
6368
6469 async def initialize (self ) -> None :
70+ self .initialize_calls += 1
71+ if self ._handshake :
72+ raise self ._handshake .pop (0 )
6573 self .initialized = True
6674
6775 def adopt (self , result : types .DiscoverResult ) -> None :
@@ -201,6 +209,77 @@ async def test_a_second_unsupported_version_after_the_corrective_retry_does_not_
201209 assert session .adopted is None
202210
203211
212+ # --- -32022 from the fallback handshake: modern evidence, one re-probe ---
213+
214+
215+ async def test_handshake_unsupported_after_a_timed_out_probe_reprobes_and_adopts () -> None :
216+ """A probe that times out client-side but succeeds on a slow-starting
217+ server locks the connection modern, so the fallback handshake answers
218+ -32022. That code is itself modern evidence: re-probe once at a version
219+ the server names and adopt - the connect must not fail."""
220+ session = _StubSession (
221+ MCPError (code = REQUEST_TIMEOUT , message = "Request 'server/discover' timed out" ),
222+ _discover_dict (),
223+ handshake = [_err_32022 (list (MODERN_PROTOCOL_VERSIONS ))],
224+ )
225+ await _negotiate (session )
226+ assert session .probed_at == [LATEST_MODERN_VERSION , MODERN_PROTOCOL_VERSIONS [- 1 ]]
227+ assert session .adopted is not None
228+ assert session .initialize_calls == 1
229+ assert not session .initialized
230+
231+
232+ @pytest .mark .parametrize (
233+ "data" ,
234+ [
235+ pytest .param ({"supported" : ["2099-01-01" ], "requested" : LATEST_MODERN_VERSION }, id = "disjoint" ),
236+ pytest .param (None , id = "no-data" ),
237+ ],
238+ )
239+ async def test_handshake_unsupported_without_a_mutual_version_reraises (data : Any ) -> None :
240+ """-32022 from the handshake naming no version we speak (or nothing
241+ parseable) leaves nothing to retry with - the error propagates."""
242+ session = _StubSession (
243+ MCPError (code = METHOD_NOT_FOUND , message = "nope" ),
244+ handshake = [MCPError (code = UNSUPPORTED_PROTOCOL_VERSION , message = "already modern" , data = data )],
245+ )
246+ with pytest .raises (MCPError ) as exc_info :
247+ await _negotiate (session )
248+ assert exc_info .value .code == UNSUPPORTED_PROTOCOL_VERSION
249+ assert session .adopted is None
250+ assert not session .initialized
251+
252+
253+ async def test_handshake_unsupported_reprobes_at_most_once () -> None :
254+ """The handshake-driven re-probe is bounded: if the second attempt also
255+ ends in a timed-out probe and a -32022 handshake, the -32022 propagates
256+ instead of looping."""
257+ timeout = MCPError (code = REQUEST_TIMEOUT , message = "Request 'server/discover' timed out" )
258+ session = _StubSession (
259+ timeout ,
260+ timeout ,
261+ handshake = [_err_32022 (list (MODERN_PROTOCOL_VERSIONS )), _err_32022 (list (MODERN_PROTOCOL_VERSIONS ))],
262+ )
263+ with pytest .raises (MCPError ) as exc_info :
264+ await _negotiate (session )
265+ assert exc_info .value .code == UNSUPPORTED_PROTOCOL_VERSION
266+ assert session .probed_at == [LATEST_MODERN_VERSION , MODERN_PROTOCOL_VERSIONS [- 1 ]]
267+ assert session .initialize_calls == 2
268+
269+
270+ async def test_any_other_handshake_error_propagates_unchanged () -> None :
271+ """A non--32022 error from the fallback handshake is a real handshake
272+ failure, not era evidence - it propagates without a re-probe."""
273+ session = _StubSession (
274+ MCPError (code = METHOD_NOT_FOUND , message = "nope" ),
275+ handshake = [MCPError (code = INTERNAL_ERROR , message = "handshake broke" )],
276+ )
277+ with pytest .raises (MCPError ) as exc_info :
278+ await _negotiate (session )
279+ assert exc_info .value .code == INTERNAL_ERROR
280+ assert session .probed_at == [LATEST_MODERN_VERSION ]
281+
282+
204283# --- non-MCP errors propagate ---
205284
206285
0 commit comments