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
61 changes: 47 additions & 14 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: 71e306956a4959c9655e5036be215d41986596e6
*
* 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 @@ -81,13 +81,20 @@
* {@link UnsupportedProtocolVersionError}.
*/
'io.modelcontextprotocol/protocolVersion': string;
/**
* Identifies the client software making the request. Required.
* Identifies the client software making the request. Clients SHOULD
* include this field on every request unless specifically configured not
* to do so.
*
* The {@link Implementation} schema requires `name` and `version`; other
* fields are optional.
*
* The value is self-reported by the client and is not verified by the
* protocol. It is intended for display, logging, and debugging. Servers
* SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for
* security decisions.
*/
'io.modelcontextprotocol/clientInfo': Implementation;
'io.modelcontextprotocol/clientInfo'?: Implementation;

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

View check run for this annotation

Claude / Claude Code Review

Spec makes request _meta clientInfo optional but SDK 2026 wire envelope still requires it (typecheck red, spec-valid requests rejected)

This spec sync makes `_meta['io.modelcontextprotocol/clientInfo']` optional on requests ("Clients SHOULD include this field... unless specifically configured not to do so"), but the SDK's 2026-07-28 wire layer still hard-requires it — `RequestMetaEnvelopeSchema` (`packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts:670`) and `REQUIRED_ENVELOPE_KEYS` (`codec.ts:60`) — so `checkInboundEnvelope` rejects every request from a spec-compliant client configured not to send clientInfo. As filed
Comment on lines 84 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 This spec sync makes _meta['io.modelcontextprotocol/clientInfo'] optional on requests ("Clients SHOULD include this field... unless specifically configured not to do so"), but the SDK's 2026-07-28 wire layer still hard-requires it — RequestMetaEnvelopeSchema (packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts:670) and REQUIRED_ENVELOPE_KEYS (codec.ts:60) — so checkInboundEnvelope rejects every request from a spec-compliant client configured not to send clientInfo. As filed the PR also breaks pnpm typecheck: the bidirectional drift-guard at test/spec.types.2026-07-28.test.ts:179 fails with TS2322 because the SDK's RequestMetaEnvelope type still requires the key. A companion SDK update (optional key in the envelope schema/type, drop it from REQUIRED_ENVELOPE_KEYS) is needed before merge.

Extended reasoning...

What the bug is

This upstream sync changes RequestMetaObject['io.modelcontextprotocol/clientInfo'] from required to optional (this diff, spec.types.2026-07-28.ts:84-97), with new prose explicitly permitting clients to omit it: "Clients SHOULD include this field on every request unless specifically configured not to do so." The PR updates only the spec anchor file — no companion change to the SDK's 2026-07-28 wire layer, which still hard-requires the key in three places:

  1. packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts:670RequestMetaEnvelopeSchema declares [CLIENT_INFO_META_KEY]: ImplementationSchema (non-optional) inside the z.looseObject at line 657.
  2. packages/core-internal/src/wire/rev2026-07-28/codec.ts:60REQUIRED_ENVELOPE_KEYS lists CLIENT_INFO_META_KEY; checkInboundEnvelope (codec.ts:266-278) safeParses every inbound request envelope against RequestMetaEnvelopeSchema and returns an "Invalid _meta envelope for protocol revision 2026-07-28" error naming clientInfo as required.
  3. packages/core-internal/src/types/types.ts:247-252 — the SDK's neutral RequestMetaEnvelope type also still requires the key.

Failure 1 — CI goes red: the drift guard no longer compiles

The drift-guard test at packages/core-internal/test/spec.types.2026-07-28.test.ts:178-181 does a bidirectional assignment between the SDK envelope type and the spec type:

(sdk: SDKTypes.RequestMetaEnvelope, spec: SpecTypes.RequestMetaObject) => {
  sdk = spec;   // TS2322 after this PR
  spec = sdk;
};

With spec clientInfo now Implementation | undefined and the SDK envelope still requiring Implementation, sdk = spec fails. This was empirically reproduced by three independent verifiers: tsc --noEmit on packages/core-internal at the PR head exits non-zero with TS2322 at test/spec.types.2026-07-28.test.ts(179,9) ("Type 'undefined' is not assignable to type '{ name: string; version: string; ... }'"), plus ~10 cascading errors for every request type embedding the envelope (tools/call, prompts/get, resources/read, subscriptions/listen, etc., around lines 548 and 623-647). So merging as filed breaks pnpm typecheck — and per this PR's own review history, CI never runs on the bot's GITHUB_TOKEN-pushed refresh branch, so the break would land on main silently.

Failure 2 — spec-valid requests rejected at runtime

Step-by-step:

  1. A 2026-07-28 client (another SDK, or a privacy-conscious deployment) is "specifically configured not to" send clientInfo — explicitly allowed by the new SHOULD prose this PR pulls in.
  2. It sends tools/call with _meta carrying protocolVersion and clientCapabilities but no io.modelcontextprotocol/clientInfo.
  3. The SDK server's checkInboundEnvelope (codec.ts:266-278) runs RequestMetaEnvelopeSchema.safeParse on the envelope; the non-optional [CLIENT_INFO_META_KEY] at buildSchemas.ts:670 makes the parse fail.
  4. The server returns the "Invalid _meta envelope" error. Every request from that client is rejected, on every method, over both stdio and Streamable HTTP.

This is exactly the repository's Schema Compliance recurring catch (REVIEW.md: over-strict Zod "rejects spec-valid payloads from other SDKs. Also confirm the spec.types.*.test.ts comparisons still pass bidirectionally" — #1768, #1849, #1169). Both prongs of that catch fire here.

Why nothing prevents it

The nightly cron regenerates only the anchor file; nothing in the automation touches the wire schemas, codec, or SDK types, and the drift-guard test that exists precisely to catch this divergence catches it only as a compile error — which never runs on this bot-pushed branch. The requiredness semantics changed upstream, and the SDK's companion update is genuine manual work this PR lacks.

How to fix

Companion SDK change in the same merge:

  • Make [CLIENT_INFO_META_KEY] optional in RequestMetaEnvelopeSchema (buildSchemas.ts:670) and in the RequestMetaEnvelope type (types.ts:247-252).
  • Drop CLIENT_INFO_META_KEY from REQUIRED_ENVELOPE_KEYS (codec.ts:60) and adjust any error-message text that names it.
  • Keep the SDK client sending clientInfo by default (codec.ts:127 outbound path is fine as-is — SHOULD-include remains the default behavior); only inbound validation and the type need loosening.
  • Re-run pnpm typecheck to confirm the bidirectional drift guard is green again.

Note: the separate serverInfo/ResultMetaObject addition and the DiscoverResult.serverInfo removal in this same sync are distinct findings with their own companion work — this comment covers only the request-side clientInfo requiredness.

/**
* The client's capabilities for this specific request. Required.
*
Expand Down Expand Up @@ -133,6 +140,30 @@
'io.modelcontextprotocol/subscriptionId'?: RequestId;
}

/**
* Extends {@link MetaObject} with additional result-specific fields. All key naming rules from `MetaObject` apply.
*
* @see {@link MetaObject} for key naming rules and reserved prefixes.
* @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details.
* @category Common Types
*/
export interface ResultMetaObject extends MetaObject {
/**
* Identifies the server software producing the response. Servers SHOULD
* include this field on every response unless specifically configured not
* to do so.
*
* The {@link Implementation} schema requires `name` and `version`; other
* fields are optional.
*
* The value is self-reported by the server and is not verified by the
* protocol. It is intended for display, logging, and debugging. Clients
* SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for
* security decisions.
*/
'io.modelcontextprotocol/serverInfo'?: Implementation;
}

/**
* A progress token, used to associate progress notifications with the original request.
*
Expand Down Expand Up @@ -197,7 +228,7 @@
* @category Common Types
*/
export interface Result {
_meta?: MetaObject;
_meta?: ResultMetaObject;
/**
* Indicates the type of the result, which allows the client to determine
* how to parse the result object.
Expand Down Expand Up @@ -647,16 +678,12 @@
*/
supportedVersions: string[];
/**
* The capabilities of the server.
*/
capabilities: ServerCapabilities;
/**
* Information about the server software implementation.
*/
serverInfo: Implementation;
/**
* Natural-language guidance describing the server and its features.
*

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

View check run for this annotation

Claude / Claude Code Review

serverInfo moved from DiscoverResult body to new optional ResultMetaObject _meta key — SDK still requires/emits the body field; drift-guard typecheck and inventory tests fail

This re-pull relocates `serverInfo` from the `DiscoverResult` body (required field deleted) to a new optional `ResultMetaObject['io.modelcontextprotocol/serverInfo']` `_meta` key, but ships no companion SDK/test update — so as filed both `pnpm typecheck` (TS2741 at `test/spec.types.2026-07-28.test.ts:544`) and the drift-guard vitest run (export inventory 154 vs pinned 153 at line 870; `ResultMetaObject` missing from the coverage map at line 888) are deterministically red. Beyond CI, the SDK stay
Comment on lines 681 to 686

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 This re-pull relocates serverInfo from the DiscoverResult body (required field deleted) to a new optional ResultMetaObject['io.modelcontextprotocol/serverInfo'] _meta key, but ships no companion SDK/test update — so as filed both pnpm typecheck (TS2741 at test/spec.types.2026-07-28.test.ts:544) and the drift-guard vitest run (export inventory 154 vs pinned 153 at line 870; ResultMetaObject missing from the coverage map at line 888) are deterministically red. Beyond CI, the SDK stays divergent on both sides of the wire: the 2026 wire schemas (buildSchemas.ts:851/:1138) and published packages/core/src/schemas.ts:590 still require body serverInfo (so discover() against a spec-compliant server that omits it fails with SdkErrorCode.InvalidResult), client.ts:1090/:1212 still read the deleted body field, server.ts:918 still emits it there, and nothing models the new _meta key (no SERVER_INFO_META_KEY in packages/core/src/constants.ts). Landing this needs the companion port: relax/relocate serverInfo in the wire + published schemas, stamp and read the new _meta key, add the constant, and update the drift-guard test pins/coverage entries.

Extended reasoning...

What the bug is

This spec re-pull (commit 71e30695) makes two coupled changes to the 2026-07-28 anchor with no companion update anywhere else in the SDK:

  1. Deletes the required serverInfo: Implementation field from DiscoverResult (the removed hunk after capabilities: ServerCapabilities, spec.types.2026-07-28.ts:681-686).
  2. Adds a new exported ResultMetaObject interface carrying an optional 'io.modelcontextprotocol/serverInfo'?: Implementation key, and retypes Result._meta from MetaObject to ResultMetaObject — i.e. server identity moves from a required discover-body field to an optional per-result _meta key.

The anchor file exists precisely so that drift like this trips CI, and it does — twice over — but the PR ships only the anchor, leaving the tree red and the SDK spec-divergent.

Deterministic CI failures (verified empirically by three independent runs each)

Typechecktsc --noEmit on packages/core-internal fails at test/spec.types.2026-07-28.test.ts:544 with TS2741: Property 'serverInfo' is missing in type 'DiscoverResult' but required in type '{...}' (the bidirectional sdk = spec assignment for DiscoverResult), plus the cascading DiscoverResultResponse error at line 619. pnpm typecheck / pnpm check:all are red as filed.

Vitest drift guardpnpm --filter @modelcontextprotocol/core-internal test -- spec.types.2026-07-28.test.ts fails twice:

  • Line 870: expect(specTypes).toHaveLength(153) receives 154 — the new export interface ResultMetaObject bumps extractExportedTypes() (grep confirms 154 exported interface/class/type declarations post-PR).
  • Line 888: the comprehensive-coverage check fails with expected [ 'ResultMetaObject' ] to have a length of +0ResultMetaObject appears nowhere in the test file, so it has neither a wireParityChecks/sdkTypeChecks parity entry nor a FEATURE_OWNED_PENDING_2026 allowlist entry (MISSING_SDK_TYPES_2026_07_28 = Object.keys(FEATURE_OWNED_PENDING_2026), line 839).

Runtime/interop divergence if forced through

Client rejects spec-compliant servers. The 2026 wire schemas still require body serverInfo: packages/core-internal/src/wire/rev2026-07-28/buildSchemas.ts:851 (DiscoverResultSchema) and :1138 (the server/discover lifted-result entry), both serverInfo: ImplementationSchema non-optional. The 2026 codec's decodeResult safeParses discover results against wireResultSchemas['server/discover'] (codec.ts:~237-247), so a server that follows the new spec — omitting body serverInfo and stamping it (optionally) in _meta — gets its discover response rejected with SdkErrorCode.InvalidResult, failing the entire connect/discovery flow. The published package has the same problem: packages/core/src/schemas.ts:590 DiscoverResultSchema still requires it, and client.ts:1448 parses discover responses with it.

Client identity plumbing reads a field the spec deleted. packages/client/src/client/client.ts:1090 (this._serverVersion = result.discover.serverInfo) and client.ts:1212 (prior.serverInfo), plus the cache-keying guidance at client.ts:282-290, all read the body location only — none can see the new _meta['io.modelcontextprotocol/serverInfo'].

Server never stamps the new key. packages/server/src/server/server.ts:918 _ondiscover() still returns serverInfo in the DiscoverResult body, and nothing anywhere stamps _meta['io.modelcontextprotocol/serverInfo'] per the new SHOULD ("Servers SHOULD include this field on every response"). There is also no SERVER_INFO_META_KEY constant in packages/core/src/constants.tsCLIENT_INFO_META_KEY exists for the request-side twin, but the new result-side vocabulary is unmodeled, and ResultSchema._meta in packages/core/src/schemas.ts:91-97 is still typed with RequestMetaSchema rather than a result-meta schema.

Why existing code doesn't prevent it

The drift-guard test is designed to catch exactly this — and it does, which is why the tree cannot go green without the companion work. The Zod side stays permissive for the new key (looseObject accepts unknown _meta keys at runtime), so nothing rejects a stamped _meta.serverInfo — but permissiveness cuts only one way: the removal of the required body field is what breaks parsing, because required-field checks are exactly what safeParse enforces.

Step-by-step proof

  1. Check out this PR's head; run pnpm --filter @modelcontextprotocol/core-internal exec tsc --noEmittest/spec.types.2026-07-28.test.ts(544,9): error TS2741: Property 'serverInfo' is missing in type 'DiscoverResult'... (plus line 619). CI typecheck gate: red.
  2. Run pnpm --filter @modelcontextprotocol/core-internal test -- spec.types.2026-07-28.test.ts → two failures: expected [...] to have a length of 153 but got 154 (line 870) and expected [ 'ResultMetaObject' ] to have a length of +0 (line 888). CI test gate: red.
  3. Hypothetically force-merge and pair the SDK client with a spec-compliant 2026-07-28 server: server returns { protocol: {...}, capabilities: {...}, ... } with serverInfo only in _meta. Client-side decodeResult safeParses against DiscoverResultSchema (required serverInfo) → parse failure → SdkErrorCode.InvalidResultdiscover()/connect fails. Interop broken in both directions (SDK server also keeps emitting the deleted body field).

How to fix

Land the companion port in the same PR (or hold this sync until it exists):

  • Wire schemas: drop the required body serverInfo from buildSchemas.ts:851/:1138 per the new spec shape (retain optional acceptance for transition if desired), and mirror in packages/core/src/schemas.ts:590; introduce a result-meta schema carrying the optional 'io.modelcontextprotocol/serverInfo' key and use it for Result._meta.
  • Constants: add SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo' alongside CLIENT_INFO_META_KEY in packages/core/src/constants.ts.
  • Client: read server identity from _meta[SERVER_INFO_META_KEY] (falling back to the body field for 2025-11-25 peers) at client.ts:1090/:1212.
  • Server: stop emitting body serverInfo on the 2026 wire shape and stamp _meta[SERVER_INFO_META_KEY] per the SHOULD in server.ts:918.
  • Drift-guard test: bump the inventory pin 153→154, add ResultMetaObject parity checks (or a FEATURE_OWNED_PENDING_2026 entry while the port is staged), and update the DiscoverResult bidirectional check.

Six independent verifiers confirmed every layer of this finding empirically (tsc and vitest runs reproduce the exact errors above); none refuted it.

* This can be used by clients to improve an LLM's understanding of
* available tools (e.g., by including it in a system prompt). It should
* focus on information that helps the model use the server effectively
Expand Down Expand Up @@ -1284,13 +1311,13 @@
}

/**
* Extends {@link MetaObject} with the subscription-stream identifier carried by a
* Extends {@link ResultMetaObject} with the subscription-stream identifier carried by a
* {@link SubscriptionsListenResult}. All key naming rules from `MetaObject` apply.
*
* @see {@link MetaObject} for key naming rules and reserved prefixes.
* @category `subscriptions/listen`
*/
export interface SubscriptionsListenResultMeta extends MetaObject {
export interface SubscriptionsListenResultMeta extends ResultMetaObject {
/**
* Identifies the subscription stream this response closes, so the client can
* correlate it with the originating subscription — mirroring the same key on
Expand Down Expand Up @@ -1333,10 +1360,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
Comment on lines +1363 to +1370

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}
Comment on lines +1363 to 1375

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