Align with spec #3002: optional clientInfo, serverInfo in result _meta
Aligns the SDK with spec #3002 (merged 2026-07-16), the 2026-07-28 draft's identity reshape. Reference implementations: go#1097, ts#2513 (shipped in 2.0.0-beta.5).
What the spec changed
- The request
_metakeyio.modelcontextprotocol/clientInfois now optional (SHOULD-include); the required pair isprotocolVersion+clientCapabilities, and the-32602/HTTP 400 MUST-reject applies only to those. DiscoverResultno longer has a top-levelserverInfofield. Servers SHOULD stampio.modelcontextprotocol/serverInfointo every result's_metainstead (newResultMetaObjecttype), "unless specifically configured not to do so".
What this PR does
Types: re-vendors the draft schema at spec commit 71e30695 and regenerates the surface models. The new ResultMetaObject joins the generator's open-classes list (the spec defines it as an open key-value bag; without this the serialize sieve would drop custom result _meta keys).
Server: accepts requests that omit clientInfo (rejecting a missing required key with -32602 naming the key); records client_capabilities as its own connection fact so capability checks work for pair-only requests (ServerSession.client_capabilities is the new preferred accessor); era evidence on the stdio dual-era loop is now the reserved protocolVersion key alone. The response envelope is assembled in a single outbound pass in the runner (cache hints, the per-version sieve, and now the serverInfo _meta stamp), the outbound counterpart of the inbound classification ladder: every 2026-era result is stamped there, custom methods and empty results included. A handler-authored value wins; notifications, error responses, and handshake-era results are never stamped, and a middleware that short-circuits without call_next owns its result envelope entirely (the pipeline never patches it after the fact). Two consequences of that pass, from review: extension tools/call interceptors now compose around the handler instead of running as middleware, so a short-circuiting Extension.intercept_tool_call result is sieved and stamped exactly like a handler result (extension authors deal in domain results and cannot author wire envelopes); and the sieve applies to core-vocabulary results only - a claimed extension shape (a resultType outside complete/input_required) is the extension's to shape, though it is still stamped.
Client: reads server identity from the discover result's _meta stamp; Client.server_info / ClientSession.server_info are now Implementation | None. This also fixes a live interop break: typescript-sdk 2.0.0-beta.5 already omits the body serverInfo, which made our auto-mode probe treat a spec-conformant discover reply as unparseable and silently fall back to the legacy handshake. The probe also gained the complementary fallback that removing the required field exposed: a discover reply advertising only handshake-era versions (go-sdk's default stateful streamable server) is treated as an explicit legacy advertisement and runs initialize, matching the typescript and go clients.
Conformance: bumps the pinned referee to conformance da56f663 (includes conformance#403, the post-#3002 checks, and #406, per-check baselines). All server legs pass with zero skip entries. The bump window adds new client auth scenarios (DPoP per SEP-1932, and a jwt-bearer grant check) for features the OAuth client does not implement; those are baselined per-check. The requiredCapabilities fixtures move from the old array shape to the schema's object shape, and the everything-server capability gate now reads client_capabilities so it works for pair-only clients.
Decisions to review
- Public API break:
Client.server_infoisImplementation | None. Identity is optional wire metadata now; an anonymous server (or a pinned connection withoutprior_discover=) reads asNone, where it previously was a placeholderImplementation(name="", version=""). Not in the migration guide by design: that file covers only v1.x code that breaks on v2, andserver/discovernever existed in v1; the client and low-level server pages carry the details. - Malformed stamp reads as absent: the spec marks
serverInfodisplay-only (clients SHOULD NOT act on it), so a malformed value must never reject an otherwise-valid response. The wire surface keeps the field opaque (schema patch) and the typed, lenient parse happens at the read edge. typescript-sdk does the same with a schema-level catch. "clientInfo": nullis accepted as absent. go rejects an explicit null with-32602; this SDK treats it like a missing key. The field is optional and display-only, so we lean tolerant; flagging it in case reviewers prefer strictness.- Every modern result carries
resultType(schema-required: servers implementing 2026-07-28 MUST include it). The per-version sieve guarantees it for core methods; the envelope pass fills"complete"for everything else (custom methods, extension methods, empty results), mirroring the typescript server'sstampResultType. An empty result becomes{"resultType": "complete", "_meta": {...}}on the 2026 wire. - Stamping is always on; there is no constructor opt-out. The spec's "unless specifically configured not to do so" stays expressible at the layer that owns response envelopes: a middleware that strips the key from the results it passes along. Neither go nor ts shipped a dedicated flag either. (An earlier revision of this PR had
include_server_info=False; it was removed as redundant with the middleware mechanism.) - The stamp is a fresh dict per response (deep copy of a once-per-server dump), so mutating a stamped result can never corrupt the identity stamped into later responses.
- Claimed extension shapes bypass the result sieve on modern versions only. A
resultTypeoutside the core vocabulary marks a shape the extension owns; on a legacy session it still goes through the per-version surface and fails as INTERNAL_ERROR, since legacy clients cannot resolve claims. - The validated
paramsargument ofExtension.intercept_tool_callis authoritative: a context rewrite throughcall_nextadjusts what the handler observes onctx, not which tool call runs. Wire-level rewriting belongs toServer.middleware, above params validation. - Unversioned servers report
version: "". The v1 fallback to the installedmcppackage version reported the SDK's version as the server's own; no spec text supports that, and the ecosystem expectation (spec discussion #1176) is the server's distributed version. go serializes an empty string for an unset version; typescript and kotlin require an explicit identity. Aligned with go, with a migration note since v1 servers that never set a version change what they report. - The interaction matrix normalizes the stamp with an era-aware fixture: on a modern cell
unstamped(result)asserts the stamp is present and well-formed, then strips it so one expected payload stays valid across eras; on a handshake-era cell it asserts the result was never stamped. Unit suites carry the (now deterministic) stamp literally in their expectations instead.
Testing
- Full suite green; coverage stays at 100% (branch).
- All six conformance legs green locally against the new referee (server active/draft/2026/all, client all/2026).
- Live wire drive: pair-only and null-
clientInforequests, malformed-envelope rejections, discover shape, stamp presence/opt-out, legacy handshake untouched, and auto-mode negotiation against both an identifying and an anonymous server.