Skip to content

Commit fe65003

Browse files
committed
Adopt the serverInfo _meta stamp fixture across the interaction suite
Main now stamps io.modelcontextprotocol/serverInfo into the _meta of every 2026-era result and ships an era-aware strip helper. Route the branch's own result comparisons through it: era-parametrized tests thread the unstamped fixture; tests pinned to a single modern connection call the strict module helper directly, aliased strip_stamp wherever a file also uses the fixture so the name never carries two meanings; raw-wire snapshots pin the stamp on the wire, with ad-hoc test servers given an explicit version so no snapshot pins an empty one. Drop the DiscoverResult(server_info=...) kwargs the reshaped model silently ignored, and correct the tests/_stamp.py docstring to match the new direct usage. No-Verification-Needed: test-only change
1 parent 20b21d5 commit fe65003

14 files changed

Lines changed: 176 additions & 100 deletions

tests/_stamp.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
and the strip is strict, so a modern result that lost its stamp fails the
77
test instead of passing silently.
88
9-
The interaction matrix does not use this function directly: its `unstamped`
10-
fixture (tests/interaction/conftest.py) resolves per cell to this strict
9+
Era-parametrized interaction tests go through the `unstamped` fixture
10+
(tests/interaction/conftest.py), which resolves per cell to this strict
1111
strip on modern cells and to a must-not-be-stamped assertion on
12-
handshake-era cells, so one comparison line enforces both eras.
12+
handshake-era cells, so one comparison line enforces both eras. Interaction
13+
tests pinned to a single modern connection call this function directly
14+
(imported as `strip_stamp` where a file also uses the fixture, so the two
15+
never share a name).
1316
"""
1417

1518
from typing import Any, Protocol, TypeVar
@@ -29,8 +32,8 @@ def unstamped(result: R) -> R:
2932
"""Assert the result carries a well-formed serverInfo stamp, then remove it.
3033
3134
Returns the result for inline use in comparisons. Use only where a stamp
32-
is required (a 2026-era result); the interaction matrix's `unstamped`
33-
fixture handles the era split.
35+
is required (a 2026-era result); tests that also run on handshake-era
36+
cells use the `unstamped` fixture instead, which handles the era split.
3437
"""
3538
meta = result.meta
3639
assert meta is not None and SERVER_INFO_META_KEY in meta, "expected a serverInfo stamp on this result"

tests/interaction/lowlevel/test_caching.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import mcp_types as types
1010
import pytest
1111
from mcp_types import (
12+
SERVER_INFO_META_KEY,
1213
DiscoverResult,
1314
ElicitRequest,
1415
ElicitRequestFormParams,
@@ -216,7 +217,7 @@ async def read_resource(
216217
cache_scope="public",
217218
)
218219

219-
server = Server("cached", on_read_resource=read_resource)
220+
server = Server("cached", version="1.0.0", on_read_resource=read_resource)
220221

221222
async def answer_who(context: ClientRequestContext, params: types.ElicitRequestParams) -> ElicitResult:
222223
assert isinstance(params, ElicitRequestFormParams)
@@ -245,7 +246,9 @@ async def answer_who(context: ClientRequestContext, params: types.ElicitRequestP
245246
interim = responses[reads[0].id]
246247
complete = responses[reads[1].id]
247248
# Exact key vocabulary, stronger than `not in` checks: any field added to interim frames fails loudly.
248-
assert sorted(interim) == ["inputRequests", "resultType"]
249+
# The interim frame is a 2026 result, so it carries the serverInfo stamp but still no caching hints.
250+
assert sorted(interim) == ["_meta", "inputRequests", "resultType"]
251+
assert interim["_meta"] == {SERVER_INFO_META_KEY: {"name": "cached", "version": "1.0.0"}}
249252
assert interim["resultType"] == "input_required"
250253
assert complete["ttlMs"] == 60_000
251254
assert complete["cacheScope"] == "public"
@@ -289,13 +292,7 @@ async def scripted_server() -> None:
289292
ClientSession(client_read, client_write, client_info=Implementation(name="cli", version="0")) as session,
290293
):
291294
task_group.start_soon(scripted_server)
292-
session.adopt(
293-
DiscoverResult(
294-
supported_versions=[LATEST_MODERN_VERSION],
295-
capabilities=ServerCapabilities(),
296-
server_info=Implementation(name="srv", version="0"),
297-
)
298-
)
295+
session.adopt(DiscoverResult(supported_versions=[LATEST_MODERN_VERSION], capabilities=ServerCapabilities()))
299296
with anyio.fail_after(5):
300297
result = await session.list_tools()
301298

tests/interaction/lowlevel/test_elicitation.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
from mcp.server import Server, ServerRequestContext
3939
from mcp.shared.memory import MessageStream, create_client_server_memory_streams
4040
from mcp.shared.message import SessionMessage
41+
from tests._stamp import Unstamp
42+
from tests._stamp import unstamped as strip_stamp
4143
from tests.interaction._connect import BASE_URL, Connect, mounted_app
4244
from tests.interaction._helpers import RecordingTransport
4345
from tests.interaction._requirements import requirement
@@ -672,7 +674,9 @@ async def scripted_server(streams: MessageStream) -> None:
672674

673675

674676
@requirement("elicitation:mrtr:form:basic")
675-
async def test_embedded_form_elicitation_accepted_content_returns_to_retried_handler(connect: Connect) -> None:
677+
async def test_embedded_form_elicitation_accepted_content_returns_to_retried_handler(
678+
connect: Connect, unstamped: Unstamp
679+
) -> None:
676680
"""An embedded form elicitation reaches the callback as sent and its accepted content reaches the handler.
677681
678682
Spec-mandated: at 2026-07-28 elicitation/create rides the MRTR flow, not a server-initiated request.
@@ -723,13 +727,15 @@ async def answer_form(context: ClientRequestContext, params: types.ElicitRequest
723727
)
724728
]
725729
)
726-
assert result == snapshot(
730+
assert unstamped(result) == snapshot(
727731
CallToolResult(content=[TextContent(text="accept")], structured_content={"username": "ada", "newsletter": True})
728732
)
729733

730734

731735
@requirement("elicitation:mrtr:form:action:decline")
732-
async def test_embedded_form_elicitation_decline_reaches_retried_handler_with_no_content(connect: Connect) -> None:
736+
async def test_embedded_form_elicitation_decline_reaches_retried_handler_with_no_content(
737+
connect: Connect, unstamped: Unstamp
738+
) -> None:
733739
"""An embedded form elicitation declined by the callback reaches the retried handler with no content."""
734740

735741
async def list_tools(
@@ -765,11 +771,13 @@ async def answer_form(context: ClientRequestContext, params: types.ElicitRequest
765771
async with connect(server, elicitation_callback=answer_form) as client:
766772
result = await client.call_tool("confirm", {})
767773

768-
assert result == snapshot(CallToolResult(content=[TextContent(text="decline content=None")]))
774+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="decline content=None")]))
769775

770776

771777
@requirement("elicitation:mrtr:form:action:cancel")
772-
async def test_embedded_form_elicitation_cancel_reaches_retried_handler_with_no_content(connect: Connect) -> None:
778+
async def test_embedded_form_elicitation_cancel_reaches_retried_handler_with_no_content(
779+
connect: Connect, unstamped: Unstamp
780+
) -> None:
773781
"""An embedded form elicitation cancelled by the callback reaches the retried handler with no content."""
774782

775783
async def list_tools(
@@ -805,11 +813,13 @@ async def answer_form(context: ClientRequestContext, params: types.ElicitRequest
805813
async with connect(server, elicitation_callback=answer_form) as client:
806814
result = await client.call_tool("confirm", {})
807815

808-
assert result == snapshot(CallToolResult(content=[TextContent(text="cancel content=None")]))
816+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="cancel content=None")]))
809817

810818

811819
@requirement("elicitation:mrtr:form:schema:primitives")
812-
async def test_embedded_form_elicitation_schema_primitives_reach_the_callback_as_sent(connect: Connect) -> None:
820+
async def test_embedded_form_elicitation_schema_primitives_reach_the_callback_as_sent(
821+
connect: Connect, unstamped: Unstamp
822+
) -> None:
813823
"""Primitive requested-schema fields on an embedded form elicitation reach the callback intact.
814824
815825
Spec-mandated. One representative constraint per type; the exhaustive sweep lives with the 2025 push-path sibling.
@@ -877,7 +887,7 @@ async def answer_form(context: ClientRequestContext, params: types.ElicitRequest
877887
)
878888
]
879889
)
880-
assert result == snapshot(
890+
assert unstamped(result) == snapshot(
881891
CallToolResult(
882892
content=[TextContent(text="accept")],
883893
structured_content={"email": "ada@example.com", "age": 36, "score": 9.5, "subscribed": True},
@@ -887,7 +897,7 @@ async def answer_form(context: ClientRequestContext, params: types.ElicitRequest
887897

888898
@requirement("elicitation:mrtr:capability:not-declared")
889899
async def test_server_embeds_elicitation_for_a_client_that_declared_no_elicitation_capability(
890-
connect: Connect,
900+
connect: Connect, unstamped: Unstamp
891901
) -> None:
892902
"""Pins a known gap: the SDK embeds an elicitation for a client that declared no elicitation capability.
893903
@@ -916,7 +926,7 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
916926
raw = await client.session.call_tool("ask", {}, allow_input_required=True)
917927

918928
assert isinstance(raw, InputRequiredResult)
919-
assert raw == snapshot(
929+
assert unstamped(raw) == snapshot(
920930
InputRequiredResult(
921931
input_requests={
922932
"ask": ElicitRequest(
@@ -983,7 +993,7 @@ async def answer_url(context: ClientRequestContext, params: types.ElicitRequestP
983993
assert received == snapshot(
984994
[ElicitRequestURLParams(message="Sign in to continue.", url="https://example.com/auth")]
985995
)
986-
assert result == snapshot(CallToolResult(content=[TextContent(text="accept content=None")]))
996+
assert strip_stamp(result) == snapshot(CallToolResult(content=[TextContent(text="accept content=None")]))
987997
# Positive control: the interim input_required leg was captured, so the scan below is not vacuous.
988998
interim = [
989999
message.message

tests/interaction/lowlevel/test_mrtr.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
from mcp.shared.exceptions import NoBackChannelError
5555
from mcp.shared.memory import MessageStream, create_client_server_memory_streams
5656
from mcp.shared.message import SessionMessage
57+
from tests._stamp import Unstamp
58+
from tests._stamp import unstamped as strip_stamp
5759
from tests.interaction._connect import BASE_URL, Connect, base_headers, mounted_app
5860
from tests.interaction._helpers import RecordingTransport
5961
from tests.interaction._requirements import requirement
@@ -106,7 +108,9 @@ async def call_tool(
106108

107109

108110
@requirement("mrtr:tools-call:write-once-roundtrip")
109-
async def test_input_required_tool_call_is_auto_fulfilled_and_retried_to_completion(connect: Connect) -> None:
111+
async def test_input_required_tool_call_is_auto_fulfilled_and_retried_to_completion(
112+
connect: Connect, unstamped: Unstamp
113+
) -> None:
110114
"""An input_required tools/call is auto-fulfilled by the client driver and retried to completion.
111115
112116
The byte-exact requestState echo (spec MUST) is the only observable proxy for the MUST NOT
@@ -125,13 +129,15 @@ async def answer_login(context: ClientRequestContext, params: types.ElicitReques
125129
async with connect(server, elicitation_callback=answer_login) as client:
126130
result = await client.call_tool("login", {})
127131

128-
assert result == snapshot(CallToolResult(content=[TextContent(text="hello octocat")]))
132+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="hello octocat")]))
129133
assert prompts == ["Provide your GitHub username"]
130134
assert request_states == [None, OPAQUE_STATE]
131135

132136

133137
@requirement("mrtr:request-state-only:retry")
134-
async def test_state_only_input_required_is_retried_with_no_responses_and_echoed_state(connect: Connect) -> None:
138+
async def test_state_only_input_required_is_retried_with_no_responses_and_echoed_state(
139+
connect: Connect, unstamped: Unstamp
140+
) -> None:
135141
"""A state-only input_required result is retried with no inputResponses and the state echoed.
136142
137143
No callbacks are registered: a driver that wrongly dispatched here would error the call.
@@ -161,12 +167,14 @@ async def call_tool(
161167
async with connect(server) as client:
162168
result = await client.call_tool("resume", {})
163169

164-
assert result == snapshot(CallToolResult(content=[TextContent(text="done")]))
170+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="done")]))
165171
assert request_states == [None, resume_token]
166172

167173

168174
@requirement("mrtr:multi-round:complete")
169-
async def test_server_reprompts_across_two_productive_rounds_then_completes(connect: Connect) -> None:
175+
async def test_server_reprompts_across_two_productive_rounds_then_completes(
176+
connect: Connect, unstamped: Unstamp
177+
) -> None:
170178
"""A server re-prompting with input_required across two productive rounds completes normally.
171179
172180
Round 1's answer rides forward inside request_state (the spec's stateless-server pattern). Each
@@ -216,7 +224,7 @@ async def answer_by_prompt(context: ClientRequestContext, params: types.ElicitRe
216224
async with connect(server, elicitation_callback=answer_by_prompt) as client:
217225
result = await client.call_tool("enroll", {})
218226

219-
assert result == snapshot(CallToolResult(content=[TextContent(text="one+two")]))
227+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="one+two")]))
220228
assert prompts == ["first question", "second question"]
221229
assert request_states == [None, "s1", "s2:one"]
222230

@@ -314,7 +322,9 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
314322

315323

316324
@requirement("mrtr:input-responses:key-correspondence")
317-
async def test_multi_request_input_responses_are_keyed_by_the_input_request_keys(connect: Connect) -> None:
325+
async def test_multi_request_input_responses_are_keyed_by_the_input_request_keys(
326+
connect: Connect, unstamped: Unstamp
327+
) -> None:
318328
"""inputResponses on the retry are keyed by the inputRequests keys, each value that key's typed result.
319329
320330
ElicitResult and ListRootsResult prove the map contract; sampling fidelity belongs to the sampling entries.
@@ -354,11 +364,11 @@ async def answer_roots(context: ClientRequestContext) -> ListRootsResult:
354364
async with connect(server, elicitation_callback=answer_login, list_roots_callback=answer_roots) as client:
355365
result = await client.call_tool("profile", {})
356366

357-
assert result == snapshot(CallToolResult(content=[TextContent(text="octocat@file:///workspace")]))
367+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="octocat@file:///workspace")]))
358368

359369

360370
@requirement("mrtr:input-responses:missing-reprompted")
361-
async def test_retry_missing_a_requested_key_is_reprompted_not_errored(connect: Connect) -> None:
371+
async def test_retry_missing_a_requested_key_is_reprompted_not_errored(connect: Connect, unstamped: Unstamp) -> None:
362372
"""A retry omitting a requested inputResponses key is re-prompted, not errored (spec SHOULD).
363373
364374
The re-prompt decision belongs to the test's handler; the SDK obligation pinned is that the partial
@@ -423,13 +433,15 @@ async def call_tool(
423433
allow_input_required=True,
424434
)
425435

426-
assert result == snapshot(CallToolResult(content=[TextContent(text="one+two")]))
436+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="one+two")]))
427437
# The partial map reached the handler as sent, not filtered or rejected.
428438
assert seen == [None, {"first"}, {"second"}]
429439

430440

431441
@requirement("mrtr:input-responses:unknown-ignored")
432-
async def test_retry_with_an_unrequested_extra_key_is_tolerated_and_the_call_completes(connect: Connect) -> None:
442+
async def test_retry_with_an_unrequested_extra_key_is_tolerated_and_the_call_completes(
443+
connect: Connect, unstamped: Unstamp
444+
) -> None:
433445
"""A retry carrying an unrequested inputResponses key completes normally (spec SHOULD: ignore).
434446
435447
The ignoring happens in the test's handler; the SDK half pinned is that the stray entry is
@@ -472,12 +484,14 @@ async def call_tool(
472484
allow_input_required=True,
473485
)
474486

475-
assert result == snapshot(CallToolResult(content=[TextContent(text="hello ada")]))
487+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="hello ada")]))
476488
assert seen == [None, {"name", "stray"}]
477489

478490

479491
@requirement("mrtr:push-api:loud-fail-2026")
480-
async def test_push_elicit_on_2026_raises_typed_local_error_and_call_still_completes(connect: Connect) -> None:
492+
async def test_push_elicit_on_2026_raises_typed_local_error_and_call_still_completes(
493+
connect: Connect, unstamped: Unstamp
494+
) -> None:
481495
"""A push API call on a 2026 connection raises a typed local error and the call still completes.
482496
483497
Spec-mandated outcome, era-routed enforcement: every modern dispatch path installs a
@@ -510,7 +524,7 @@ async def never_deliverable(context: ClientRequestContext, params: types.ElicitR
510524
result = await client.call_tool("ask", {})
511525

512526
# The failed push did not poison the request: the call completes with the handler's fallback.
513-
assert result == snapshot(CallToolResult(content=[TextContent(text="fallback")]))
527+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="fallback")]))
514528
assert len(caught) == 1
515529
assert caught[0].method == "elicitation/create"
516530
assert caught[0].error == snapshot(
@@ -562,7 +576,7 @@ async def never_deliverable(context: ClientRequestContext, params: types.ElicitR
562576
result = await client.call_tool("ask", {})
563577

564578
# The failed push did not poison the request: the call completes with the handler's fallback.
565-
assert result == snapshot(CallToolResult(content=[TextContent(text="fallback")]))
579+
assert strip_stamp(result) == snapshot(CallToolResult(content=[TextContent(text="fallback")]))
566580
assert len(caught) == 1
567581
assert caught[0].method == "elicitation/create"
568582
assert caught[0].error == snapshot(
@@ -819,7 +833,7 @@ async def answer(context: ClientRequestContext, params: types.ElicitRequestParam
819833
):
820834

821835
async def call(name: str) -> None:
822-
results[name] = await client.call_tool(name, {})
836+
results[name] = strip_stamp(await client.call_tool(name, {}))
823837

824838
task_group.start_soon(call, "alpha")
825839
task_group.start_soon(call, "beta")
@@ -879,7 +893,7 @@ async def answer(context: ClientRequestContext, params: types.ElicitRequestParam
879893
result = await client.call_tool("ask", {})
880894

881895
# Non-vacuity: the elicitation genuinely happened and the round trip completed through it.
882-
assert result == snapshot(CallToolResult(content=[TextContent(text="done:Berlin:s1")]))
896+
assert strip_stamp(result) == snapshot(CallToolResult(content=[TextContent(text="done:Berlin:s1")]))
883897
assert elicited == ["Need a name"]
884898
# Prove the received log holds only messages before narrowing: a filtered-out transport exception would fake it.
885899
received_messages = [message for message in recording.received if isinstance(message, SessionMessage)]
@@ -1030,7 +1044,7 @@ def respond(request_id: types.RequestId, result: dict[str, object]) -> SessionMe
10301044

10311045
@requirement("protocol:result-type:unrecognized-invalid")
10321046
async def test_an_unrecognized_result_type_value_is_surfaced_unchanged_instead_of_treated_as_invalid(
1033-
connect: Connect,
1047+
connect: Connect, unstamped: Unstamp
10341048
) -> None:
10351049
"""PINS A KNOWN GAP: an unrecognized resultType round-trips instead of being treated as invalid (spec MUST).
10361050
@@ -1061,4 +1075,4 @@ def probe() -> CallToolResult:
10611075

10621076
# The divergent observable: the unrecognized discriminator survives unchanged, never a rejection.
10631077
assert result.result_type == "bogus"
1064-
assert result == snapshot(CallToolResult(content=[TextContent(text="still here")], result_type="bogus"))
1078+
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="still here")], result_type="bogus"))

tests/interaction/lowlevel/test_pagination.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ async def list_tools(ctx: ServerRequestContext, params: types.PaginatedRequestPa
6262

6363

6464
@requirement("protocol:pagination:empty-cursor-valid")
65-
async def test_an_empty_string_next_cursor_round_trips_as_a_cursor_not_end_of_results(connect: Connect) -> None:
65+
async def test_an_empty_string_next_cursor_round_trips_as_a_cursor_not_end_of_results(
66+
connect: Connect, unstamped: Unstamp
67+
) -> None:
6668
"""An empty-string next_cursor round-trips verbatim, distinct from absent.
6769
6870
Spec-mandated: an empty string is a valid cursor and MUST NOT be treated as the end of results.
@@ -84,7 +86,9 @@ async def list_tools(ctx: ServerRequestContext, params: types.PaginatedRequestPa
8486

8587
assert first_page.next_cursor == ""
8688
assert seen_cursors == [None, ""]
87-
assert second_page == snapshot(ListToolsResult(tools=[Tool(name="beta", input_schema={"type": "object"})]))
89+
assert unstamped(second_page) == snapshot(
90+
ListToolsResult(tools=[Tool(name="beta", input_schema={"type": "object"})])
91+
)
8892

8993

9094
@requirement("pagination:exhaustion")

0 commit comments

Comments
 (0)