"""MCP 2026-07-28 protocol-era negotiation (_negotiate_session). The negotiation helper decides between the legacy ``initialize`` handshake and the stateless ``server/discover`` probe (SEP-2575) per the per-server ``protocol`` config key. These tests drive it with duck-typed sessions — the live-path integration is covered by the real-server E2E in the PR. """ import asyncio import pytest from tools.mcp_tool import ( MCPServerTask, _handshake_rejected_as_modern, _JSONRPC_UNSUPPORTED_PROTOCOL_VERSION, ) class _Err(Exception): def __init__(self, code, msg="err"): super().__init__(msg) self.error = type("E", (), {"code": code})() class _Session: def __init__(self, init=None, disc=None): self._init = init self._disc = disc self.calls = [] async def initialize(self): self.calls.append("initialize") if isinstance(self._init, Exception): raise self._init return self._init async def discover(self): self.calls.append("discover") if isinstance(self._disc, Exception): raise self._disc return self._disc class _LegacySession: """mcp 1.x sessions have no discover() attribute at all.""" def __init__(self, init=None): self._init = init self.calls = [] async def initialize(self): self.calls.append("initialize") if isinstance(self._init, Exception): raise self._init return self._init def _task(protocol=None): t = MCPServerTask("negotest") t._config = {} if protocol is None else {"protocol": protocol} return t def _run(coro): return asyncio.new_event_loop().run_until_complete(coro) class TestAutoMode: def test_handshake_first_no_discover_on_success(self): s = _Session(init="INIT_RESULT") out = _run(_task()._negotiate_session(s, 5)) assert out == "INIT_RESULT" assert s.calls == ["initialize"] def test_falls_back_to_discover_on_unsupported_protocol_version(self): s = _Session(init=_Err(_JSONRPC_UNSUPPORTED_PROTOCOL_VERSION), disc="DISC_RESULT") out = _run(_task()._negotiate_session(s, 5)) assert out == "DISC_RESULT" assert s.calls == ["initialize", "discover"] def test_falls_back_on_method_not_found(self): s = _Session(init=_Err(-32601, "Method not found: initialize"), disc="DISC_RESULT") out = _run(_task()._negotiate_session(s, 5)) assert out == "DISC_RESULT" def test_unrelated_error_propagates_without_discover(self): s = _Session(init=_Err(-32000, "borked")) with pytest.raises(_Err): _run(_task()._negotiate_session(s, 5)) assert s.calls == ["initialize"] def test_timeout_propagates_not_swallowed(self): class _Hang(_Session): async def initialize(self): await asyncio.sleep(30) with pytest.raises(asyncio.TimeoutError): _run(_task()._negotiate_session(_Hang(), 0.05)) class TestExplicitModes: def test_stateless_probes_discover_first(self): s = _Session(init="INIT_RESULT", disc="DISC_RESULT") out = _run(_task("stateless")._negotiate_session(s, 5)) assert out == "DISC_RESULT" assert s.calls == ["discover"] def test_stateless_falls_back_to_handshake(self): s = _Session(init="INIT_RESULT", disc=_Err(-32601)) out = _run(_task("stateless")._negotiate_session(s, 5)) assert out == "INIT_RESULT" assert s.calls == ["discover", "initialize"] def test_legacy_never_discovers(self): s = _Session(init=_Err(_JSONRPC_UNSUPPORTED_PROTOCOL_VERSION), disc="DISC_RESULT") with pytest.raises(_Err): _run(_task("legacy")._negotiate_session(s, 5)) assert s.calls == ["initialize"] def test_unknown_mode_treated_as_auto(self): s = _Session(init="INIT_RESULT") out = _run(_task("bogus")._negotiate_session(s, 5)) assert out == "INIT_RESULT" def test_legacy_sdk_session_without_discover_reraises(self): # mcp 1.x ClientSession has no .discover(): the auto fallback must # re-raise the original handshake error, not AttributeError. s = _LegacySession(init=_Err(_JSONRPC_UNSUPPORTED_PROTOCOL_VERSION)) with pytest.raises(_Err): _run(_task()._negotiate_session(s, 5)) assert s.calls == ["initialize"] class TestModernRejectionClassifier: def test_structural_codes(self): assert _handshake_rejected_as_modern(_Err(-32022)) assert _handshake_rejected_as_modern(_Err(-32601)) assert not _handshake_rejected_as_modern(_Err(-32000)) def test_substring_fallbacks(self): assert _handshake_rejected_as_modern(Exception("Unsupported protocol version")) assert _handshake_rejected_as_modern(Exception("Unknown method: initialize")) assert not _handshake_rejected_as_modern(Exception("connection reset by peer"))