@@ -383,10 +383,15 @@ def __post_init__(self) -> None:
383383 source = f"{ SPEC_2026_BASE_URL } /basic#_meta" ,
384384 behavior = (
385385 "Every client→server request on a modern-negotiated session carries "
386- "_meta.{protocolVersion,clientInfo,clientCapabilities}; notifications do not ."
386+ "_meta.{protocolVersion,clientInfo,clientCapabilities}."
387387 ),
388388 added_in = "2026-07-28" ,
389389 supersedes = ("lifecycle:initialize:client-info" , "lifecycle:initialize:client-capabilities" ),
390+ note = (
391+ "The spec MUST covers requests only. The session's modern stamp is message-agnostic, so "
392+ "session-sent notifications carry the envelope too, while dispatcher-built frames (the "
393+ "courtesy cancel) do not; neither notification arm is asserted here."
394+ ),
390395 ),
391396 "lifecycle:envelope:header-matches-meta" : Requirement (
392397 source = f"{ SPEC_2026_BASE_URL } /basic/transports/streamable-http#protocol-version-header" ,
@@ -415,18 +420,25 @@ def __post_init__(self) -> None:
415420 "lifecycle:discover:fallback-method-not-found" : Requirement (
416421 source = f"{ SPEC_2026_BASE_URL } /basic/transports/stdio#backward-compatibility" ,
417422 behavior = (
418- "When server/discover returns any JSON-RPC error or a bare HTTP 4xx, an auto-negotiating "
419- "client falls back to the legacy initialize handshake and the connection succeeds at a "
420- "handshake-era version (legacy servers reject the probe with various codes)."
423+ "When server/discover returns a JSON-RPC error that is not a recognized modern negotiation "
424+ "error (-32022 retries or raises instead; see lifecycle:discover:retry-on-32022), or a bare "
425+ "HTTP 4xx, an auto-negotiating client falls back to the legacy initialize handshake and the "
426+ "connection succeeds at a handshake-era version; the fallback is not keyed to specific codes "
427+ "(legacy servers reject the probe with various codes)."
421428 ),
422429 added_in = "2026-07-28" ,
430+ note = (
431+ "The SDK keys its no-fallback carve-out to -32022 alone, while the spec's carve-out is any "
432+ "recognized modern JSON-RPC error (an open set); no test drives a modern-error probe "
433+ "rejection other than -32022."
434+ ),
423435 ),
424436 "lifecycle:discover:network-error-raises" : Requirement (
425437 source = "sdk" ,
426438 behavior = (
427439 "A network/connection error during server/discover propagates to the caller without "
428- "falling back to initialize; any rpc-error or 4xx falls back (legacy servers reject the "
429- "probe with various codes ). An outage is never an era verdict."
440+ "falling back to initialize; fallback is reserved for server rejections (see "
441+ "lifecycle:discover:fallback-method-not-found ). An outage is never an era verdict."
430442 ),
431443 transports = ("streamable-http" , "streamable-http-stateless" ),
432444 added_in = "2026-07-28" ,
@@ -657,14 +669,15 @@ def __post_init__(self) -> None:
657669 note = (
658670 "The dispatcher drops null-id error responses with a debug log; in v1, JSONRPCError.id was "
659671 "non-nullable, so a null-id error response failed transport validation and the resulting "
660- "ValidationError was surfaced to message_handler as an exception. A typed fault channel "
661- "restoring visibility is planned."
672+ "ValidationError was surfaced to message_handler as an exception. The v2 fault channel "
673+ "exists (message_handler receives stream exceptions), but response routing drops the "
674+ "null-id error before anything reaches it."
662675 ),
663676 ),
664677 deferred = (
665678 "Not yet covered here: the current drop is pinned at the dispatcher level by "
666- "tests/shared/test_jsonrpc_dispatcher.py; an interaction-level test waits on the planned "
667- "fault channel."
679+ "tests/shared/test_jsonrpc_dispatcher.py; an interaction-level test waits on the dispatcher "
680+ "routing null-id errors into the existing fault channel."
668681 ),
669682 ),
670683 "protocol:meta:related-task" : Requirement (
@@ -895,6 +908,13 @@ def __post_init__(self) -> None:
895908 "tools:call:structured-content:text-mirror" : Requirement (
896909 source = f"{ SPEC_BASE_URL } /server/tools#structured-content" ,
897910 behavior = "A tool returning structured content also returns the serialized JSON as a text content block." ,
911+ divergence = Divergence (
912+ note = (
913+ "Holds for object returns (the bound test pins the serialized-JSON mirror); a "
914+ "list-returning tool yields one text block per element rather than the serialized JSON "
915+ "of its structured value (pinned by the test on mcpserver:tool:output-schema:wrapped)."
916+ ),
917+ ),
898918 ),
899919 "tools:call:unknown-name" : Requirement (
900920 source = f"{ SPEC_BASE_URL } /server/tools#error-handling" ,
@@ -1096,6 +1116,13 @@ def __post_init__(self) -> None:
10961116 "The Context logging helpers (debug/info/warning/error) send log message notifications at the "
10971117 "corresponding severity."
10981118 ),
1119+ divergence = Divergence (
1120+ note = (
1121+ "At 2026-07-28 the spec forbids notifications/message for a request whose _meta lacks the "
1122+ "io.modelcontextprotocol/logLevel opt-in; the Context helpers never read that key and emit "
1123+ "unconditionally, so a bound test pins the un-gated delivery on the live 2026-07-28 cells."
1124+ ),
1125+ ),
10991126 ),
11001127 "mcpserver:context:progress" : Requirement (
11011128 source = "sdk" ,
@@ -1437,7 +1464,7 @@ def __post_init__(self) -> None:
14371464 behavior = "prompts/get for a name that was never registered returns JSON-RPC error -32602 (Invalid params)." ,
14381465 divergence = Divergence (
14391466 note = (
1440- "The spec's example uses -32602 Invalid params for unknown prompts ; MCPServer raises "
1467+ "The spec SHOULD-lists -32602 Invalid params for an invalid prompt name ; MCPServer raises "
14411468 "ValueError, which the low-level server converts to error code 0."
14421469 ),
14431470 ),
@@ -2155,9 +2182,8 @@ def __post_init__(self) -> None:
21552182 "mcpserver:register:post-connect" : Requirement (
21562183 source = "sdk" ,
21572184 behavior = (
2158- "A tool, resource, or prompt registered or removed after the client connected appears in (or "
2159- "disappears from) the corresponding list results, and the change is announced with a "
2160- "list_changed notification."
2185+ "A tool registered or removed after the client connected appears in (or disappears from) "
2186+ "tools/list results, and the change is announced with a list_changed notification."
21612187 ),
21622188 divergence = Divergence (
21632189 note = (
@@ -2635,8 +2661,7 @@ def __post_init__(self) -> None:
26352661 "transport:streamable-http:notifications" : Requirement (
26362662 source = f"{ SPEC_BASE_URL } /basic/transports#streamable-http" ,
26372663 behavior = (
2638- "Notifications emitted during a request are delivered on that request's SSE stream and reach "
2639- "the client's callbacks, in order, before the response."
2664+ "Notifications emitted during a request reach the client's callbacks over the streamable HTTP framing."
26402665 ),
26412666 transports = ("streamable-http" ,),
26422667 note = "Only observable over streamable HTTP: per-request SSE streams are HTTP-specific." ,
@@ -3260,8 +3285,9 @@ def __post_init__(self) -> None:
32603285 "hosting:http:protocol-version-rejection-literal" : Requirement (
32613286 source = "sdk" ,
32623287 behavior = (
3263- "The legacy streamable-HTTP transport's version-rejection body contains the literal substring "
3264- "'Unsupported protocol version', which other-SDK clients substring-match during negotiation."
3288+ "The streamable-HTTP version-rejection body contains the literal substring 'Unsupported "
3289+ "protocol version', which other-SDK clients substring-match during negotiation; the modern "
3290+ "request classifier is its only emission site."
32653291 ),
32663292 transports = ("streamable-http" ,),
32673293 note = (
@@ -3303,17 +3329,18 @@ def __post_init__(self) -> None:
33033329 ),
33043330 "hosting:http:modern:initialize-removed" : Requirement (
33053331 source = f"{ SPEC_2026_BASE_URL } /basic/index" ,
3306- behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND." ,
3332+ behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND at HTTP 404 ." ,
33073333 added_in = "2026-07-28" ,
33083334 transports = ("streamable-http" ,),
33093335 note = ("Only observable over streamable HTTP: the modern entry's method registry omits initialize." ),
33103336 ),
33113337 "hosting:http:modern:legacy-fallthrough" : Requirement (
33123338 source = f"{ SPEC_2026_BASE_URL } /basic/versioning" ,
33133339 behavior = (
3314- "Non-2026-07-28 traffic on the same /mcp endpoint reaches the legacy transport "
3315- "byte-unchanged: a 2025-era initialize handshake still completes, and an unrecognised "
3316- "MCP-Protocol-Version header still produces the legacy 400 'Unsupported protocol version' literal."
3340+ "Initialize-handshake-era traffic on the same /mcp endpoint reaches the legacy transport "
3341+ "byte-unchanged: a 2025-era initialize handshake still completes. Any other "
3342+ "MCP-Protocol-Version header routes to the modern entry, whose validation ladder rejects "
3343+ "the envelope-less request with 400 INVALID_PARAMS."
33173344 ),
33183345 added_in = "2026-07-28" ,
33193346 transports = ("streamable-http" ,),
@@ -3458,10 +3485,7 @@ def __post_init__(self) -> None:
34583485 ),
34593486 "client-transport:http:custom-client" : Requirement (
34603487 source = "sdk" ,
3461- behavior = (
3462- "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic, "
3463- "including auth flows."
3464- ),
3488+ behavior = "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic." ,
34653489 transports = ("streamable-http" ,),
34663490 note = "Only observable over HTTP: the httpx2 client is HTTP-specific." ,
34673491 ),
@@ -3633,9 +3657,7 @@ def __post_init__(self) -> None:
36333657 ),
36343658 "client-auth:403-scope-upgrade" : Requirement (
36353659 source = f"{ SPEC_BASE_URL } /basic/authorization#step-up-authorization-flow" ,
3636- behavior = (
3637- "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt; repeated 403s do not loop."
3638- ),
3660+ behavior = "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt." ,
36393661 transports = ("streamable-http" ,),
36403662 note = "OAuth is HTTP-only." ,
36413663 ),
@@ -3895,8 +3917,7 @@ def __post_init__(self) -> None:
38953917 source = f"{ SPEC_2026_BASE_URL } /basic/authorization#authorization-response-validation" ,
38963918 behavior = (
38973919 "The client validates the RFC 9207 iss authorization-response parameter against the "
3898- "authorization server issuer (simple string comparison) and rejects a mismatch, or a "
3899- "missing iss when the server advertises support (SEP-2468)."
3920+ "authorization server issuer (simple string comparison) and rejects a mismatch (SEP-2468)."
39003921 ),
39013922 added_in = "2026-07-28" ,
39023923 transports = ("streamable-http" ,),
0 commit comments