diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 35cae2d314..247ad5302b 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -78,6 +78,17 @@ def _clamp_inbound_ttl(raw: dict[str, Any]) -> None: raw["ttlMs"] = 0 +def _default_inbound_complete_result(raw: dict[str, Any], version: str) -> dict[str, Any]: + if ( + version not in MODERN_PROTOCOL_VERSIONS + or "resultType" in raw + or "inputRequests" in raw + or "requestState" in raw + ): + return raw + return {"resultType": "complete", **raw} + + @cache def _wire_fields(target: type[BaseModel] | UnionType) -> frozenset[str]: """Top-level wire keys `target` declares (its members', for a union).""" @@ -587,6 +598,7 @@ async def send_request( _clamp_inbound_ttl(raw) # Literal fallback covers pre-handshake and stateless; matches runner.py. version = self._negotiated_version or "2025-11-25" + raw = _default_inbound_complete_result(raw, version) try: _methods.validate_server_result(method, version, raw) except KeyError: diff --git a/tests/client/test_result_type_compat.py b/tests/client/test_result_type_compat.py new file mode 100644 index 0000000000..a6cc5a60a7 --- /dev/null +++ b/tests/client/test_result_type_compat.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import pytest + +from mcp.client.session import _default_inbound_complete_result +from mcp_types import methods + + +def test_modern_missing_discriminator_is_a_complete_result_for_the_client() -> None: + original = {"content": [{"type": "text", "text": "ok"}]} + normalized = _default_inbound_complete_result(original, "2026-07-28") + assert original == {"content": [{"type": "text", "text": "ok"}]} + assert normalized["resultType"] == "complete" + methods.validate_server_result("tools/call", "2026-07-28", normalized) + + +@pytest.mark.parametrize("body", [ + {"resultType": "input_required", "inputRequests": {"confirm": {}}}, + {"resultType": "task", "content": []}, + {"inputRequests": {"confirm": {}}}, + {"requestState": "sealed"}, + {"resultType": None, "content": []}, +]) +def test_explicit_or_pending_result_types_are_never_downgraded(body: dict) -> None: + assert _default_inbound_complete_result(body, "2026-07-28") == body + + +def test_legacy_wire_is_never_rewritten() -> None: + body = {"content": []} + assert _default_inbound_complete_result(body, "2025-11-25") is body