chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
+469
View File
@@ -0,0 +1,469 @@
"""`ClientSession` result claims: construction validation, activation at modern
adopts only, claimed-result routing, the version-aware capability ad, and the
`allow_claimed` escape hatch."""
from collections.abc import Mapping
from typing import Any, Literal
import anyio
import anyio.abc
import mcp_types as types
import pytest
from inline_snapshot import snapshot
from mcp_types import (
CLIENT_CAPABILITIES_META_KEY,
CallToolResult,
Implementation,
InputRequiredResult,
ListToolsResult,
Result,
ServerCapabilities,
TextContent,
Tool,
)
from mcp_types.methods import validate_server_result
from mcp_types.version import LATEST_HANDSHAKE_VERSION, LATEST_MODERN_VERSION
from pydantic import ValidationError
from typing_extensions import assert_type
from mcp.client.extension import ClaimContext, ResultClaim, UnexpectedClaimedResult
from mcp.client.session import ClientSession, _CallToolResultAdapter
from mcp.shared.dispatcher import CallOptions, OnNotify, OnNotifyIntercept, OnRequest
_TASKS_EXT = "com.example/tasks"
_AD_ONLY_EXT = "com.example/flags"
class _TaskResult(Result):
"""A claimed result shape, tagged `task`."""
result_type: Literal["task"] = "task"
task_id: str
async def _resolve_task(result: _TaskResult, ctx: ClaimContext) -> CallToolResult:
raise NotImplementedError # session-tier tests never drive a resolver; that is the Client's job
def _task_claim(**kwargs: Any) -> ResultClaim[_TaskResult]:
return ResultClaim(result_type="task", model=_TaskResult, resolve=_resolve_task, **kwargs)
_COMPLETE_TOOL_RESULT = CallToolResult(content=[TextContent(type="text", text="ok")]).model_dump(
by_alias=True, mode="json", exclude_none=True
)
_CLAIMED_TASK_RESULT = {"resultType": "task", "taskId": "t-1"}
_TOOL_LISTING = ListToolsResult(tools=[Tool(name="t", input_schema={"type": "object"})]).model_dump(
by_alias=True, mode="json", exclude_none=True
)
_INITIALIZE_RESULT = types.InitializeResult(
protocol_version=LATEST_HANDSHAKE_VERSION,
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
).model_dump(by_alias=True, mode="json", exclude_none=True)
class _RecordingDispatcher:
"""Records every send and answers each method with a canned result."""
def __init__(self, tool_result: dict[str, Any] | None = None) -> None:
self.calls: list[tuple[str, Mapping[str, Any] | None, CallOptions]] = []
self.notifications: list[str] = []
self._tool_result = tool_result if tool_result is not None else _COMPLETE_TOOL_RESULT
async def run(
self,
on_request: OnRequest,
on_notify: OnNotify,
on_notify_intercept: OnNotifyIntercept | None = None,
*,
task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED,
) -> None:
task_status.started()
await anyio.sleep_forever()
async def send_raw_request(
self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None
) -> dict[str, Any]:
self.calls.append((method, params, opts or {}))
if method == "tools/call":
return self._tool_result
if method == "tools/list":
return _TOOL_LISTING
if method == "initialize":
return _INITIALIZE_RESULT
return {}
async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None:
self.notifications.append(method)
def _claims_session(dispatcher: _RecordingDispatcher, *claims: ResultClaim[Any]) -> ClientSession:
return ClientSession(dispatcher=dispatcher, extensions={_TASKS_EXT: {}}, result_claims={_TASKS_EXT: list(claims)})
def _adopt_modern(session: ClientSession) -> None:
session.adopt(
types.DiscoverResult(
supported_versions=[LATEST_MODERN_VERSION],
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
)
)
def _adopt_handshake(session: ClientSession) -> None:
session.adopt(
types.InitializeResult(
protocol_version=LATEST_HANDSHAKE_VERSION,
capabilities=ServerCapabilities(),
server_info=Implementation(name="stub", version="0"),
)
)
def test_duplicate_claim_tag_across_extensions_rejected() -> None:
"""SDK-defined: two claims on the same resultType cannot be routed apart, so construction fails."""
with pytest.raises(ValueError) as exc_info:
ClientSession(
dispatcher=_RecordingDispatcher(),
extensions={_TASKS_EXT: {}, _AD_ONLY_EXT: {}},
result_claims={_TASKS_EXT: [_task_claim()], _AD_ONLY_EXT: [_task_claim()]},
)
assert str(exc_info.value) == snapshot("duplicate result claim for resultType 'task'")
def test_claims_keyed_to_unadvertised_extension_rejected() -> None:
"""SDK-defined: a `result_claims` key with no `extensions` entry advertises nothing, so construction fails."""
messages: list[str] = []
for extensions in (None, {_AD_ONLY_EXT: {"flag": True}}):
with pytest.raises(ValueError) as exc_info:
ClientSession(
dispatcher=_RecordingDispatcher(),
extensions=extensions,
result_claims={_TASKS_EXT: [_task_claim()]},
)
messages.append(str(exc_info.value))
assert messages == snapshot(
[
"result_claims key 'com.example/tasks' has no extensions entry; a claim is only "
"advertised through its extension's capability ad",
"result_claims key 'com.example/tasks' has no extensions entry; a claim is only "
"advertised through its extension's capability ad",
]
)
def test_empty_claim_sequence_rejected() -> None:
"""SDK-defined: an empty claim list is rejected at construction; a claim-less extension omits the key."""
with pytest.raises(ValueError) as exc_info:
ClientSession(dispatcher=_RecordingDispatcher(), extensions={_TASKS_EXT: {}}, result_claims={_TASKS_EXT: []})
assert str(exc_info.value) == snapshot(
"result_claims['com.example/tasks'] is empty and would drop the extension from "
"the capability ad at every version. Omit the key instead"
)
def test_empty_settings_count_as_an_advertised_extension() -> None:
"""SDK-defined: empty settings ({}) still count as an ad, so claims keyed to the extension construct."""
session = _claims_session(_RecordingDispatcher(), _task_claim())
assert isinstance(session, ClientSession)
def test_without_claims_the_call_tool_adapter_is_the_module_constant() -> None:
"""SDK-defined: with zero active claims the session holds the module-level adapter by identity."""
session = ClientSession(dispatcher=_RecordingDispatcher())
assert session._call_tool_adapter is _CallToolResultAdapter
_adopt_modern(session)
assert session._call_tool_adapter is _CallToolResultAdapter
_adopt_handshake(session)
assert session._call_tool_adapter is _CallToolResultAdapter
@pytest.mark.anyio
@pytest.mark.parametrize("protocol_versions", [None, frozenset({LATEST_MODERN_VERSION})])
async def test_modern_adopt_activates_claims_and_routes_claimed_results(
protocol_versions: frozenset[str] | None,
) -> None:
"""SDK-defined: at a modern adopt, a claim active at the negotiated version routes
the claimed raw to the claim model."""
dispatcher = _RecordingDispatcher(tool_result=_CLAIMED_TASK_RESULT)
session = _claims_session(dispatcher, _task_claim(protocol_versions=protocol_versions))
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
result = await session.call_tool("t", {}, allow_claimed=True)
assert isinstance(result, _TaskResult)
assert result.task_id == "t-1"
@pytest.mark.anyio
async def test_legacy_adopt_clears_active_claims() -> None:
"""SDK-defined: a legacy adopt clears active claims and restores the module-level adapter."""
dispatcher = _RecordingDispatcher(tool_result=_CLAIMED_TASK_RESULT)
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
assert isinstance(await session.call_tool("t", {}, allow_claimed=True), _TaskResult)
_adopt_handshake(session)
assert session._call_tool_adapter is _CallToolResultAdapter
with pytest.raises(ValidationError):
await session.call_tool("t", {}, allow_claimed=True)
# Rejected at response parsing; the request did reach the wire.
assert dispatcher.calls[-1][0] == "tools/call"
@pytest.mark.anyio
async def test_modern_readopt_after_legacy_reactivates_claims() -> None:
"""SDK-defined: a modern re-adopt after legacy reactivates the claims."""
dispatcher = _RecordingDispatcher(tool_result=_CLAIMED_TASK_RESULT)
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
_adopt_handshake(session)
assert session._call_tool_adapter is _CallToolResultAdapter
_adopt_modern(session)
result = await session.call_tool("t", {}, allow_claimed=True)
assert isinstance(result, _TaskResult)
assert session._call_tool_adapter is not _CallToolResultAdapter
@pytest.mark.anyio
async def test_legacy_initialize_ad_drops_claim_bearing_identifiers() -> None:
"""SDK-defined: the legacy initialize ad drops claim-bearing identifiers; ad-only ones ride along."""
dispatcher = _RecordingDispatcher()
session = ClientSession(
dispatcher=dispatcher,
extensions={_TASKS_EXT: {}, _AD_ONLY_EXT: {"flag": True}},
result_claims={_TASKS_EXT: [_task_claim()]},
)
with anyio.fail_after(5):
async with session:
await session.initialize()
[(_, params, _)] = [call for call in dispatcher.calls if call[0] == "initialize"]
assert params is not None
assert params["capabilities"]["extensions"] == {_AD_ONLY_EXT: {"flag": True}}
@pytest.mark.anyio
async def test_legacy_ad_omits_extensions_entirely_when_every_identifier_drops() -> None:
"""SDK-defined: when every identifier drops, the ad omits the `extensions` key entirely."""
dispatcher = _RecordingDispatcher()
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
await session.initialize()
[(_, params, _)] = [call for call in dispatcher.calls if call[0] == "initialize"]
assert params is not None
assert "extensions" not in params["capabilities"]
@pytest.mark.anyio
async def test_modern_adopt_ad_includes_active_claim_identifiers() -> None:
"""SDK-defined: the modern per-request `_meta` ad includes identifiers whose claims are active."""
dispatcher = _RecordingDispatcher()
session = ClientSession(
dispatcher=dispatcher,
extensions={_TASKS_EXT: {}, _AD_ONLY_EXT: {"flag": True}},
result_claims={_TASKS_EXT: [_task_claim()]},
)
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
await session.send_ping()
[(_, params, _)] = dispatcher.calls
assert params is not None
capabilities = params["_meta"][CLIENT_CAPABILITIES_META_KEY]
assert capabilities["extensions"] == {_TASKS_EXT: {}, _AD_ONLY_EXT: {"flag": True}}
@pytest.mark.anyio
async def test_discover_probe_ad_includes_claim_identifiers_at_the_probe_version() -> None:
"""SDK-defined: `send_discover` builds its `_meta` ad at the probe version, where claims are active."""
dispatcher = _RecordingDispatcher()
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
await session.send_discover(LATEST_MODERN_VERSION)
[(_, params, _)] = dispatcher.calls
assert params is not None
capabilities = params["_meta"][CLIENT_CAPABILITIES_META_KEY]
assert capabilities["extensions"] == {_TASKS_EXT: {}}
@pytest.mark.anyio
async def test_discover_probe_ad_drops_claim_identifiers_at_a_legacy_probe_version() -> None:
"""SDK-defined: at a legacy probe version no claim can be active, so the identifier drops."""
dispatcher = _RecordingDispatcher()
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
await session.send_discover(LATEST_HANDSHAKE_VERSION)
[(_, params, _)] = dispatcher.calls
assert params is not None
capabilities = params["_meta"][CLIENT_CAPABILITIES_META_KEY]
assert "extensions" not in capabilities
class _CoreTaggedResult(Result):
"""A claim whose wire tag collides with the adapter's internal routing sentinel."""
result_type: Literal["core"] = "core"
payload: str = ""
async def _resolve_core_tagged(result: _CoreTaggedResult, ctx: ClaimContext) -> CallToolResult:
raise NotImplementedError
@pytest.mark.anyio
async def test_claim_tagged_core_cannot_hijack_core_parsing() -> None:
"""SDK-defined: a claim may use "core" as its wire tag without colliding with core parsing."""
claim = ResultClaim(result_type="core", model=_CoreTaggedResult, resolve=_resolve_core_tagged)
dispatcher = _RecordingDispatcher(tool_result={"resultType": "core", "payload": "p-1"})
session = ClientSession(dispatcher=dispatcher, extensions={_TASKS_EXT: {}}, result_claims={_TASKS_EXT: [claim]})
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
ordinary = session._call_tool_adapter.validate_python(_COMPLETE_TOOL_RESULT)
claimed = await session.call_tool("t", {}, allow_claimed=True)
assert isinstance(ordinary, CallToolResult)
assert isinstance(claimed, _CoreTaggedResult)
@pytest.mark.anyio
@pytest.mark.parametrize("with_claims", [True, False])
async def test_unknown_result_type_fails_validation_with_and_without_claims(with_claims: bool) -> None:
"""SDK-defined: a resultType outside the active claim set fails core validation, claims or not."""
raw = {"resultType": "weird", "taskId": "t-1"}
dispatcher = _RecordingDispatcher(tool_result=raw)
session = _claims_session(dispatcher, _task_claim()) if with_claims else ClientSession(dispatcher=dispatcher)
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
with pytest.raises(ValidationError):
await session.call_tool("t", {}, allow_claimed=True)
# Rejected at response parsing; the request did reach the wire.
assert dispatcher.calls[-1][0] == "tools/call"
@pytest.mark.anyio
async def test_non_string_result_type_fails_core_validation_not_discrimination() -> None:
"""SDK-defined: a non-string resultType stays on the core arm and fails as ValidationError, not TypeError."""
raw: dict[str, Any] = {"resultType": {"nested": True}}
dispatcher = _RecordingDispatcher(tool_result=raw)
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
with pytest.raises(ValidationError):
await session.call_tool("t", {}, allow_claimed=True)
# Rejected at response parsing; the request did reach the wire.
assert dispatcher.calls[-1][0] == "tools/call"
def test_adopt_built_adapter_revalidates_model_instances() -> None:
"""SDK-defined: the adopt-built adapter routes already-built model instances as well as raw dicts."""
session = _claims_session(_RecordingDispatcher(), _task_claim())
_adopt_modern(session)
adapter = session._call_tool_adapter
claimed = adapter.validate_python(_TaskResult(task_id="t-2"))
assert isinstance(claimed, _TaskResult)
core = adapter.validate_python(CallToolResult(content=[]))
assert isinstance(core, CallToolResult)
@pytest.mark.anyio
async def test_input_required_routes_to_core_arm_with_claims_active() -> None:
"""Spec-mandated: `input_required` is core vocabulary; active claims leave that arm untouched."""
raw = {"resultType": "input_required", "requestState": "s-1"}
session = _claims_session(_RecordingDispatcher(tool_result=raw), _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
result = await session.call_tool("t", {}, allow_input_required=True, allow_claimed=True)
assert isinstance(result, InputRequiredResult)
assert result.request_state == "s-1"
@pytest.mark.anyio
async def test_claimed_result_raises_unexpected_claimed_result_by_default() -> None:
"""SDK-defined: without `allow_claimed` a claimed shape raises, carrying the parsed
result so the caller can clean up any server-side state it references."""
dispatcher = _RecordingDispatcher(tool_result=_CLAIMED_TASK_RESULT)
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
with pytest.raises(UnexpectedClaimedResult) as exc_info:
await session.call_tool("t", {})
# The shape parsed and then raised; the request did reach the wire.
assert dispatcher.calls[-1][0] == "tools/call"
assert isinstance(exc_info.value.result, _TaskResult)
assert exc_info.value.result.task_id == "t-1"
assert str(exc_info.value) == snapshot(
"Server returned a claimed result (_TaskResult); pass the owning extension to "
"Client(extensions=[...]) for transparent resolution, or call with allow_claimed=True "
"and handle the shape. The carried result may reference server-side state needing cleanup."
)
@pytest.mark.anyio
async def test_call_tool_result_path_identical_under_both_allow_claimed_values() -> None:
"""SDK-defined: `allow_claimed` only affects claimed shapes; ordinary results come back identical."""
dispatcher = _RecordingDispatcher()
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
r_default = await session.call_tool("t", {})
r_opted = await session.call_tool("t", {}, allow_claimed=True)
assert isinstance(r_opted, CallToolResult)
assert r_opted == r_default
@pytest.mark.anyio
async def test_call_tool_overload_matrix_narrows_statically() -> None:
"""SDK-defined: each flag combination narrows `call_tool` to its documented return union under pyright."""
dispatcher = _RecordingDispatcher()
session = _claims_session(dispatcher, _task_claim())
with anyio.fail_after(5):
async with session:
_adopt_modern(session)
r1 = await session.call_tool("t", {})
assert_type(r1, CallToolResult)
r2 = await session.call_tool("t", {}, allow_input_required=True)
assert_type(r2, CallToolResult | InputRequiredResult)
r3 = await session.call_tool("t", {}, allow_claimed=True)
assert_type(r3, CallToolResult | Result)
r4 = await session.call_tool("t", {}, allow_input_required=True, allow_claimed=True)
assert_type(r4, CallToolResult | InputRequiredResult | Result)
assert [type(r) for r in (r1, r2, r3, r4)] == [CallToolResult] * 4
def test_claimed_raw_passes_v2026_tools_call_surface_validation() -> None:
"""Pins the claim path's dependency: an unknown resultType passes `validate_server_result`
at 2026-07-28; this failing is the signal that mcp-types tightened the surface."""
validate_server_result("tools/call", LATEST_MODERN_VERSION, {"resultType": "task", "taskId": "t-1"})