Align 2026-07-28 wire with spec PR #3002: serverInfo in result _meta, clientInfo optional
Spec PR modelcontextprotocol/modelcontextprotocol#3002 (part of the final 2026-07-28
revision) moved serverInfo from the DiscoverResult body to the result-_meta key
io.modelcontextprotocol/serverInfo, and demoted the request envelope's clientInfo
from required to SHOULD. This PR updates the 2026-07-28 wire implementation to match —
the final revision is modeled exactly, with no pre-#3002 shape support in either
direction.
Motivation and Context
We shipped the pre-#3002 shape in both directions, which breaks interop with SDKs that
follow the final revision (go-sdk v1.7.0-pre.3+, csharp):
- Our client required body
serverInfowhen parsing aDiscoverResult, so a conforming
server's discover response failed validation, the version-negotiation probe classified
the server as legacy, and the client attempted aninitializehandshake against it —
a hard connect failure against a modern-only server. - Our server rejected requests whose
_metaenvelope omitsclientInfo, which the final
revision explicitly allows.
What changed
- Spec artifacts regenerated at spec commit
71e30695(the #3002 merge): the
2026-07-28 anchor types, the vendored schema.json twin, and the example corpus. - Wire schemas (2026-07-28): the final revision exactly — the envelope's
clientInfois optional (present-but-malformed still fails);DiscoverResulthas no
bodyserverInfo; result_metais typed as the anchor'sResultMetaObject. A
malformed_metaserverInfo value is treated as absent on receive: the spec marks the
field self-reported, unverified, and display-only ("clients SHOULD NOT use it to
change their behavior"), so a bad identity stamp never invalidates a result. The
leniency lives in both the 2026 wire schema and the publicResultMetaObjectSchema—
the latter sits insideJSONRPCResultResponseSchema, whose guard classifies every
inbound message on both eras, so a strict key there would drop whole responses. - Server: every 2026-era response gets the
_metaserverInfo stamp (spec SHOULD;
a handler-authored value wins; the 2025-era wire is untouched) — including the
entry-builtsubscriptions/listengraceful-close results, whose_metacarries the
identity next to the subscription id (SubscriptionsListenResultMetaextends
ResultMetaObject). - Client: still sends
clientInfoon every request (SHOULD). Reads server identity
from the discover result's_metaonly. A server that stamps no identity is
anonymous: the connection works,getServerVersion()isundefined, and the response
cache partitions under a per-connection surrogate so identity-less servers can never
share cache entries. - Types and public schemas:
DiscoverResultno longer declaresserverInfo;
RequestMetaEnvelope['io.modelcontextprotocol/clientInfo']is optional; new public
constantSERVER_INFO_META_KEY; newResultMetaObjectSchemain
@modelcontextprotocol/core(result_metanow validates against it, and
SubscriptionsListenResultMetaSchemaextends it), with the matching
ResultMetaObjecttype. - Conformance baseline: the pinned referee (
0.2.0-alpha.9) still asserts the OLD
shape, soserver-statelessis expected-failed on exactly three checks
(sep-2575-request-meta-invalid-missing-client-info, the missing-client-info
iteration ofsep-2575-http-server-meta-invalid-400, and
sep-2575-server-implements-discover, which requires body serverInfo). The entries
burn when the pin bumps to a referee release that incorporates #3002.
How Has This Been Tested?
- Regression fixture: the exact discover response a go-sdk v1.7.0-pre.3 server sends
(serverInfo in_meta, no body field) — classifiesmodernnow, misclassified
legacybefore this change. A malformed_metaserverInfo also staysmodern. - Server e2e: discover carries identity in
_metaand no body field; requests without
clientInfoare served; every modern response is stamped; 2025-era responses never
are. - Client e2e: identity resolves from
_meta; a stray bodyserverInfois ignored
(anonymous connect); a persisted stale blob carrying a bodyserverInfoconnects
with anonymous identity; a MALFORMED_metaserverInfo still connects modern through
the real transport path and a post-connect call carrying one still resolves (the
neutral-layer classification guard must not drop such frames — pinned on both eras). - Full suite: typecheck / lint / unit + integration + e2e / conformance (all six CI
legs) / docs:check / docs:examples — all green.
Breaking Changes
DiscoverResult no longer declares serverInfo, and RequestMetaEnvelope's
clientInfo is optional. Code that read discover.serverInfo moves to
client.getServerVersion() (or the _meta key directly). See
docs/migration/support-2026-07-28.md.
Types of changes
- Bug fix (non-breaking change which fixes an issue)
- Breaking change (fix or feature that would cause existing functionality to change)
- Documentation update
Checklist
- I have read the MCP Documentation
- My code follows the repository's style guidelines
- New and existing tests pass locally
- I have added appropriate error handling
- I have added or updated documentation as needed