@@ -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 before v2 stable."
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 ),
@@ -2124,9 +2151,8 @@ def __post_init__(self) -> None:
21242151 "mcpserver:register:post-connect" : Requirement (
21252152 source = "sdk" ,
21262153 behavior = (
2127- "A tool, resource, or prompt registered or removed after the client connected appears in (or "
2128- "disappears from) the corresponding list results, and the change is announced with a "
2129- "list_changed notification."
2154+ "A tool registered or removed after the client connected appears in (or disappears from) "
2155+ "tools/list results, and the change is announced with a list_changed notification."
21302156 ),
21312157 divergence = Divergence (
21322158 note = (
@@ -2594,8 +2620,7 @@ def __post_init__(self) -> None:
25942620 "transport:streamable-http:notifications" : Requirement (
25952621 source = f"{ SPEC_BASE_URL } /basic/transports#streamable-http" ,
25962622 behavior = (
2597- "Notifications emitted during a request are delivered on that request's SSE stream and reach "
2598- "the client's callbacks, in order, before the response."
2623+ "Notifications emitted during a request reach the client's callbacks over the streamable HTTP framing."
25992624 ),
26002625 transports = ("streamable-http" ,),
26012626 note = "Only observable over streamable HTTP: per-request SSE streams are HTTP-specific." ,
@@ -3219,8 +3244,9 @@ def __post_init__(self) -> None:
32193244 "hosting:http:protocol-version-rejection-literal" : Requirement (
32203245 source = "sdk" ,
32213246 behavior = (
3222- "The legacy streamable-HTTP transport's version-rejection body contains the literal substring "
3223- "'Unsupported protocol version', which other-SDK clients substring-match during negotiation."
3247+ "The streamable-HTTP version-rejection body contains the literal substring 'Unsupported "
3248+ "protocol version', which other-SDK clients substring-match during negotiation; the modern "
3249+ "request classifier is its only emission site."
32243250 ),
32253251 transports = ("streamable-http" ,),
32263252 note = (
@@ -3262,17 +3288,18 @@ def __post_init__(self) -> None:
32623288 ),
32633289 "hosting:http:modern:initialize-removed" : Requirement (
32643290 source = f"{ SPEC_2026_BASE_URL } /basic/index" ,
3265- behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND." ,
3291+ behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND at HTTP 404 ." ,
32663292 added_in = "2026-07-28" ,
32673293 transports = ("streamable-http" ,),
32683294 note = ("Only observable over streamable HTTP: the modern entry's method registry omits initialize." ),
32693295 ),
32703296 "hosting:http:modern:legacy-fallthrough" : Requirement (
32713297 source = f"{ SPEC_2026_BASE_URL } /basic/versioning" ,
32723298 behavior = (
3273- "Non-2026-07-28 traffic on the same /mcp endpoint reaches the legacy transport "
3274- "byte-unchanged: a 2025-era initialize handshake still completes, and an unrecognised "
3275- "MCP-Protocol-Version header still produces the legacy 400 'Unsupported protocol version' literal."
3299+ "Initialize-handshake-era traffic on the same /mcp endpoint reaches the legacy transport "
3300+ "byte-unchanged: a 2025-era initialize handshake still completes. Any other "
3301+ "MCP-Protocol-Version header routes to the modern entry, whose validation ladder rejects "
3302+ "the envelope-less request with 400 INVALID_PARAMS."
32763303 ),
32773304 added_in = "2026-07-28" ,
32783305 transports = ("streamable-http" ,),
@@ -3417,10 +3444,7 @@ def __post_init__(self) -> None:
34173444 ),
34183445 "client-transport:http:custom-client" : Requirement (
34193446 source = "sdk" ,
3420- behavior = (
3421- "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic, "
3422- "including auth flows."
3423- ),
3447+ behavior = "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic." ,
34243448 transports = ("streamable-http" ,),
34253449 note = "Only observable over HTTP: the httpx2 client is HTTP-specific." ,
34263450 ),
@@ -3592,9 +3616,7 @@ def __post_init__(self) -> None:
35923616 ),
35933617 "client-auth:403-scope-upgrade" : Requirement (
35943618 source = f"{ SPEC_BASE_URL } /basic/authorization#step-up-authorization-flow" ,
3595- behavior = (
3596- "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt; repeated 403s do not loop."
3597- ),
3619+ behavior = "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt." ,
35983620 transports = ("streamable-http" ,),
35993621 note = "OAuth is HTTP-only." ,
36003622 ),
@@ -3854,8 +3876,7 @@ def __post_init__(self) -> None:
38543876 source = f"{ SPEC_2026_BASE_URL } /basic/authorization#authorization-response-validation" ,
38553877 behavior = (
38563878 "The client validates the RFC 9207 iss authorization-response parameter against the "
3857- "authorization server issuer (simple string comparison) and rejects a mismatch, or a "
3858- "missing iss when the server advertises support (SEP-2468)."
3879+ "authorization server issuer (simple string comparison) and rejects a mismatch (SEP-2468)."
38593880 ),
38603881 added_in = "2026-07-28" ,
38613882 transports = ("streamable-http" ,),
0 commit comments