1515own contract) says should happen. Tests always pin the SDK's current behaviour. Where current
1616behaviour falls short of `behavior`, the gap is recorded as data: `divergence` on entries whose
1717tests pin the divergent behaviour, or `deferred` on entries that are tracked but not yet covered
18- by a test in this suite. `issue` carries the tracking link for a recorded gap once one is filed.
18+ by a test in this suite. An entry may carry both: `divergence` records the spec-compliance gap
19+ (issue-able) and `deferred` records why no test exists; `divergence` alone implies a test pins
20+ the divergent behaviour. `issue` carries the tracking link for a recorded gap once one is filed.
1921
2022`deferred` reasons take one of three shapes: where the behaviour is exercised elsewhere in this
2123repo the reason names the covering test path; where the SDK does not implement the behaviour at
@@ -85,6 +87,12 @@ def __post_init__(self) -> None:
8587 behavior = (
8688 "The client rejects sending notifications or registering handlers for capabilities it did not declare."
8789 ),
90+ divergence = Divergence (
91+ note = (
92+ "The client does not check its own declared capabilities before sending notifications or "
93+ "serving callbacks; nothing prevents a caller from violating the spec's SHOULD."
94+ ),
95+ ),
8896 deferred = (
8997 "Not implemented in the SDK: the client does not check its own declared capabilities before "
9098 "sending notifications or serving callbacks."
@@ -95,6 +103,12 @@ def __post_init__(self) -> None:
95103 behavior = (
96104 "The client rejects calls to methods (e.g. resources/list) for capabilities the server did not advertise."
97105 ),
106+ divergence = Divergence (
107+ note = (
108+ "The client sends any request regardless of the server's advertised capabilities and "
109+ "surfaces whatever the server answers; the spec's SHOULD is not enforced."
110+ ),
111+ ),
98112 deferred = (
99113 "Not implemented in the SDK: the client sends any request regardless of the server's "
100114 "advertised capabilities and surfaces whatever the server answers."
@@ -168,9 +182,19 @@ def __post_init__(self) -> None:
168182 "Before initialization completes, the client sends no requests other than pings, and the "
169183 "server sends no requests other than pings and logging."
170184 ),
185+ divergence = Divergence (
186+ note = (
187+ "The server's send methods (create_message / elicit_form / list_roots) do not check "
188+ "initialization state before sending; on the client side, Client always completes the "
189+ "handshake before any caller code runs."
190+ ),
191+ ),
171192 deferred = (
172- "Not yet covered here: the sender-side restraint (especially the server half — no sampling, "
173- "elicitation, or roots requests before the initialized notification) has no test yet."
193+ "Not implemented in the SDK: neither side enforces sender-side restraint. The server's send "
194+ "methods (create_message / elicit_form / list_roots) do not check initialization state before "
195+ "sending, and there is no natural hook to issue a server-to-client request between the "
196+ "initialize response and the initialized notification through the public API; on the client "
197+ "side, Client always completes the handshake before any caller code runs."
174198 ),
175199 ),
176200 "lifecycle:version:downgrade" : Requirement (
@@ -179,12 +203,6 @@ def __post_init__(self) -> None:
179203 "When the server returns an older supported protocol version, the client downgrades to it "
180204 "and the connection succeeds at that version."
181205 ),
182- transports = ("streamable-http" ,),
183- deferred = (
184- "Not yet covered here: observing the negotiated version requires the MCP-Protocol-Version "
185- "request header, which only exists on the HTTP transport; planned with the transport "
186- "conformance work."
187- ),
188206 ),
189207 "lifecycle:version:match" : Requirement (
190208 source = f"{ SPEC_BASE_URL } /basic/lifecycle#version-negotiation" ,
@@ -268,9 +286,13 @@ def __post_init__(self) -> None:
268286 "A response that arrives after the sender issued notifications/cancelled is ignored; the "
269287 "request stays failed and no error is raised."
270288 ),
271- deferred = (
272- "Not yet covered here: needs the scripted-peer wire pattern to deliver a response after a "
273- "cancellation; today the receive loop logs an unknown-request-id error for such responses."
289+ divergence = Divergence (
290+ note = (
291+ "A response whose id matches no in-flight request is delivered to the message handler "
292+ "as a RuntimeError rather than being silently ignored. The post-cancellation case is the "
293+ "same code path; tested in its unknown-id form because that is deterministic without the "
294+ "client-side cancellation API the SDK does not yet provide."
295+ ),
274296 ),
275297 ),
276298 "protocol:cancel:server-survives" : Requirement (
@@ -283,6 +305,13 @@ def __post_init__(self) -> None:
283305 "A server that abandons an in-flight server-initiated request (sampling, elicitation, roots) "
284306 "cancels it, and the client stops processing the cancelled request."
285307 ),
308+ divergence = Divergence (
309+ note = (
310+ "Abandoning a server-side send_request emits no cancellation notification, and the client "
311+ "could not act on one anyway: client callbacks run inline in the receive loop, so a "
312+ "cancellation is not even read until the callback has finished."
313+ ),
314+ ),
286315 deferred = (
287316 "Not implemented in the SDK: abandoning a server-side send_request emits no cancellation "
288317 "notification (the same sender-side gap recorded on protocol:timeout:sends-cancellation), and "
@@ -311,10 +340,6 @@ def __post_init__(self) -> None:
311340 "protocol:error:connection-closed" : Requirement (
312341 source = "sdk" ,
313342 behavior = "Closing the transport fails all in-flight requests with a connection-closed error." ,
314- deferred = (
315- "Not yet covered here: planned gap test (close the transport while a request is in flight and "
316- "pin the error the caller receives)."
317- ),
318343 ),
319344 "protocol:error:internal-error" : Requirement (
320345 source = f"{ SPEC_BASE_URL } /basic#responses" ,
@@ -332,10 +357,6 @@ def __post_init__(self) -> None:
332357 "protocol:error:invalid-params" : Requirement (
333358 source = f"{ SPEC_BASE_URL } /basic#responses" ,
334359 behavior = "A request with malformed params is answered with JSON-RPC error -32602 Invalid params." ,
335- deferred = (
336- "Not yet covered here: the typed client API cannot send malformed params; needs a request "
337- "driven one level below it (planned gap test)."
338- ),
339360 ),
340361 "protocol:error:method-not-found" : Requirement (
341362 source = f"{ SPEC_BASE_URL } /basic#responses" ,
@@ -371,27 +392,34 @@ def __post_init__(self) -> None:
371392 "protocol:progress:token-unique" : Requirement (
372393 source = f"{ SPEC_BASE_URL } /basic/utilities/progress#progress-flow" ,
373394 behavior = ("Concurrent in-flight requests that each supply a progress callback carry distinct progress tokens." ),
374- deferred = (
375- "Not yet covered here: planned gap test (two concurrent requests with progress callbacks, "
376- "asserting their tokens differ and each callback only sees its own notifications)."
377- ),
378395 ),
379396 "protocol:progress:monotonic" : Requirement (
380397 source = f"{ SPEC_BASE_URL } /basic/utilities/progress#progress-flow" ,
381398 behavior = (
382399 "The progress value increases with each notification for a given token, even when the total is unknown."
383400 ),
384- deferred = (
385- "Not implemented in the SDK: progress values are not validated anywhere; a handler can emit "
386- "non-increasing values and they are forwarded as-is."
401+ divergence = Divergence (
402+ note = (
403+ "The spec MUST is not enforced: progress values are not validated on either side, so a "
404+ "handler that emits non-increasing values has them forwarded to the callback unchanged."
405+ ),
387406 ),
388407 ),
389408 "protocol:progress:stops-after-completion" : Requirement (
390409 source = f"{ SPEC_BASE_URL } /basic/utilities/progress#behavior-requirements" ,
391410 behavior = "Progress notifications for a token stop once the associated request completes." ,
392- deferred = (
393- "Not yet covered here: needs a test that a handler reporting progress after its request "
394- "completed produces no further notifications for the caller."
411+ divergence = Divergence (
412+ note = (
413+ "send_progress_notification does not check whether the token's request has already "
414+ "completed; the late notification is sent and reaches the client."
415+ ),
416+ ),
417+ ),
418+ "protocol:progress:late-dropped-by-client" : Requirement (
419+ source = "sdk" ,
420+ behavior = (
421+ "A progress notification that arrives after its request has completed is not delivered to the "
422+ "original progress callback."
395423 ),
396424 ),
397425 "protocol:progress:no-token" : Requirement (
@@ -415,6 +443,12 @@ def __post_init__(self) -> None:
415443 "protocol:timeout:max-total" : Requirement (
416444 source = f"{ SPEC_BASE_URL } /basic/lifecycle#timeouts" ,
417445 behavior = "A maximum total timeout is enforced even when progress notifications keep arriving." ,
446+ divergence = Divergence (
447+ note = (
448+ "There is no maximum-total-timeout option; only the per-request read timeout exists, so the "
449+ "spec's SHOULD that an overall maximum is always enforced cannot be satisfied."
450+ ),
451+ ),
418452 deferred = (
419453 "Not implemented in the SDK: there is no maximum-total-timeout option; only the per-request "
420454 "read timeout exists."
@@ -1097,6 +1131,12 @@ def __post_init__(self) -> None:
10971131 "The server does not use includeContext values thisServer or allServers unless the client "
10981132 "declared the sampling.context capability."
10991133 ),
1134+ divergence = Divergence (
1135+ note = (
1136+ "include_context is forwarded regardless of the client's declared sampling.context "
1137+ "capability; the server-side validator only checks tools/tool_choice."
1138+ ),
1139+ ),
11001140 deferred = (
11011141 "Not implemented in the SDK: include_context is forwarded regardless of the client's declared "
11021142 "sampling.context capability (unlike tools, which are gated by the server-side validator)."
@@ -1223,6 +1263,12 @@ def __post_init__(self) -> None:
12231263 "The server refuses to send an elicitation request with a mode the connected client did not "
12241264 "declare in its capabilities."
12251265 ),
1266+ divergence = Divergence (
1267+ note = (
1268+ "The server does not check the client's declared elicitation modes before sending "
1269+ "elicitation/create; the spec's SHOULD is not enforced."
1270+ ),
1271+ ),
12261272 deferred = (
12271273 "Not implemented in the SDK: the server does not check the client's declared elicitation "
12281274 "modes before sending elicitation/create."
@@ -1295,6 +1341,12 @@ def __post_init__(self) -> None:
12951341 "Form-mode requested schemas are flat objects with primitive-typed properties only; nested "
12961342 "structures and arrays of objects are not used."
12971343 ),
1344+ divergence = Divergence (
1345+ note = (
1346+ "Nothing restricts or validates the requested-schema shape on the sending side; a server "
1347+ "can send nested or non-primitive schemas and the SDK forwards them unchanged."
1348+ ),
1349+ ),
12981350 deferred = (
12991351 "Not implemented in the SDK: nothing restricts or validates the requested-schema shape on the "
13001352 "sending side; hand-built lowlevel elicitation requests pass through unchecked."
@@ -1306,6 +1358,9 @@ def __post_init__(self) -> None:
13061358 "Accepted form-mode content is validated against the requested schema: the client validates "
13071359 "the response before sending and the server validates the content it receives."
13081360 ),
1361+ divergence = Divergence (
1362+ note = "Accepted elicitation content passes through unvalidated on both sides." ,
1363+ ),
13091364 deferred = ("Not implemented in the SDK: accepted elicitation content passes through unvalidated on both sides." ),
13101365 ),
13111366 "elicitation:url:action:accept-no-content" : Requirement (
@@ -2147,6 +2202,12 @@ def __post_init__(self) -> None:
21472202 "with a fresh InitializeRequest and no session ID attached."
21482203 ),
21492204 transports = ("streamable-http" ,),
2205+ divergence = Divergence (
2206+ note = (
2207+ "The client surfaces the 404 as an error to the caller instead of re-initializing a new "
2208+ "session; the spec's MUST is not satisfied."
2209+ ),
2210+ ),
21502211 deferred = (
21512212 "Not implemented in the SDK: the client surfaces the 404 as an error to the caller instead of "
21522213 "re-initializing a new session."
0 commit comments