Skip to content

Align 2026-07-28 wire with spec PR #3002: serverInfo in result _meta, clientInfo optional#2513

Merged
felixweinberger merged 1 commit into
mainfrom
fweinberger/spec-3002-alignment
Jul 20, 2026
Merged

Align 2026-07-28 wire with spec PR #3002: serverInfo in result _meta, clientInfo optional#2513
felixweinberger merged 1 commit into
mainfrom
fweinberger/spec-3002-alignment

Conversation

@felixweinberger

@felixweinberger felixweinberger commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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 serverInfo when parsing a DiscoverResult, so a conforming
    server's discover response failed validation, the version-negotiation probe classified
    the server as legacy, and the client attempted an initialize handshake against it —
    a hard connect failure against a modern-only server.
  • Our server rejected requests whose _meta envelope omits clientInfo, 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
    clientInfo is optional (present-but-malformed still fails); DiscoverResult has no
    body serverInfo; result _meta is typed as the anchor's ResultMetaObject. A
    malformed _meta serverInfo 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 public ResultMetaObjectSchema
    the latter sits inside JSONRPCResultResponseSchema, 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 _meta serverInfo stamp (spec SHOULD;
    a handler-authored value wins; the 2025-era wire is untouched) — including the
    entry-built subscriptions/listen graceful-close results, whose _meta carries the
    identity next to the subscription id (SubscriptionsListenResultMeta extends
    ResultMetaObject).
  • Client: still sends clientInfo on every request (SHOULD). Reads server identity
    from the discover result's _meta only. A server that stamps no identity is
    anonymous: the connection works, getServerVersion() is undefined, and the response
    cache partitions under a per-connection surrogate so identity-less servers can never
    share cache entries.
  • Types and public schemas: DiscoverResult no longer declares serverInfo;
    RequestMetaEnvelope['io.modelcontextprotocol/clientInfo'] is optional; new public
    constant SERVER_INFO_META_KEY; new ResultMetaObjectSchema in
    @modelcontextprotocol/core (result _meta now validates against it, and
    SubscriptionsListenResultMetaSchema extends it), with the matching
    ResultMetaObject type.
  • Conformance baseline: the pinned referee (0.2.0-alpha.9) still asserts the OLD
    shape, so server-stateless is expected-failed on exactly three checks
    (sep-2575-request-meta-invalid-missing-client-info, the missing-client-info
    iteration of sep-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) — classifies modern now, misclassified
    legacy before this change. A malformed _meta serverInfo also stays modern.
  • Server e2e: discover carries identity in _meta and no body field; requests without
    clientInfo are served; every modern response is stamped; 2025-era responses never
    are.
  • Client e2e: identity resolves from _meta; a stray body serverInfo is ignored
    (anonymous connect); a persisted stale blob carrying a body serverInfo connects
    with anonymous identity; a MALFORMED _meta serverInfo 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

@felixweinberger
felixweinberger requested a review from a team as a code owner July 17, 2026 15:53
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d1040b8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@modelcontextprotocol/core-internal Minor
@modelcontextprotocol/core Minor
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/server-legacy Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major
@modelcontextprotocol/codemod Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2513

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2513

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2513

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2513

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2513

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2513

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2513

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2513

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2513

commit: d1040b8

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline doc-prose nits, two candidate issues were examined and ruled out: (1) whether Client.discover() re-probing leaves getServerVersion() and the cache partition stale — it only ever updated _discoverResult, a pre-existing behavior this PR does not change (the PR only alters how _serverVersion is derived at connect time); (2) the stampServerInfoMeta / probe-classifier interplay (malformed _meta stamp, handler-authored value, 2025 never-stamp) is pinned by the new encodeContract and probeFixtureCorpus tests and behaves as documented.

Extended reasoning...

Bugs were found (three doc/prose nits, posted inline), so per policy this is an informational note only, not a verdict. The core wire changes (envelope clientInfo optionality, DiscoverResult serverInfo relocation to _meta, the encode-seam stamp, and the transitional dual emission) are extensively test-pinned in this diff; the ruled-out items above were re-checked against packages/client/src/client/client.ts and the new test suites. This PR is large and touches the protocol wire layer with breaking public-type changes, so human maintainer review is still expected.

Comment thread packages/server/src/server/server.ts Outdated
Comment thread docs/advanced/gateway.md
Comment thread .changeset/spec-3002-servinfo-meta.md Outdated
Comment thread packages/server/src/server/server.ts
@felixweinberger
felixweinberger force-pushed the fweinberger/spec-3002-alignment branch 2 times, most recently from d2da01e to 6eb7046 Compare July 17, 2026 17:37
Comment thread packages/core/src/schemas.ts
Comment thread packages/server/src/server/listenRouter.ts

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the previously posted inline findings (which still stand — no new commits since), this run also examined the anonymous-server cache-identity path in Client._deriveServerIdentity and ruled it out: the fallback order (serverInfo → transport sessionId → per-connection surrogate) matches the documented contract, the sessionId fallback is pre-existing behavior, and partition collision-safety comes from the cache's JSON-array encoding rather than the identity value itself.

Extended reasoning...

This run's bug hunters surfaced one candidate — that the anonymous-server cache identity uses the server-controlled sessionId instead of the promised per-connection surrogate — and verification refuted it: the JSDoc on _deriveServerIdentity (packages/client/src/client/client.ts:1301-1319) explicitly documents sessionId as the intermediate fallback before the surrogate, that ordering predates this PR, and the surrogate is only needed for the sessionId-less anonymous case introduced by #3002. Not approving: this is a large breaking wire-protocol change and the prior run's finding on ResultMetaObjectSchema in packages/core/src/schemas.ts (missing .catch(undefined) receive-side leniency, affecting message classification) is still present in the current code with no commits since it was posted.

… clientInfo optional

The final 2026-07-28 revision (spec PR #3002) moved serverInfo from the
DiscoverResult body to _meta['io.modelcontextprotocol/serverInfo'] and
demoted the request envelope's clientInfo from required to SHOULD. We
shipped the pre-#3002 shape in both directions: the client hard-rejected a
conforming server's discover response (misclassifying it as legacy and
attempting initialize), and the server rejected requests without clientInfo.

This models the final revision exactly — no pre-#3002 shape support in
either direction.

- Regenerate the 2026-07-28 anchor types, schema.json twin, and example
  corpus at spec commit 71e30695.
- Wire schemas: envelope clientInfo optional (malformed still rejected);
  DiscoverResult has no body serverInfo; result _meta typed as the anchor's
  ResultMetaObject. A malformed _meta serverInfo is treated as absent on
  receive — the spec marks the field self-reported, unverified, and
  display-only, so a bad identity stamp never invalidates a result. The
  leniency lives in both the 2026 wire schema and the public
  ResultMetaObjectSchema: the latter sits inside JSONRPCResultResponseSchema,
  whose guard classifies every inbound message on both eras, so a strict key
  there would drop whole responses (probe failures, hung requests).
- Server: stamp _meta serverInfo on every 2026-era result (handler-authored
  value wins; the 2025-era wire is untouched) — including the entry-built
  subscriptions/listen graceful-close results (SubscriptionsListenResultMeta
  extends ResultMetaObject).
- Public schemas (@modelcontextprotocol/core): new ResultMetaObjectSchema;
  result _meta validates against it and SubscriptionsListenResultMetaSchema
  extends it, with the matching ResultMetaObject type.
- Client: still sends clientInfo; reads identity from _meta only. An
  identity-less server is anonymous: getServerVersion() is undefined and
  the response cache partitions under a per-connection surrogate so
  anonymous servers never share cache entries.
- Conformance: the pinned referee (0.2.0-alpha.9) still asserts the old
  shape, so server-stateless is expected-failed on exactly three checks
  until the pin bumps to a #3002-aware release.
- Tests: the exact go-sdk v1.7.0-pre.3 discover response now classifies
  modern (was legacy); new e2e requirements for the identity stamp and
  clientInfo optionality; a stale persisted blob with a body serverInfo
  connects with anonymous identity.
@felixweinberger
felixweinberger force-pushed the fweinberger/spec-3002-alignment branch from 6eb7046 to d1040b8 Compare July 20, 2026 13:14

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bugs found this pass, and all findings from my earlier review rounds are resolved in the latest revision — but this is a 61-file breaking change to the 2026-07-28 wire (schemas, codec encode seam, probe classification, public API), so it needs a human maintainer's look rather than a bot approval.

What was reviewed:

  • The previously flagged classification-guard hazard is fixed: the neutral ResultMetaObjectSchema serverInfo key now carries .catch(undefined) (packages/core/src/schemas.ts), so a malformed identity stamp can no longer drop whole responses at isJSONRPCResultResponse; regression tests pin it on both the neutral layer and the real transport connect path.
  • Checked the encode seam: stampServerInfoMeta runs only on the 2026 codec (2025 never-stamp pinned), handler-authored values win, non-object _meta is left untouched, and both entry-built subscriptions/listen close paths (HTTP + stdio) carry the stamp consistently with the schema (SubscriptionsListenResultMeta keeps subscriptionId strict, serverInfo lenient).
  • Checked the anonymous-identity cache path: the per-connection surrogate in _deriveServerIdentity prevents two identity-less servers on sessionId-less transports from sharing the pre-connect '' partition.
  • Prior doc/changeset staleness findings are resolved: the _ondiscover JSDoc now says capabilities are advertised as-is, the changeset scopes fail-on-malformed to clientInfo, and the PR description now matches the shipped no-dual-emission design and the three expected-failed conformance checks.
Extended reasoning...

Overview

This PR aligns the 2026-07-28 wire implementation with spec PR #3002: serverInfo moves from the DiscoverResult body to the result _meta key io.modelcontextprotocol/serverInfo, and the request envelope's clientInfo demotes from required to SHOULD. It touches 61 files across the spec anchor types, vendored schema twins and example corpus, the 2026 wire schemas and codec (a new stampServerInfoMeta encode step and an encodeResult signature extension threaded through Protocol._outboundServerInfo()), the neutral public schemas in @modelcontextprotocol/core (new ResultMetaObjectSchema inside ResultSchema._meta), client identity resolution and response-cache partitioning, both listen-router entries, conformance baselines, and migration docs. It is explicitly breaking (DiscoverResult loses serverInfo; RequestMetaEnvelope.clientInfo becomes optional).

Security risks

Low. serverInfo/clientInfo are self-reported display-only metadata and the diff consistently documents that neither side should use them for behavior or security decisions. The one identity-adjacent surface is response-cache partitioning: the new per-connection surrogate (anonymous:Date.now()-Math.random()) closes the hole where two identity-less servers over sessionId-less transports would have shared the pre-connect '' partition — an improvement, and the partition's collision safety comes from the cache's JSON-array encoding rather than the identity string's contents. Making clientInfo optional removes a validation requirement but only for a field servers were already told not to trust.

Level of scrutiny

High — this is production-critical protocol code. The most dangerous part of the design is the interaction between the newly typed _meta key and JSONRPCResultResponseSchema, whose guard classifies every inbound message on both eras: a strict key there silently drops whole responses (hung requests, probe misclassification to legacy). A prior review round flagged exactly that as a release blocker; the current revision fixes it with .catch(undefined) on the neutral schema and pins it with tests exercising the full transport path (discover.test.ts), the guard directly (types.test.ts), and the probe classifier (probeFixtureCorpus.test.ts, including the byte-real go-sdk v1.7.0-pre.3 fixture). I verified the fix is present in the checkout at the PR head. Even with that resolved, the breadth (public API additions, breaking type changes, cross-SDK interop claims, conformance-baseline expected-failure entries keyed to a future referee bump) is the kind of judgment call a maintainer should own.

Other factors

Test coverage is thorough and adversarial: leniency vs strictness is pinned on both layers (malformed clientInfo still rejects; malformed serverInfo drops to absent; subscriptionId stays strict), the 2025 never-stamp guarantee is pinned, and stale-blob/body-serverInfo paths are covered. All findings from my earlier rounds — the 🔴 classification-guard bug, the _ondiscover JSDoc contradiction, the changeset over-generalization, and the stale PR-description claims (dual emission, body fallback, two-vs-three conformance checks, the 'unstamped exception' clause) — are resolved in the latest commit; the description, changeset, migration doc, and code now tell one consistent story. The remaining open question for a human is the design ruling itself (no pre-#3002 compatibility shape in either direction, which hard-breaks pre-#3002 peers), which the PR states was decided deliberately.

@felixweinberger
felixweinberger merged commit f413763 into main Jul 20, 2026
20 checks passed
@felixweinberger
felixweinberger deleted the fweinberger/spec-3002-alignment branch July 20, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant