@@ -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" ,
@@ -399,7 +404,7 @@ def __post_init__(self) -> None:
399404 source = f"{ SPEC_2026_BASE_URL } /server/discover" ,
400405 behavior = (
401406 "Calling discover() sends server/discover with no params and returns a typed DiscoverResult "
402- "carrying protocolVersion , capabilities, serverInfo and the cache hint fields."
407+ "carrying supportedVersions , capabilities, serverInfo and the cache hint fields."
403408 ),
404409 added_in = "2026-07-28" ,
405410 ),
@@ -414,18 +419,25 @@ def __post_init__(self) -> None:
414419 "lifecycle:discover:fallback-method-not-found" : Requirement (
415420 source = f"{ SPEC_2026_BASE_URL } /basic/transports/stdio#backward-compatibility" ,
416421 behavior = (
417- "When server/discover returns any JSON-RPC error or a bare HTTP 4xx, an auto-negotiating "
418- "client falls back to the legacy initialize handshake and the connection succeeds at a "
419- "handshake-era version (legacy servers reject the probe with various codes)."
422+ "When server/discover returns a JSON-RPC error that is not a recognized modern negotiation "
423+ "error (-32022 retries or raises instead; see lifecycle:discover:retry-on-32022), or a bare "
424+ "HTTP 4xx, an auto-negotiating client falls back to the legacy initialize handshake and the "
425+ "connection succeeds at a handshake-era version; the fallback is not keyed to specific codes "
426+ "(legacy servers reject the probe with various codes)."
420427 ),
421428 added_in = "2026-07-28" ,
429+ note = (
430+ "The SDK keys its no-fallback carve-out to -32022 alone, while the spec's carve-out is any "
431+ "recognized modern JSON-RPC error (an open set); no test drives a modern-error probe "
432+ "rejection other than -32022."
433+ ),
422434 ),
423435 "lifecycle:discover:network-error-raises" : Requirement (
424436 source = "sdk" ,
425437 behavior = (
426438 "A network/connection error during server/discover propagates to the caller without "
427- "falling back to initialize; any rpc-error or 4xx falls back (legacy servers reject the "
428- "probe with various codes ). An outage is never an era verdict."
439+ "falling back to initialize; fallback is reserved for server rejections (see "
440+ "lifecycle:discover:fallback-method-not-found ). An outage is never an era verdict."
429441 ),
430442 transports = ("streamable-http" , "streamable-http-stateless" ),
431443 added_in = "2026-07-28" ,
@@ -613,14 +625,15 @@ def __post_init__(self) -> None:
613625 note = (
614626 "The dispatcher drops null-id error responses with a debug log; in v1, JSONRPCError.id was "
615627 "non-nullable, so a null-id error response failed transport validation and the resulting "
616- "ValidationError was surfaced to message_handler as an exception. A typed fault channel "
617- "restoring visibility is planned before v2 stable."
628+ "ValidationError was surfaced to message_handler as an exception. The v2 fault channel "
629+ "exists (message_handler receives stream exceptions), but response routing drops the "
630+ "null-id error before anything reaches it."
618631 ),
619632 ),
620633 deferred = (
621634 "Not yet covered here: the current drop is pinned at the dispatcher level by "
622- "tests/shared/test_jsonrpc_dispatcher.py; an interaction-level test waits on the planned "
623- "fault channel."
635+ "tests/shared/test_jsonrpc_dispatcher.py; an interaction-level test waits on the dispatcher "
636+ "routing null-id errors into the existing fault channel."
624637 ),
625638 ),
626639 "protocol:meta:related-task" : Requirement (
@@ -851,6 +864,13 @@ def __post_init__(self) -> None:
851864 "tools:call:structured-content:text-mirror" : Requirement (
852865 source = f"{ SPEC_BASE_URL } /server/tools#structured-content" ,
853866 behavior = "A tool returning structured content also returns the serialized JSON as a text content block." ,
867+ divergence = Divergence (
868+ note = (
869+ "Holds for object returns (the bound test pins the serialized-JSON mirror); a "
870+ "list-returning tool yields one text block per element rather than the serialized JSON "
871+ "of its structured value (pinned by the test on mcpserver:tool:output-schema:wrapped)."
872+ ),
873+ ),
854874 ),
855875 "tools:call:unknown-name" : Requirement (
856876 source = f"{ SPEC_BASE_URL } /server/tools#error-handling" ,
@@ -1052,6 +1072,13 @@ def __post_init__(self) -> None:
10521072 "The Context logging helpers (debug/info/warning/error) send log message notifications at the "
10531073 "corresponding severity."
10541074 ),
1075+ divergence = Divergence (
1076+ note = (
1077+ "At 2026-07-28 the spec forbids notifications/message for a request whose _meta lacks the "
1078+ "io.modelcontextprotocol/logLevel opt-in; the Context helpers never read that key and emit "
1079+ "unconditionally, so a bound test pins the un-gated delivery on the live 2026-07-28 cells."
1080+ ),
1081+ ),
10551082 ),
10561083 "mcpserver:context:progress" : Requirement (
10571084 source = "sdk" ,
@@ -1340,7 +1367,7 @@ def __post_init__(self) -> None:
13401367 behavior = "prompts/get for a name that was never registered returns JSON-RPC error -32602 (Invalid params)." ,
13411368 divergence = Divergence (
13421369 note = (
1343- "The spec's example uses -32602 Invalid params for unknown prompts ; MCPServer raises "
1370+ "The spec SHOULD-lists -32602 Invalid params for an invalid prompt name ; MCPServer raises "
13441371 "ValueError, which the low-level server converts to error code 0."
13451372 ),
13461373 ),
@@ -2027,9 +2054,8 @@ def __post_init__(self) -> None:
20272054 "mcpserver:register:post-connect" : Requirement (
20282055 source = "sdk" ,
20292056 behavior = (
2030- "A tool, resource, or prompt registered or removed after the client connected appears in (or "
2031- "disappears from) the corresponding list results, and the change is announced with a "
2032- "list_changed notification."
2057+ "A tool registered or removed after the client connected appears in (or disappears from) "
2058+ "tools/list results, and the change is announced with a list_changed notification."
20332059 ),
20342060 divergence = Divergence (
20352061 note = (
@@ -2481,8 +2507,7 @@ def __post_init__(self) -> None:
24812507 "transport:streamable-http:notifications" : Requirement (
24822508 source = f"{ SPEC_BASE_URL } /basic/transports#streamable-http" ,
24832509 behavior = (
2484- "Notifications emitted during a request are delivered on that request's SSE stream and reach "
2485- "the client's callbacks, in order, before the response."
2510+ "Notifications emitted during a request reach the client's callbacks over the streamable HTTP framing."
24862511 ),
24872512 transports = ("streamable-http" ,),
24882513 note = "Only observable over streamable HTTP: per-request SSE streams are HTTP-specific." ,
@@ -3095,8 +3120,9 @@ def __post_init__(self) -> None:
30953120 "hosting:http:protocol-version-rejection-literal" : Requirement (
30963121 source = "sdk" ,
30973122 behavior = (
3098- "The legacy streamable-HTTP transport's version-rejection body contains the literal substring "
3099- "'Unsupported protocol version', which other-SDK clients substring-match during negotiation."
3123+ "The streamable-HTTP version-rejection body contains the literal substring 'Unsupported "
3124+ "protocol version', which other-SDK clients substring-match during negotiation; the modern "
3125+ "request classifier is its only emission site."
31003126 ),
31013127 transports = ("streamable-http" ,),
31023128 note = (
@@ -3138,17 +3164,18 @@ def __post_init__(self) -> None:
31383164 ),
31393165 "hosting:http:modern:initialize-removed" : Requirement (
31403166 source = f"{ SPEC_2026_BASE_URL } /basic/index" ,
3141- behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND." ,
3167+ behavior = "A 2026-07-28 initialize request is answered with METHOD_NOT_FOUND at HTTP 404 ." ,
31423168 added_in = "2026-07-28" ,
31433169 transports = ("streamable-http" ,),
31443170 note = ("Only observable over streamable HTTP: the modern entry's method registry omits initialize." ),
31453171 ),
31463172 "hosting:http:modern:legacy-fallthrough" : Requirement (
31473173 source = f"{ SPEC_2026_BASE_URL } /basic/versioning" ,
31483174 behavior = (
3149- "Non-2026-07-28 traffic on the same /mcp endpoint reaches the legacy transport "
3150- "byte-unchanged: a 2025-era initialize handshake still completes, and an unrecognised "
3151- "MCP-Protocol-Version header still produces the legacy 400 'Unsupported protocol version' literal."
3175+ "Initialize-handshake-era traffic on the same /mcp endpoint reaches the legacy transport "
3176+ "byte-unchanged: a 2025-era initialize handshake still completes. Any other "
3177+ "MCP-Protocol-Version header routes to the modern entry, whose validation ladder rejects "
3178+ "the envelope-less request with 400 INVALID_PARAMS."
31523179 ),
31533180 added_in = "2026-07-28" ,
31543181 transports = ("streamable-http" ,),
@@ -3267,10 +3294,7 @@ def __post_init__(self) -> None:
32673294 ),
32683295 "client-transport:http:custom-client" : Requirement (
32693296 source = "sdk" ,
3270- behavior = (
3271- "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic, "
3272- "including auth flows."
3273- ),
3297+ behavior = "A caller-supplied HTTP client (and its event hooks and headers) is used for all MCP traffic." ,
32743298 transports = ("streamable-http" ,),
32753299 note = "Only observable over HTTP: the httpx client is HTTP-specific." ,
32763300 ),
@@ -3442,9 +3466,7 @@ def __post_init__(self) -> None:
34423466 ),
34433467 "client-auth:403-scope-upgrade" : Requirement (
34443468 source = f"{ SPEC_BASE_URL } /basic/authorization#step-up-authorization-flow" ,
3445- behavior = (
3446- "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt; repeated 403s do not loop."
3447- ),
3469+ behavior = "A 403 with WWW-Authenticate triggers a scope-upgrade authorization attempt." ,
34483470 transports = ("streamable-http" ,),
34493471 note = "OAuth is HTTP-only." ,
34503472 ),
@@ -3691,8 +3713,7 @@ def __post_init__(self) -> None:
36913713 source = f"{ SPEC_2026_BASE_URL } /basic/authorization#authorization-response-validation" ,
36923714 behavior = (
36933715 "The client validates the RFC 9207 iss authorization-response parameter against the "
3694- "authorization server issuer (simple string comparison) and rejects a mismatch, or a "
3695- "missing iss when the server advertises support (SEP-2468)."
3716+ "authorization server issuer (simple string comparison) and rejects a mismatch (SEP-2468)."
36963717 ),
36973718 added_in = "2026-07-28" ,
36983719 transports = ("streamable-http" ,),
0 commit comments