Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions packages/core-internal/src/types/spec.types.2026-07-28.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Source: https://github.com/modelcontextprotocol/modelcontextprotocol
* Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/draft/schema.ts
* Last updated from commit: f68d864a813754e188c6df52dcc5772a12f96c63
* Last updated from commit: 9a4ff8af92ba00cbddbf94672dfade9279987e66

Check warning on line 6 in packages/core-internal/src/types/spec.types.2026-07-28.ts

View check run for this annotation

Claude / Claude Code Review

Verified: fixtures corpus is content-current at 9a4ff8af — only its manifest provenance is stale, narrowing the rule-4 refresh to the schema twin plus a provenance-only fixtures bump

Verified: the fixtures corpus at test/corpus/fixtures/2026-07-28/ is content-current at 9a4ff8af — all 128 vendored files are byte-identical to upstream — so only its manifest's source.commit (f68d864a) is stale. This closes the item the 2026-07-14 status update left unverified and narrows the mandatory rule-4 refresh: the schema twin is the only content-level item; the fixtures need at most a provenance-only manifest bump ('pnpm fetch:spec-examples 9a4ff8af92ba00cbddbf94672dfade9279987e66' shou

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Unlike the prior two re-pulls (2ffc3fa2, 93671a3f), which were verified content-noops, this re-pull to 9a4ff8af changes the JSDoc body of SubscriptionsAcknowledgedNotification — and upstream JSDoc becomes the description fields of the generated schema.json, so the vendored 2026-07-28 schema twin (still pinned at f68d864a, sha256 14398c3d…/178,613 bytes, vs upstream 670dda8b…/179,002 bytes at 9a4ff8af) is now genuinely stale in content, not just provenance — and nothing in CI catches it. This invalidates the standing close-as-no-op guidance and the pin SHAs in prior comments: any twin/fixtures refresh landed with this PR must run pnpm fetch:schema-twins 9a4ff8af92ba00cbddbf94672dfade9279987e66 (2026-07-28 entry only, per the per-revision-pinning caveat), mindful that the nightly force-push wipes extra branch commits.

Extended reasoning...

What changed

Every prior re-pull on this PR (f68d864a2ffc3fa293671a3f) was verified to be a content-noop: upstream schema/draft/schema.ts and schema.json were byte-identical (or formatting-only), so the vendored conformance oracle stayed content-current and the accepted rule-4 finding (#3485357818) was a provenance-only skew. That is the premise behind the standing guidance in #3487174588 and the 2026-07-05 SHA-correction: "close as no-op or land with a manifest refresh."

This re-pull to 9a4ff8af92ba00cbddbf94672dfade9279987e66 is different — it is content-bearing. The diff rewrites the JSDoc body of SubscriptionsAcknowledgedNotification (spec.types.2026-07-28.ts:1335-1345), adding new normative MUST clauses ("This notification MUST be the first message the server sends carrying the subscription's ID… The server MUST NOT send any notification on the subscription before acknowledging it…"). Upstream generates schema.json from schema.ts, and JSDoc becomes the description fields of the generated schema — so the twin the SDK vendors as its conformance oracle changed too.

Step-by-step proof

  1. Upstream schema/draft/schema.json at 9a4ff8af is 179,002 bytes, sha256 670dda8b3f9bd07888f8e7ef02d04357af7e24a4bb0dbbda9fc8d268fb03f0b1 (verified live by multiple independent fetches; the new "first message the server sends carrying" prose appears in its description fields, and a structural diff shows the sole changed $defs entry is SubscriptionsAcknowledgedNotification's description — exactly mirroring this diff's JSDoc change).
  2. packages/core-internal/test/corpus/schema-twins/manifest.json still pins the 2026-07-28 twin at 178,613 bytes, sha256 14398c3d…ba5c4c83, source.commit: f68d864a — and test/corpus/fixtures/2026-07-28/manifest.json also still records f68d864a.
  3. So for the first time on this PR, the rule-4 anchor↔twin divergence is a genuine content mismatch: the vendored oracle no longer matches upstream at the commit the anchor header records.
  4. Nothing in CI catches it: the provenance-integrity test in schemaTwinConformance.test.ts (~lines 40-56) only hashes the vendored twin bytes against the manifest's own sha256/bytes entries — both unchanged in this PR, so it stays green — and never cross-checks upstream or the anchor header's commit. A description-only delta also cannot affect Ajv fixture validation, and the type drift guards compare types, not JSDoc. The oracle drifts with zero red.

Why this invalidates the standing guidance

  • Close-as-no-op is dead. #3487174588 and the 2026-07-05 correction offered that option explicitly premised on "schema.json hash unchanged." That premise no longer holds: closing this PR leaves the twin stale relative to the spec content the anchor is supposed to mirror, and merging it as-is lands main in the state OPERATIVE lifecycle rule 4 forbids — now substantively, not just in provenance metadata.
  • The pin SHAs in prior comments are wrong. #3485357818 says pin 2ffc3fa2; the 2026-07-05 correction says pin 93671a3f. Following either recreates a three-way skew. The correct pin is 9a4ff8af92ba00cbddbf94672dfade9279987e66 (or whatever the anchor records at merge time).

How to fix

Land the twin/fixtures refresh with this PR: pnpm fetch:schema-twins 9a4ff8af92ba00cbddbf94672dfade9279987e662026-07-28 entry only, per the per-revision-pinning caveat already on record (the script as written applies one SHA to every twin and would re-stamp the frozen 2025-11-25 released-revision oracle from the floating draft commit, contrary to RELEASED_REVISION_PINS). Also re-verify whether schema/draft/examples changed at 9a4ff8af for the fixtures manifest. And be mindful of the already-documented workflow behavior: the nightly cron force-recreates this branch and wipes any extra commit, so the refresh must be merged promptly or with the cron paused.

Why normal severity

The prior rule-4 finding was accepted at blocking severity for a provenance-only skew; this is the substantive version of the same violation — merging (or closing) without the refresh leaves main with a genuinely stale conformance oracle and lying manifests, with zero CI signal, and the standing remediation instructions on this PR now point at the wrong SHAs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Verified: the fixtures corpus at test/corpus/fixtures/2026-07-28/ is content-current at 9a4ff8af — all 128 vendored files are byte-identical to upstream — so only its manifest's source.commit (f68d864a) is stale. This closes the item the 2026-07-14 status update left unverified and narrows the mandatory rule-4 refresh: the schema twin is the only content-level item; the fixtures need at most a provenance-only manifest bump ('pnpm fetch:spec-examples 9a4ff8af92ba00cbddbf94672dfade9279987e66' should produce a manifest-only diff, leaving directoryCount 87 / fileCount 128 and the specCorpus.test.ts pins unchanged — anything else is itself a signal to investigate).

Extended reasoning...

What this closes

The 2026-07-14 status update on this PR (the one that established the schema twin is genuinely content-stale at 9a4ff8af — upstream schema/draft/schema.json is 179,002 bytes / sha256 670dda8b… vs the vendored twin's 178,613 / 14398c3d… pinned at f68d864a) explicitly left one remediation item open: "Whether schema/draft/examples changed between f68d864a and 9a4ff8af should be verified as part of that refresh (the compare API was unavailable in this session)." This finding closes that item with a live byte-level verification, and the answer materially narrows the required rule-4 refresh.

Verification performed (all against the exact commit this diff records)

1. Content-currency of every vendored fixture. All 128 files enumerated by packages/core-internal/test/corpus/fixtures/2026-07-28/manifest.json were fetched from raw.githubusercontent.com at 9a4ff8af92ba00cbddbf94672dfade9279987e66 (path schema/draft/examples/<dir>/<file>) and compared byte-for-byte against the vendored copies (which were vendored at f68d864a). Result: 128 identical, 0 mismatches, 0 missing (every file returned HTTP 200). Multiple independent verifiers reproduced the same result.

2. Added-file closure via the anchor. Raw hosting has no directory listing, so the added-file question is closed through the anchor itself: the post-sync spec.types.2026-07-28.ts contains exactly 125 {@includeCode ./examples/...} references, and every one resolves to a file already present in the vendored corpus (0 missing). Since the anchor at 9a4ff8af is the source of truth for the canonical example set and this diff adds no new @includeCode references, there is no evidence of upstream example additions between f68d864a and 9a4ff8af. (Caveat, acknowledged: this cannot detect an upstream file the anchor never references — which is exactly why the refresh command's output should be inspected, per the consequence below.)

3. The example this very hunk references is already consistent with the new prose. SubscriptionsAcknowledgedNotification/listen-acknowledged.json — the file the rewritten JSDoc's @example points at — is byte-identical to upstream at 9a4ff8af and already carries the _meta['io.modelcontextprotocol/subscriptionId'] key the new MUST clauses require, so the canonical example needs no update.

Step-by-step consequence for the mandatory rule-4 refresh

  1. test/corpus/fixtures/2026-07-28/manifest.json records source.commit: f68d864a with directoryCount: 87 / fileCount: 128 — stale provenance vs the anchor header's 9a4ff8af, but current bytes (step 1 above).
  2. Therefore running pnpm fetch:spec-examples 9a4ff8af92ba00cbddbf94672dfade9279987e66 should produce a manifest-only diff (the source.commit line), leaving all 128 files unchanged and the counts at 87/128.
  3. If the command produces anything beyond that one manifest line, that is itself a signal to investigate — it would contradict this verification.
  4. The specCorpus.test.ts inventory pins (87 directories / 128 files) therefore remain correct and need no update.
  5. The schema twin remains the only content-level item in the atomic refresh: pnpm fetch:schema-twins 9a4ff8af… (2026-07-28 entry only, per the per-revision-pinning caveat already on record) genuinely changes the vendored twin's bytes; the fixtures half of the refresh is provenance-only.

Why this matters

The maintainer performing the mandatory pre-merge refresh is working from a checklist whose last entry says the examples delta is unknown. Recording the verified answer prevents both over-scoping (re-vendoring and re-reviewing 128 fixtures for a content change that does not exist) and the residual doubt that would otherwise justify deferring the refresh again while the nightly force-push keeps rewriting this branch. It also gives the refresh a concrete expected outcome (manifest-only diff) that doubles as a self-check.

Severity

Nit: there is no code defect and nothing breaks without this — it is a verified status/scoping correction in the same tier as the content-noop verifications and SHA corrections already accepted on this PR. It does not block merge on its own; it refines the already-blocking rule-4 refresh so it is executed at the right scope.

*
* DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates.
* To update this file, run: pnpm run fetch:spec-types 2026-07-28
Expand Down Expand Up @@ -1333,10 +1333,16 @@
}

/**
* Sent by the server as the first message on a
* {@link SubscriptionsListenRequest | subscriptions/listen} stream to acknowledge
* that the subscription has been established and to report which notification
* types it agreed to honor.
* Sent by the server to acknowledge that a
* {@link SubscriptionsListenRequest | subscriptions/listen} subscription has been
* established and to report which notification types it agreed to honor.
*
* This notification MUST be the first message the server sends carrying the
* subscription's ID in `io.modelcontextprotocol/subscriptionId`. The server MUST
* NOT send any notification on the subscription before acknowledging it. On
* stdio, where every subscription shares one channel, this ordering is defined

Check warning on line 1343 in packages/core-internal/src/types/spec.types.2026-07-28.ts

View check run for this annotation

Claude / Claude Code Review

SDK JSDoc for SubscriptionsAcknowledgedNotificationSchema and listenRouter spec citation still carry the superseded first-message-on-stream wording

The SDK's published JSDoc for `SubscriptionsAcknowledgedNotificationSchema` (packages/core/src/schemas.ts:952-955) and the spec-citation header in packages/server/src/server/listenRouter.ts:14-16 both still use the "first message on a `subscriptions/listen` stream" wording that this sync deliberately replaces with per-subscription-ID ordering (on stdio the ack need only be the first message carrying that subscription's ID — other subscriptions' messages may be interleaved before it). No runtime
Comment on lines +1336 to +1343

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The SDK's published JSDoc for SubscriptionsAcknowledgedNotificationSchema (packages/core/src/schemas.ts:952-955) and the spec-citation header in packages/server/src/server/listenRouter.ts:14-16 both still use the "first message on a subscriptions/listen stream" wording that this sync deliberately replaces with per-subscription-ID ordering (on stdio the ack need only be the first message carrying that subscription's ID — other subscriptions' messages may be interleaved before it). No runtime change is needed (both routers already satisfy the new, weaker per-channel requirement); just refresh the two doc comments so consumers of @modelcontextprotocol/core don't implement per-channel ordering assumptions the 2026-07-28 spec now explicitly disclaims.

Extended reasoning...

What the bug is

This sync's only content change rewrites the spec JSDoc for SubscriptionsAcknowledgedNotification (spec.types.2026-07-28.ts:1335-1345): the old framing — "Sent by the server as the first message on a subscriptions/listen stream…" — is replaced with per-subscription-ID ordering semantics: "This notification MUST be the first message the server sends carrying the subscription's ID in io.modelcontextprotocol/subscriptionId… On stdio, where every subscription shares one channel, this ordering is defined per subscription ID and not per channel: messages belonging to other subscriptions MAY be interleaved before it."

Two SDK doc surfaces copied the superseded wording and become stale the moment this sync lands:

  1. packages/core/src/schemas.ts:952-955 — the public SubscriptionsAcknowledgedNotificationSchema is documented as "Sent by the server as the first message on a subscriptions/listen stream to acknowledge that the subscription has been established and report which notification types it agreed to honor (protocol revision 2026-07-28)." It explicitly cites protocol revision 2026-07-28, yet after this sync it describes semantics that revision deliberately re-framed.
  2. packages/server/src/server/listenRouter.ts:14-16 — the file header cites "Per the spec at protocol revision 2026-07-28: — The acknowledged notification is the FIRST message on the stream…". For the HTTP per-stream router this remains an accurate implementation property (per-stream ordering trivially implies per-subscription ordering), but as a spec citation it now paraphrases superseded text — and StdioListenRouter (same file, line 274) shares this header and is exactly the case the new wording re-defines.

Why it matters

packages/core is the published Zod-schema package, so the schemas.ts JSDoc is consumer-facing: a client author reading the hover doc for SubscriptionsAcknowledgedNotificationSchema could reasonably implement a per-channel assertion ("the first notification I receive on the stdio channel after subscriptions/listen must be the ack") — an assumption the spec now explicitly disclaims. Against a spec-compliant server multiplexing several subscriptions over one stdio channel, that client would spuriously reject valid interleavings.

Step-by-step proof

  1. Pre-sync spec text (and the SDK JSDoc copied from it): the ack is "the first message on a subscriptions/listen stream" — on stdio, where there is no dedicated stream, the natural reading is first-on-channel.
  2. Post-sync spec text (this diff, spec.types.2026-07-28.ts:1336-1343): the ack MUST be the first message carrying that subscription's ID; "messages belonging to other subscriptions MAY be interleaved before it."
  3. A client opens subscription A, then subscription B, over stdio. The server sends A's resources/updated notification (stamped with A's subscription ID), then B's ack. This is spec-valid post-sync.
  4. A client built to the published schemas.ts JSDoc ("first message on the stream") treats B's ack as out-of-order because a non-ack message arrived on the channel first — rejecting a compliant server.
  5. The SDK's own implementations are fine: createListenRouter enqueues the ack before subscribing to the bus, and StdioListenRouter.serve() sends the ack synchronously — both satisfy the stronger per-stream/per-subscription property. Only the prose is wrong.

Why nothing catches it

Neither packages/core/src/schemas.ts nor packages/server/src/server/listenRouter.ts imports the spec snapshot, and JSDoc prose is invisible to the drift-guard type checks — so there is no CI signal. This matches the repo's recurring documentation-catch pattern (prose contradicting a spec change landed in the same diff).

How to fix

Companion doc-only edit: update the SubscriptionsAcknowledgedNotificationSchema JSDoc (schemas.ts:952-955) to the new per-subscription-ID wording (first message carrying the subscription's ID; per-subscription, not per-channel, ordering on stdio), and refresh the spec-citation bullet in listenRouter.ts:14-16 to match. No runtime change is needed.

* per subscription ID and not per channel: messages belonging to other
* subscriptions MAY be interleaved before it.
*
* @example Listen acknowledged
* {@includeCode ./examples/SubscriptionsAcknowledgedNotification/listen-acknowledged.json}

Check warning on line 1348 in packages/core-internal/src/types/spec.types.2026-07-28.ts

View check run for this annotation

Claude / Claude Code Review

New ack-ordering MUST prose is internally inconsistent: subscriptionId type-optional on the ack, and pre-ack SubscriptionsListenResult graceful close violates the first-message rule

Upstream spec-internal inconsistency introduced by this exact hunk's new MUST prose, to batch with the other schema.ts feedback: (1) the ack is required by prose to carry the subscription ID in `io.modelcontextprotocol/subscriptionId`, but the types leave it optional at both levels (SubscriptionsAcknowledgedNotificationParams → optional `_meta`, NotificationMetaObject → optional `subscriptionId` key), so an ID-less ack is type-valid yet uncorrelatable on a shared stdio channel; and (2) the MUST
Comment on lines +1336 to 1348

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Upstream spec-internal inconsistency introduced by this exact hunk's new MUST prose, to batch with the other schema.ts feedback: (1) the ack is required by prose to carry the subscription ID in io.modelcontextprotocol/subscriptionId, but the types leave it optional at both levels (SubscriptionsAcknowledgedNotificationParams → optional _meta, NotificationMetaObject → optional subscriptionId key), so an ID-less ack is type-valid yet uncorrelatable on a shared stdio channel; and (2) the MUST says first message (not notification), yet SubscriptionsListenResult — whose _meta.subscriptionId is REQUIRED — may be sent as a pre-ack graceful close per its own JSDoc, making the result the first ID-carrying message and violating the MUST as worded, a reading the shipped client (client.ts:2118-2148 _onresponse demux) already treats as legal. Suggest upstream require the ID on the ack's meta (mirroring SubscriptionsListenResultMeta) and either scope the ordering MUST to notifications ('first notification') or define pre-ack termination explicitly, before the companion port encodes either guess into conformance assertions.

Extended reasoning...

What the new prose says

This re-pull rewrites the SubscriptionsAcknowledgedNotification JSDoc (spec.types.2026-07-28.ts:1340-1345) from positional stream framing ('first message on a subscriptions/listen stream') to an ID-carrying, per-subscription definition: 'This notification MUST be the first message the server sends carrying the subscription's ID in io.modelcontextprotocol/subscriptionId. The server MUST NOT send any notification on the subscription before acknowledging it. On stdio, where every subscription shares one channel, this ordering is defined per subscription ID and not per channel.' The new MUST is internally inconsistent with the surrounding types in two independent ways.

Inconsistency 1 — the ack is not type-required to carry the ID it is defined by

The MUST presupposes the ack carries the subscription ID — that is the only thing that makes the per-subscription-ID ordering on stdio well-defined at all. But SubscriptionsAcknowledgedNotificationParams (line 1325) extends NotificationParams, whose _meta is optional (line 173), and NotificationMetaObject's 'io.modelcontextprotocol/subscriptionId' key is itself optional (line 133). So { method: 'notifications/subscriptions/acknowledged', params: { notifications: {} } } with no _meta at all is type-valid yet cannot satisfy the MUST — and a client with multiple in-flight listen requests (which share one channel on stdio, per this very JSDoc) has nothing to correlate an ID-less ack against. The generic optionality of NotificationMetaObject.subscriptionId is justified for notifications not delivered on a subscription stream (lines 126-128), but the ack is only ever delivered on one, so on this specific type the key is prose-required/type-optional. The in-file precedent for the fix is right below: SubscriptionsListenResultMeta (lines 1293-1302) declares the same key REQUIRED for the graceful-close result of the same stream ('mirroring the same key on the stream's notifications'). The ack params could reference a dedicated required-key meta interface the same way — this is a plainly expressible constraint, not an inexpressible prose-only one.

Inconsistency 2 — pre-ack graceful close makes the result the first ID-carrying message

The MUST says first message, not notification — and the word choice is not accidental redundancy, because the very next sentence covers notifications separately ('MUST NOT send any notification on the subscription before acknowledging it'). So the first sentence's broader scope sweeps in JSON-RPC responses. But the spec REQUIRES the subscription ID on exactly one other message: SubscriptionsListenResult._meta is required and its SubscriptionsListenResultMeta.'io.modelcontextprotocol/subscriptionId' is required (lines 1293-1318), and the result's JSDoc (lines 1304-1309, unchanged by this diff) allows it whenever 'the server tears the subscription down' — e.g. during server shutdown — with no precondition that an ack was ever sent. A server that receives subscriptions/listen and tears the subscription down before serving it (shutdown between receipt and ack) sends the result as the first — and only — message carrying that subscription's ID, violating the new MUST as literally worded.

Step-by-step proof

  1. Client sends subscriptions/listen (id 7). Server begins shutdown before serving the subscription.
  2. Per the SubscriptionsListenResult JSDoc, the server's graceful-close signal is { id: 7, result: { resultType: 'complete', _meta: { 'io.modelcontextprotocol/subscriptionId': 7 } } } — the _meta and its subscriptionId key are type-required on this result.
  3. That response is now the first message the server has sent carrying subscription 7's ID under the named meta key — but per the new MUST, that first message must be the acknowledged notification. The server's only alternatives are (a) an error response for a non-error condition, or (b) sending an ack it never intends to honor followed immediately by the result — two messages for a dead subscription. Neither is stated anywhere.
  4. Separately, a different server sends the ack with no _meta: { method: 'notifications/subscriptions/acknowledged', params: { notifications: { toolsListChanged: true } } }. This is type-valid against the spec interfaces, yet it carries no subscription ID — so it cannot be the 'first message carrying the subscription's ID', and a client with two in-flight listens on stdio cannot tell which subscription was just acknowledged.

Why this is concrete, not hypothetical

The shipped SDK client already encodes the lenient reading of inconsistency 2: the _onresponse demux at packages/client/src/client/client.ts:2118-2148 explicitly treats a JSON-RPC result for the listen id received while still 'opening' as a graceful close, with the in-code comment 'a server that answers before the ack is shutting down before serving'. The SDK's own servers can never produce the pre-ack sequence (the HTTP listen router enqueues the ack as the first frame before the teardown is registered, listenRouter.ts:189-213; the stdio router registers and acks synchronously, listenRouter.ts:317-337), so SDK-to-SDK never hits the ambiguity — but against foreign 2026-07-28 servers the client's shipped behavior and the new prose now disagree about what is compliant. And on inconsistency 1, the published validator surface already mirrors the gap: SubscriptionsAcknowledgedNotificationParamsSchema (packages/core/src/schemas.ts:945-960) is built on the generic optional-_meta params schema while SubscriptionsListenResultMetaSchema right below it requires the key — so a literal port validates acks that violate the MUST.

How to fix

No change to this automated sync itself (the file is DO-NOT-EDIT). Batch with the upstream schema.ts feedback already collected on this PR: (a) give the ack params a dedicated required-key meta interface (the SubscriptionsListenResultMeta pattern) so the ack is type-required to carry the ID the prose defines it by; and (b) either reword the first sentence to 'the first notification the server sends carrying…' — scoping it away from responses and ratifying the SDK client's current pre-ack-result handling — or state the pre-ack-termination rule explicitly in the SubscriptionsListenResult/ack JSDoc. Whichever way upstream resolves (b), the client's pre-ack-result graceful-close path (client.ts:2127-2144) and any future conformance assertions must encode the same answer. Not a duplicate of #3433435417 (generic NotificationMetaObject schema gap / stamp-on-delivery) or the 2026-06-24 comment (result-vs-cancelled composition on stdio, which predates this MUST); the definitional MUST prose only lands in this re-pull.

Expand Down