@@ -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" ,
@@ -655,14 +667,15 @@ def __post_init__(self) -> None:
655667 note = (
656668 "The dispatcher drops null-id error responses with a debug log; in v1, JSONRPCError.id was "
657669 "non-nullable, so a null-id error response failed transport validation and the resulting "
658- "ValidationError was surfaced to message_handler as an exception. A typed fault channel "
659- "restoring visibility is planned before v2 stable."
670+ "ValidationError was surfaced to message_handler as an exception. The v2 fault channel "
671+ "exists (message_handler receives stream exceptions), but response routing drops the "
672+ "null-id error before anything reaches it."
660673 ),
661674 ),
662675 deferred = (
663676 "Not yet covered here: the current drop is pinned at the dispatcher level by "
664- "tests/shared/test_jsonrpc_dispatcher.py; an interaction-level test waits on the planned "
665- "fault channel."
677+ "tests/shared/test_jsonrpc_dispatcher.py; an interaction-level test waits on the dispatcher "
678+ "routing null-id errors into the existing fault channel."
666679 ),
667680 ),
668681 "protocol:meta:related-task" : Requirement (
@@ -893,6 +906,13 @@ def __post_init__(self) -> None:
893906 "tools:call:structured-content:text-mirror" : Requirement (
894907 source = f"{ SPEC_BASE_URL } /server/tools#structured-content" ,
895908 behavior = "A tool returning structured content also returns the serialized JSON as a text content block." ,
909+ divergence = Divergence (
910+ note = (
911+ "Holds for object returns (the bound test pins the serialized-JSON mirror); a "
912+ "list-returning tool yields one text block per element rather than the serialized JSON "
913+ "of its structured value (pinned by the test on mcpserver:tool:output-schema:wrapped)."
914+ ),
915+ ),
896916 ),
897917 "tools:call:unknown-name" : Requirement (
898918 source = f"{ SPEC_BASE_URL } /server/tools#error-handling" ,
@@ -1094,6 +1114,13 @@ def __post_init__(self) -> None:
10941114 "The Context logging helpers (debug/info/warning/error) send log message notifications at the "
10951115 "corresponding severity."
10961116 ),
1117+ divergence = Divergence (
1118+ note = (
1119+ "At 2026-07-28 the spec forbids notifications/message for a request whose _meta lacks the "
1120+ "io.modelcontextprotocol/logLevel opt-in; the Context helpers never read that key and emit "
1121+ "unconditionally, so a bound test pins the un-gated delivery on the live 2026-07-28 cells."
1122+ ),
1123+ ),
10971124 ),
10981125 "mcpserver:context:progress" : Requirement (
10991126 source = "sdk" ,
@@ -1435,7 +1462,7 @@ def __post_init__(self) -> None:
14351462 behavior = "prompts/get for a name that was never registered returns JSON-RPC error -32602 (Invalid params)." ,
14361463 divergence = Divergence (
14371464 note = (
1438- "The spec's example uses -32602 Invalid params for unknown prompts ; MCPServer raises "
1465+ "The spec SHOULD-lists -32602 Invalid params for an invalid prompt name ; MCPServer raises "
14391466 "ValueError, which the low-level server converts to error code 0."
14401467 ),
14411468 ),
@@ -2122,9 +2149,8 @@ def __post_init__(self) -> None:
21222149 "mcpserver:register:post-connect" : Requirement (
21232150 source = "sdk" ,
21242151 behavior = (
2125- "A tool, resource, or prompt registered or removed after the client connected appears in (or "
2126- "disappears from) the corresponding list results, and the change is announced with a "
2127- "list_changed notification."
2152+ "A tool registered or removed after the client connected appears in (or disappears from) "
2153+ "tools/list results, and the change is announced with a list_changed notification."
21282154 ),
21292155 divergence = Divergence (
21302156 note = (
@@ -2576,8 +2602,7 @@ def __post_init__(self) -> None:
25762602 "transport:streamable-http:notifications" : Requirement (
25772603 source = f"{ SPEC_BASE_URL } /basic/transports#streamable-http" ,
25782604 behavior = (
2579- "Notifications emitted during a request are delivered on that request's SSE stream and reach "
2580- "the client's callbacks, in order, before the response."
2605+ "Notifications emitted during a request reach the client's callbacks over the streamable HTTP framing."
25812606 ),
25822607 transports = ("streamable-http" ,),
25832608 note = "Only observable over streamable HTTP: per-request SSE streams are HTTP-specific." ,
@@ -3190,8 +3215,9 @@ def __post_init__(self) -> None:
31903215 "hosting:http:protocol-version-rejection-literal" : Requirement (
31913216 source = "sdk" ,
31923217 behavior = (
3193- "The legacy streamable-HTTP transport's version-rejection body contains the literal substring "
3194- "'Unsupported protocol version', which other-SDK clients substring-match during negotiation."
3218+ "The streamable-HTTP version-rejection body contains the literal substring 'Unsupported "
3219+ "protocol version', which other-SDK clients substring-match during negotiation; the modern "
3220+ "request classifier is its only emission site."
31953221 ),
31963222 transports = ("streamable-http" ,),
31973223 note = (
@@ -3233,17 +3259,18 @@ def __post_init__(self) -> None:
32333259 ),
32343260 "hosting:http:modern:initialize-removed" : Requirement (
32353261 source = f"{ SPEC_2026_BASE_URL } /basic/index" ,
3236- behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND." ,
3262+ behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND at HTTP 404 ." ,
32373263 added_in = "2026-07-28" ,
32383264 transports = ("streamable-http" ,),
32393265 note = ("Only observable over streamable HTTP: the modern entry's method registry omits initialize." ),
32403266 ),
32413267 "hosting:http:modern:legacy-fallthrough" : Requirement (
32423268 source = f"{ SPEC_2026_BASE_URL } /basic/versioning" ,
32433269 behavior = (
3244- "Non-2026-07-28 traffic on the same /mcp endpoint reaches the legacy transport "
3245- "byte-unchanged: a 2025-era initialize handshake still completes, and an unrecognised "
3246- "MCP-Protocol-Version header still produces the legacy 400 'Unsupported protocol version' literal."
3270+ "Initialize-handshake-era traffic on the same /mcp endpoint reaches the legacy transport "
3271+ "byte-unchanged: a 2025-era initialize handshake still completes. Any other "
3272+ "MCP-Protocol-Version header routes to the modern entry, whose validation ladder rejects "
3273+ "the envelope-less request with 400 INVALID_PARAMS."
32473274 ),
32483275 added_in = "2026-07-28" ,
32493276 transports = ("streamable-http" ,),
@@ -3388,10 +3415,7 @@ def __post_init__(self) -> None:
33883415 ),
33893416 "client-transport:http:custom-client" : Requirement (
33903417 source = "sdk" ,
3391- behavior = (
3392- "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic, "
3393- "including auth flows."
3394- ),
3418+ behavior = "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic." ,
33953419 transports = ("streamable-http" ,),
33963420 note = "Only observable over HTTP: the httpx2 client is HTTP-specific." ,
33973421 ),
@@ -3563,9 +3587,7 @@ def __post_init__(self) -> None:
35633587 ),
35643588 "client-auth:403-scope-upgrade" : Requirement (
35653589 source = f"{ SPEC_BASE_URL } /basic/authorization#step-up-authorization-flow" ,
3566- behavior = (
3567- "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt; repeated 403s do not loop."
3568- ),
3590+ behavior = "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt." ,
35693591 transports = ("streamable-http" ,),
35703592 note = "OAuth is HTTP-only." ,
35713593 ),
@@ -3812,8 +3834,7 @@ def __post_init__(self) -> None:
38123834 source = f"{ SPEC_2026_BASE_URL } /basic/authorization#authorization-response-validation" ,
38133835 behavior = (
38143836 "The client validates the RFC 9207 iss authorization-response parameter against the "
3815- "authorization server issuer (simple string comparison) and rejects a mismatch, or a "
3816- "missing iss when the server advertises support (SEP-2468)."
3837+ "authorization server issuer (simple string comparison) and rejects a mismatch (SEP-2468)."
38173838 ),
38183839 added_in = "2026-07-28" ,
38193840 transports = ("streamable-http" ,),
0 commit comments