Skip to content

Commit 0033409

Browse files
committed
Align interaction-manifest ids with the cross-SDK vocabulary
Rename 14 requirement ids to the names shared with the typescript-sdk e2e suite (sampling:create:*, elicitation:url:action:*, protocol:meta:*, the client-auth stepup/iss/scope/dcr family, the mcpserver context helpers), updating every @requirement decorator to match. The resources:read:unknown-uri name previously sat on an entry whose test proves lowlevel handler-error passthrough, not the unknown-resource rule; that entry is re-described honestly as protocol:error:handler-error- passthrough (source 'sdk'), and the real unknown-resource entry (-32602 with the URI in error.data, SEP-2164) takes the vacated name with its source repointed to the 2026 page that mandates it. The protocol:meta:request-to-handler 2026 arm exclusion now carries the accurate reason (legacy-only-vocabulary) and a note explaining the envelope- key merge that breaks the equality assertion, so the re-admission checklist finds it. Three test docstrings quoting pre-rename spec anchors updated. Cell generation unchanged (830 before and after).
1 parent a2837c7 commit 0033409

11 files changed

Lines changed: 72 additions & 58 deletions

‎tests/interaction/_requirements.py‎

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,13 @@ def __post_init__(self) -> None:
572572
source="sdk",
573573
behavior="Closing the transport fails all in-flight requests with a connection-closed error.",
574574
),
575+
"protocol:error:handler-error-passthrough": Requirement(
576+
source="sdk",
577+
behavior=(
578+
"An MCPError raised by a request handler is returned to the caller as a JSON-RPC error "
579+
"carrying the handler-chosen code and message verbatim."
580+
),
581+
),
575582
"protocol:error:internal-error": Requirement(
576583
source=f"{SPEC_BASE_URL}/basic#responses",
577584
behavior=(
@@ -635,12 +642,23 @@ def __post_init__(self) -> None:
635642
"extension."
636643
),
637644
),
638-
"meta:request-to-handler": Requirement(
645+
"protocol:meta:request-to-handler": Requirement(
639646
source=f"{SPEC_BASE_URL}/basic#_meta",
640647
behavior="The _meta object the client attaches to a request is visible to the server handler.",
641-
arm_exclusions=(ArmExclusion(reason="asserts-legacy-handshake", spec_version="2026-07-28"),),
648+
arm_exclusions=(
649+
ArmExclusion(
650+
reason="legacy-only-vocabulary",
651+
spec_version="2026-07-28",
652+
note=(
653+
"The pass-through itself holds at 2026, but the modern envelope merges the reserved "
654+
"io.modelcontextprotocol/* keys into every request's _meta, so the test's "
655+
"nothing-else-injected equality assertion only holds on the legacy wire; needs an "
656+
"era-aware assertion before re-admission."
657+
),
658+
),
659+
),
642660
),
643-
"meta:result-to-client": Requirement(
661+
"protocol:meta:result-to-client": Requirement(
644662
source=f"{SPEC_BASE_URL}/basic#_meta",
645663
behavior="The _meta object a handler attaches to its result is delivered to the client.",
646664
),
@@ -1037,7 +1055,7 @@ def __post_init__(self) -> None:
10371055
# ═══════════════════════════════════════════════════════════════════════════
10381056
# MCPServer: Context helpers (SDK)
10391057
# ═══════════════════════════════════════════════════════════════════════════
1040-
"mcpserver:context:logging": Requirement(
1058+
"mcpserver:context:log-from-handler": Requirement(
10411059
source="sdk",
10421060
behavior=(
10431061
"The Context logging helpers (debug/info/warning/error) send log message notifications at the "
@@ -1050,7 +1068,7 @@ def __post_init__(self) -> None:
10501068
"Context.report_progress sends a progress notification against the requesting client's progress token."
10511069
),
10521070
),
1053-
"mcpserver:context:elicit": Requirement(
1071+
"mcpserver:context:elicit-from-handler": Requirement(
10541072
source="sdk",
10551073
behavior=(
10561074
"Context.elicit sends a form elicitation built from a typed schema and returns a typed "
@@ -1121,8 +1139,11 @@ def __post_init__(self) -> None:
11211139
behavior="resources/read returns text contents carrying uri, mimeType, and the text.",
11221140
),
11231141
"resources:read:unknown-uri": Requirement(
1124-
source=f"{SPEC_BASE_URL}/server/resources#error-handling",
1125-
behavior="resources/read for an unknown URI returns JSON-RPC error -32002 (resource not found).",
1142+
source=f"{SPEC_2026_BASE_URL}/server/resources#error-handling",
1143+
behavior=(
1144+
"resources/read for a URI matching no registered resource returns JSON-RPC error -32602 "
1145+
"(invalid params) with the requested URI in error.data, per SEP-2164."
1146+
),
11261147
),
11271148
"resources:subscribe": Requirement(
11281149
source=f"{SPEC_BASE_URL}/server/resources#subscriptions",
@@ -1228,13 +1249,6 @@ def __post_init__(self) -> None:
12281249
"by resources/read, receiving the parameters extracted from the requested URI."
12291250
),
12301251
),
1231-
"mcpserver:resource:unknown-uri": Requirement(
1232-
source=f"{SPEC_BASE_URL}/server/resources#error-handling",
1233-
behavior=(
1234-
"resources/read for a URI matching no registered resource returns JSON-RPC error -32602 "
1235-
"(invalid params) with the requested URI in error.data, per SEP-2164."
1236-
),
1237-
),
12381252
# ═══════════════════════════════════════════════════════════════════════════
12391253
# Prompts
12401254
# ═══════════════════════════════════════════════════════════════════════════
@@ -1528,23 +1542,23 @@ def __post_init__(self) -> None:
15281542
"rejects every tool-enabled request before it is sent."
15291543
),
15301544
),
1531-
"sampling:create-message:audio-content": Requirement(
1545+
"sampling:create:audio-content": Requirement(
15321546
source=f"{SPEC_BASE_URL}/client/sampling#audio-content",
15331547
behavior="Sampling messages can carry audio content: base64 data with a mimeType.",
15341548
arm_exclusions=(
15351549
ArmExclusion(reason="server-initiated-request", transport="streamable-http-stateless"),
15361550
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
15371551
),
15381552
),
1539-
"sampling:create-message:image-content": Requirement(
1553+
"sampling:create:image-content": Requirement(
15401554
source=f"{SPEC_BASE_URL}/client/sampling#image-content",
15411555
behavior="Sampling messages can carry image content: base64 data with a mimeType.",
15421556
arm_exclusions=(
15431557
ArmExclusion(reason="server-initiated-request", transport="streamable-http-stateless"),
15441558
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
15451559
),
15461560
),
1547-
"sampling:create-message:not-supported": Requirement(
1561+
"sampling:create:not-supported": Requirement(
15481562
source=f"{SPEC_BASE_URL}/client/sampling#capabilities",
15491563
behavior=(
15501564
"A sampling request to a client that did not declare the sampling capability fails with an "
@@ -1832,20 +1846,28 @@ def __post_init__(self) -> None:
18321846
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
18331847
),
18341848
),
1835-
"elicitation:url:basic": Requirement(
1836-
source=f"{SPEC_BASE_URL}/client/elicitation#url-mode-elicitation-requests",
1837-
behavior=(
1838-
"A url-mode elicitation delivers the elicitation id and URL to the client callback exactly as "
1839-
"the server sent them."
1840-
),
1849+
"elicitation:url:action:cancel": Requirement(
1850+
source=f"{SPEC_BASE_URL}/client/elicitation#response-actions",
1851+
behavior="A URL-mode elicitation answered with cancel returns the action with no content.",
18411852
arm_exclusions=(
18421853
ArmExclusion(reason="server-initiated-request", transport="streamable-http-stateless"),
18431854
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
18441855
),
18451856
),
1846-
"elicitation:url:cancel": Requirement(
1857+
"elicitation:url:action:decline": Requirement(
18471858
source=f"{SPEC_BASE_URL}/client/elicitation#response-actions",
1848-
behavior="A URL-mode elicitation answered with cancel returns the action with no content.",
1859+
behavior="A URL-mode elicitation answered with decline returns the action with no content.",
1860+
arm_exclusions=(
1861+
ArmExclusion(reason="server-initiated-request", transport="streamable-http-stateless"),
1862+
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
1863+
),
1864+
),
1865+
"elicitation:url:basic": Requirement(
1866+
source=f"{SPEC_BASE_URL}/client/elicitation#url-mode-elicitation-requests",
1867+
behavior=(
1868+
"A url-mode elicitation delivers the elicitation id and URL to the client callback exactly as "
1869+
"the server sent them."
1870+
),
18491871
arm_exclusions=(
18501872
ArmExclusion(reason="server-initiated-request", transport="streamable-http-stateless"),
18511873
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
@@ -1873,14 +1895,6 @@ def __post_init__(self) -> None:
18731895
removed_in="2026-07-28",
18741896
note="removed in 2026-07-28 (spec PR #2891); notifications/elicitation/complete removed, no replacement.",
18751897
),
1876-
"elicitation:url:decline": Requirement(
1877-
source=f"{SPEC_BASE_URL}/client/elicitation#response-actions",
1878-
behavior="A URL-mode elicitation answered with decline returns the action with no content.",
1879-
arm_exclusions=(
1880-
ArmExclusion(reason="server-initiated-request", transport="streamable-http-stateless"),
1881-
ArmExclusion(reason="server-initiated-request", spec_version="2026-07-28"),
1882-
),
1883-
),
18841898
"elicitation:url:not-supported": Requirement(
18851899
source=f"{SPEC_BASE_URL}/client/elicitation#error-handling",
18861900
behavior=(
@@ -3453,7 +3467,7 @@ def __post_init__(self) -> None:
34533467
transports=("streamable-http",),
34543468
note="OAuth is HTTP-only.",
34553469
),
3456-
"client-auth:403-scope-union": Requirement(
3470+
"client-auth:stepup:scope-union": Requirement(
34573471
source=f"{SPEC_BASE_URL}/basic/authorization#step-up-authorization-flow",
34583472
behavior=(
34593473
"On a 403 insufficient_scope step-up, the re-authorization request carries the union of the "
@@ -3496,7 +3510,7 @@ def __post_init__(self) -> None:
34963510
),
34973511
),
34983512
),
3499-
"client-auth:authorize:offline-access-consent": Requirement(
3513+
"client-auth:scope:offline-access-gate": Requirement(
35003514
source="sdk",
35013515
behavior=(
35023516
"When the authorization server's metadata advertises offline_access in scopes_supported and "
@@ -3530,7 +3544,7 @@ def __post_init__(self) -> None:
35303544
transports=("streamable-http",),
35313545
note="OAuth is HTTP-only.",
35323546
),
3533-
"client-auth:dcr:registration-error-surfaces": Requirement(
3547+
"client-auth:dcr:registration-rejected-error": Requirement(
35343548
source="sdk",
35353549
behavior=(
35363550
"A 400 from the registration endpoint surfaces to the caller as an OAuthRegistrationError "
@@ -3692,7 +3706,7 @@ def __post_init__(self) -> None:
36923706
transports=("streamable-http",),
36933707
note="OAuth is HTTP-only.",
36943708
),
3695-
"client-auth:authorization-response:iss-verify": Requirement(
3709+
"client-auth:iss:mismatch-reject": Requirement(
36963710
source=f"{SPEC_2026_BASE_URL}/basic/authorization#authorization-response-validation",
36973711
behavior=(
36983712
"The client validates the RFC 9207 iss authorization-response parameter against the "

‎tests/interaction/auth/test_authorize_token.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async def recorded_oauth_flow() -> AsyncIterator[RecordedFlow]:
113113

114114
@requirement("client-auth:pkce:s256")
115115
@requirement("client-auth:resource-parameter")
116-
@requirement("client-auth:authorize:offline-access-consent")
116+
@requirement("client-auth:scope:offline-access-gate")
117117
async def test_the_authorize_url_carries_s256_pkce_and_the_resource_indicator(
118118
recorded_oauth_flow: RecordedFlow,
119119
) -> None:
@@ -187,7 +187,7 @@ async def test_a_mismatched_state_on_the_callback_aborts_the_flow() -> None:
187187
await connect_with_oauth(server, provider=provider, headless=headless).__aenter__()
188188

189189

190-
@requirement("client-auth:authorization-response:iss-verify")
190+
@requirement("client-auth:iss:mismatch-reject")
191191
async def test_a_mismatched_iss_on_the_callback_aborts_the_flow() -> None:
192192
"""A callback whose RFC 9207 iss does not match the authorization server issuer aborts the flow.
193193

‎tests/interaction/auth/test_discovery.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ async def test_when_every_prm_probe_fails_the_client_discovers_as_metadata_at_th
150150
assert result.tools[0].name == "probe"
151151

152152

153-
@requirement("client-auth:dcr:registration-error-surfaces")
153+
@requirement("client-auth:dcr:registration-rejected-error")
154154
async def test_a_400_from_the_registration_endpoint_surfaces_as_a_registration_error() -> None:
155155
"""A 400 from `/register` surfaces as `OAuthRegistrationError` carrying the server's body.
156156

‎tests/interaction/auth/test_lifecycle.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ async def test_a_403_insufficient_scope_triggers_one_reauthorize_with_the_challe
179179
assert counts[("POST", "/token")] == 2
180180

181181

182-
@requirement("client-auth:403-scope-union")
182+
@requirement("client-auth:stepup:scope-union")
183183
async def test_a_403_step_up_re_authorizes_with_the_union_of_prior_and_challenged_scopes() -> None:
184184
"""The step-up re-authorize requests the union of the previously requested and challenged scopes.
185185

‎tests/interaction/lowlevel/test_client_connect.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async def test_prior_discover_populates_state_with_zero_connect_time_traffic() -
154154
async def test_auto_mode_probes_server_discover_and_adopts_the_result() -> None:
155155
"""`Client(..., mode='auto')` sends `server/discover` first and adopts the returned version and server_info.
156156
157-
Requirement `lifecycle:discover:basic` (spec basic/lifecycle#discover): the probe is a
157+
Requirement `lifecycle:discover:basic` (spec server/discover): the probe is a
158158
single `server/discover` request whose result carries supported versions, capabilities,
159159
server_info and the cache-hint fields, after which the session is modern-negotiated.
160160
"""
@@ -179,7 +179,7 @@ async def test_auto_mode_probes_server_discover_and_adopts_the_result() -> None:
179179
async def test_auto_mode_retries_discover_once_on_unsupported_protocol_version() -> None:
180180
"""A -32022 from `server/discover` triggers exactly one retry at the highest mutual modern version.
181181
182-
Requirement `lifecycle:discover:retry-on-32022` (spec basic/lifecycle#version-errors): the
182+
Requirement `lifecycle:discover:retry-on-32022` (spec basic/versioning#protocol-version-negotiation): the
183183
client intersects `error.data.supported` with its own modern versions and re-probes once;
184184
the second success is adopted. The server's `server/discover` handler is overridden to fail
185185
the first call and succeed on the second.
@@ -349,7 +349,7 @@ async def list_tools(
349349
async def test_http_protocol_version_header_matches_meta_protocol_version_on_every_post() -> None:
350350
"""On streamable-HTTP, the `MCP-Protocol-Version` header on each POST equals `_meta.protocolVersion` in its body.
351351
352-
Requirement `lifecycle:envelope:header-matches-meta` (spec streamable-http#headers): the
352+
Requirement `lifecycle:envelope:header-matches-meta` (spec streamable-http#protocol-version-header): the
353353
body-derived header and the envelope's protocol version are kept in lockstep so the server's
354354
header-based routing and body-based validation never disagree.
355355
"""

‎tests/interaction/lowlevel/test_elicitation.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ async def answer_url(context: ClientRequestContext, params: types.ElicitRequestP
240240
assert result == snapshot(CallToolResult(content=[TextContent(text="accept content=None")]))
241241

242242

243-
@requirement("elicitation:url:decline")
243+
@requirement("elicitation:url:action:decline")
244244
async def test_elicit_url_decline_returns_no_content(connect: Connect) -> None:
245245
"""A declined URL elicitation returns the decline action to the handler with no content."""
246246

@@ -269,7 +269,7 @@ async def answer_url(context: ClientRequestContext, params: types.ElicitRequestP
269269
assert result == snapshot(CallToolResult(content=[TextContent(text="decline content=None")]))
270270

271271

272-
@requirement("elicitation:url:cancel")
272+
@requirement("elicitation:url:action:cancel")
273273
async def test_elicit_url_cancel_returns_no_content(connect: Connect) -> None:
274274
"""A cancelled URL elicitation returns the cancel action to the handler with no content."""
275275

‎tests/interaction/lowlevel/test_meta.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
pytestmark = pytest.mark.anyio
1717

1818

19-
@requirement("meta:request-to-handler")
19+
@requirement("protocol:meta:request-to-handler")
2020
async def test_request_meta_reaches_handler(connect: Connect) -> None:
2121
"""The _meta object the client attaches to a request arrives at the tool handler unchanged."""
2222
request_meta: RequestParamsMeta = {"example.com/trace": "abc-123"}
@@ -41,7 +41,7 @@ async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestPara
4141
assert observed_metas == [dict(request_meta)]
4242

4343

44-
@requirement("meta:result-to-client")
44+
@requirement("protocol:meta:result-to-client")
4545
async def test_result_meta_reaches_client(connect: Connect) -> None:
4646
"""The _meta object a handler attaches to its result is delivered to the client unchanged."""
4747
result_meta = {"example.com/cost": 3}

‎tests/interaction/lowlevel/test_resources.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ async def read_resource(ctx: ServerRequestContext, params: types.ReadResourceReq
140140
)
141141

142142

143-
@requirement("resources:read:unknown-uri")
143+
@requirement("protocol:error:handler-error-passthrough")
144144
async def test_read_resource_unknown_uri_is_protocol_error(connect: Connect) -> None:
145145
"""A handler that rejects an unrecognised URI with MCPError produces a JSON-RPC error.
146146

‎tests/interaction/lowlevel/test_sampling.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ async def sampling_callback(
155155
)
156156

157157

158-
@requirement("sampling:create-message:image-content")
158+
@requirement("sampling:create:image-content")
159159
async def test_create_message_request_with_image_content_reaches_callback(connect: Connect) -> None:
160160
"""A sampling request message carrying image content arrives at the client callback intact.
161161
@@ -207,7 +207,7 @@ async def sampling_callback(
207207
)
208208

209209

210-
@requirement("sampling:create-message:image-content")
210+
@requirement("sampling:create:image-content")
211211
async def test_create_message_result_with_image_content_returns_to_handler(connect: Connect) -> None:
212212
"""A sampling result whose content is an image is returned to the requesting handler intact.
213213
@@ -281,7 +281,7 @@ async def sampling_callback(context: ClientRequestContext, params: CreateMessage
281281
assert result == snapshot(CallToolResult(content=[TextContent(text="-1: User rejected sampling request")]))
282282

283283

284-
@requirement("sampling:create-message:not-supported")
284+
@requirement("sampling:create:not-supported")
285285
async def test_create_message_without_callback_is_error(connect: Connect) -> None:
286286
"""A sampling request to a client with no sampling callback fails with the SDK's default error."""
287287

@@ -437,7 +437,7 @@ async def sampling_callback(
437437
assert captured == snapshot([SamplingCapability()])
438438

439439

440-
@requirement("sampling:create-message:audio-content")
440+
@requirement("sampling:create:audio-content")
441441
async def test_create_message_request_with_audio_content_reaches_callback(connect: Connect) -> None:
442442
"""A sampling request message carrying audio content arrives at the client callback intact.
443443
@@ -489,7 +489,7 @@ async def sampling_callback(
489489
)
490490

491491

492-
@requirement("sampling:create-message:audio-content")
492+
@requirement("sampling:create:audio-content")
493493
async def test_create_message_result_with_audio_content_returns_to_handler(connect: Connect) -> None:
494494
"""A sampling result whose content is audio is returned to the requesting handler intact.
495495

‎tests/interaction/mcpserver/test_context.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
pytestmark = pytest.mark.anyio
2828

2929

30-
@requirement("mcpserver:context:logging")
30+
@requirement("mcpserver:context:log-from-handler")
3131
@requirement("logging:capability:declared")
3232
async def test_context_logging_helpers_send_log_notifications(connect: Connect) -> None:
3333
"""Each Context logging helper sends a log message notification at the matching severity.
@@ -121,7 +121,7 @@ async def whoami(ctx: Context) -> str:
121121
assert request_id
122122

123123

124-
@requirement("mcpserver:context:logging")
124+
@requirement("mcpserver:context:log-from-handler")
125125
@requirement("protocol:progress:no-token")
126126
async def test_report_progress_without_a_progress_token_sends_nothing(connect: Connect) -> None:
127127
"""When the caller supplied no progress callback, Context.report_progress is a silent no-op.
@@ -153,7 +153,7 @@ async def collect(message: IncomingMessage) -> None:
153153
)
154154

155155

156-
@requirement("mcpserver:context:elicit")
156+
@requirement("mcpserver:context:elicit-from-handler")
157157
@requirement("tools:call:elicitation-roundtrip")
158158
async def test_context_elicit_returns_typed_result(connect: Connect) -> None:
159159
"""Context.elicit sends a form elicitation built from a pydantic schema and returns a typed result.

0 commit comments

Comments
 (0)