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
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:
@@ -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"})
|
||||
Reference in New Issue
Block a user