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
149 changes: 105 additions & 44 deletions packages/core/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: 9d700ed62dcf86cb77475c9b81930611a9182f46
* Last updated from commit: e3f281c7243b7dfe4c0dca450230fe3d7de3cf8b
*
* 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 @@ -110,6 +110,29 @@ export interface RequestMetaObject extends MetaObject {
'io.modelcontextprotocol/logLevel'?: LoggingLevel;
}

/**
* Extends {@link MetaObject} with additional notification-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 NotificationMetaObject extends MetaObject {
/**
* Identifies the subscription stream a notification was delivered on. The
* server MUST include this key on every notification delivered via a
* {@link SubscriptionsListenRequest | subscriptions/listen} stream, so the
* client can correlate the notification with the originating subscription.
* The key is absent on notifications not delivered via a subscription
* stream (e.g. progress notifications for an in-flight request), which is
* why it is optional here.
*
* The value is the JSON-RPC ID of the `subscriptions/listen` request that
* opened the stream.
*/
'io.modelcontextprotocol/subscriptionId'?: RequestId;
}
Comment on lines +113 to +134

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 sync adds a new exported NotificationMetaObject interface (carrying the reserved io.modelcontextprotocol/subscriptionId meta key) and changes NotificationParams._meta from MetaObject to it, but the SDK has no NotificationMetaObjectSchema and the drift-guard test has neither an sdkTypeChecks entry nor a MISSING_SDK_TYPES_2026_07_28 allowlist entry for the new name — so the coverage check and the hardcoded export-count assertion in spec.types.2026-07-28.test.ts both fail. The companion port also needs to note that notifications now require their own meta shape (distinct from both request and result meta), which extends the two-way RequestMetaSchema/MetaObjectSchema split prescribed in #3278874070 into a three-way split, and that the eventual subscriptions/listen server implementation MUST stamp subscriptionId on every stream-delivered notification.

Extended reasoning...

What changed

This re-pull adds export interface NotificationMetaObject extends MetaObject at packages/core/src/types/spec.types.2026-07-28.ts:120, defining the reserved key 'io.modelcontextprotocol/subscriptionId'?: RequestId — the JSON-RPC ID of the subscriptions/listen request that opened the stream, which the server MUST include on every notification delivered via that stream (and omit on notifications not delivered via a subscription stream, e.g. progress notifications for an in-flight request). The same hunk changes NotificationParams._meta from MetaObject to NotificationMetaObject (spec.types.2026-07-28.ts:173).

Why the drift guard goes red on this name

packages/core/test/spec.types.2026-07-28.test.ts extracts every export interface|class|type name from the snapshot and asserts each one not in MISSING_SDK_TYPES_2026_07_28 (lines 381-491) has an sdkTypeChecks entry; it also hardcodes expect(specTypes).toHaveLength(150) at line 521. NotificationMetaObject appears nowhere in the test file (grep: 0 matches), so:

  1. Coverage checkshould have comprehensive compatibility tests fails with NotificationMetaObject in missingTests (line 539's toHaveLength(0)).
  2. Type count — the file now has 151 exports (grep -cE '^export (interface|class|type) ' = 151), so the toHaveLength(150) assertion at line 521 fails.

Step-by-step: (1) extractExportedTypes picks up 'NotificationMetaObject' from line 120; (2) the allowlist at lines 381-491 does not contain it; (3) sdkTypeChecks['NotificationMetaObject'] is undefined → the name lands in missingTests → assertion fails; (4) independently, 151 ≠ 150 fails the count check. None of the existing porting checklists on this PR (#3223937253's additive batch, #3206453749, #3252228073, or the later corrections) enumerate this export, so a porter working through them would still have a red coverage check on this one name.

Why the SDK side is also a real gap, not just a test-file addition

A grep across packages/core/src finds NotificationMetaObject and io.modelcontextprotocol/subscriptionId only in the spec snapshot — there is no NotificationMetaObjectSchema in schemas.ts, no types.ts alias, and no SUBSCRIPTION_ID_META_KEY constant. NotificationsParamsSchema._meta currently reuses RequestMetaSchema (schemas.ts:110), and the test file does have an sdkTypeChecks.NotificationParams bidirectional entry (test line 77), so the spec-side change of NotificationParams._meta makes that check relevant: the notification meta shape now carries an optional RequestId-typed reserved key the SDK schema does not model.

This also invalidates the prescription in #3278874070, which directed the companion port to split the shared RequestMetaSchema two ways — a request-direction RequestMetaSchema for BaseRequestParamsSchema and a bare MetaObjectSchema for ResultSchema and NotificationsParamsSchema. After this revision, notifications need their own meta shape (bare MetaObject plus the optional subscriptionId key), distinct from both the request meta and the bare result meta — i.e. a three-way split (RequestMetaSchema / NotificationMetaObjectSchema / MetaObjectSchema), not the two-way one the existing comment prescribes. A porter following #3278874070 verbatim would wire NotificationsParamsSchema._meta to the bare MetaObjectSchema and the bidirectional/exact-keys checks would still need the subscriptionId-bearing shape.

Companion-work scoping (server side)

The MUST-include rule is a server-side delivery requirement: when the eventual subscriptions/listen implementation lands (per the existing transport-layer comments #3239359153/#3246176899), every notification routed onto a subscription stream must be stamped with _meta['io.modelcontextprotocol/subscriptionId'] = <id of the listen request>. No notification-emit path in packages/server stamps any subscription correlation key today, and no existing porting checklist names this requirement.

How to fix

In the companion PR (this automated sync is already held for that work): add NotificationMetaObjectSchema to schemas.ts (bare loose meta object plus optional 'io.modelcontextprotocol/subscriptionId': RequestIdSchema), point NotificationsParamsSchema._meta at it (adjusting the #3278874070 split to three-way), export the SUBSCRIPTION_ID_META_KEY constant, add the sdkTypeChecks.NotificationMetaObject entry (or an allowlist entry if the SDK chooses not to model it), bump the toHaveLength count once all deltas are reconciled, and include the stamp-subscriptionId-on-stream-delivery requirement in the subscriptions/listen server implementation's scope.


/**
* A progress token, used to associate progress notifications with the original request.
*
Expand Down Expand Up @@ -147,7 +170,7 @@ export interface Request {
* @category Common Types
*/
export interface NotificationParams {
_meta?: MetaObject;
_meta?: NotificationMetaObject;
}

/** @internal */
Expand Down Expand Up @@ -298,7 +321,7 @@ export interface InvalidRequestError extends Error {
*
* In MCP, a server returns this error when a client invokes a method the server does not implement — either a genuinely unknown method, or one gated behind a server capability the server did not advertise (e.g., calling `prompts/list` when the `prompts` capability was not advertised).
*
* A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32003`).
* A request that requires a client capability the client did not declare is signalled instead by {@link MissingRequiredClientCapabilityError} (`-32021`).
*
* @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object}
*
Expand Down Expand Up @@ -357,21 +380,67 @@ export interface InternalError extends Error {
code: typeof INTERNAL_ERROR;
}

/*
* MCP error codes.
*
* JSON-RPC 2.0 reserves `-32000` to `-32099` for implementation-defined
* server errors. MCP partitions that range:
*
* - `-32000` to `-32019`: implementation-defined. Existing SDKs and
* implementations use codes here for their own purposes; the specification
* will never define codes in this sub-range, and receivers must not assign
* cross-implementation semantics to them.
* - `-32020` to `-32099`: reserved for error codes defined by the MCP
* specification. Every code allocated here is recorded in this file.
* Codes are allocated sequentially starting at `-32020` and proceeding
* toward `-32099`.
*
* Codes defined by earlier protocol versions remain reserved and are never
* reused: `-32002` (resource not found, 2025-11-25 and earlier; replaced by
* `-32602`) and `-32042` (URL elicitation required, 2025-11-25 only).
Comment on lines +396 to +400

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Companion-work item: this sync's new error-code partition prose (spec.types.2026-07-28.ts:398-400) retires -32002 — "resource not found, 2025-11-25 and earlier; replaced by -32602" — but the SDK still defines ProtocolErrorCode.ResourceNotFound = -32_002 (packages/core/src/types/enums.ts:14, with no version-scoping JSDoc, unlike the adjacent 2026-07-28-annotated members) and still throws it for unknown resource URIs in the resources/read handler (packages/server/src/server/mcp.ts:427), so a 2026-07-28-compliant client expecting -32602 won't recognize the response. The companion 2026-07-28 work should map resource-not-found to -32602 (InvalidParams) when speaking the new revision — keeping -32002 for the 2025-11-25 interop window — and/or add version-scoping JSDoc to the enum member, mirroring the -32042/UrlElicitationRequired treatment.

Extended reasoning...

What changed in the spec

This sync adds the new error-code partition comment (spec.types.2026-07-28.ts:396-400), which states that codes defined by earlier protocol versions remain reserved and are never reused, explicitly listing -32002 (resource not found, 2025-11-25 and earlier; replaced by -32602). In other words, under the 2026-07-28 revision the dedicated resource-not-found code is gone: an unknown-resource error folds into the standard INVALID_PARAMS (-32602), and -32002 becomes a never-reused legacy value with no meaning for a 2026-07-28 peer.

What the SDK still ships

  • packages/core/src/types/enums.ts:14ProtocolErrorCode.ResourceNotFound = -32_002, with no version-scoping JSDoc. The asymmetry is visible in the same enum: the immediately following members MissingRequiredClientCapability = -32_003 and UnsupportedProtocolVersion = -32_004 both carry explicit "protocol revision 2026-07-28" notes, while ResourceNotFound gives no hint that it is a 2025-11-25-and-earlier-only code.
  • packages/server/src/server/mcp.ts:427 — the resources/read handler throws new ProtocolError(ProtocolErrorCode.ResourceNotFound, ...) when no registered resource or template matches the requested URI, so the SDK actively emits -32002 on the wire for every unknown-resource read.

Why nothing in CI surfaces it

Neither enums.ts nor mcp.ts imports the spec snapshot, and the partition prose is JSDoc commentary rather than an exported type, so neither tsc nor the spec.types.2026-07-28.test.ts drift guard trips on this. It is the same silent-divergence class as the accepted findings on this PR for -32042/UrlElicitationRequired, the session bootstrap, and the -32021/-32022 error-code renumbering — and unlike those, none of the ~50 existing comments names -32002/ResourceNotFound or the mcp.ts emission site (the existing error-code coverage is limited to -32001/-32003/-32004/-32020/-32021/-32022/-32042; -32002 appears only in passing as a reserved legacy value). A porter working through the existing checklist would therefore miss this item.

Step-by-step proof

  1. The companion 2026-07-28 implementation lands and the SDK negotiates the new revision with a spec-compliant client.
  2. The client calls resources/read with a URI the server does not serve.
  3. The handler at mcp.ts:427 throws ProtocolError(ProtocolErrorCode.ResourceNotFound), which Protocol serialises as a JSON-RPC error with code: -32002.
  4. Per the 2026-07-28 partition, -32002 is an unassigned legacy value — the spec says the unknown-resource case is now signalled with -32602 (INVALID_PARAMS). A client switching on error.code === -32602 for "resource not found" does not recognize the response and falls into its generic-error path instead of its resource-not-found handling.
  5. Conversely, an SDK client built only against ProtocolErrorCode.ResourceNotFound will not classify a compliant 2026-07-28 server's -32602 unknown-resource response as resource-not-found.

Why this is not an immediate defect

The SDK currently targets 2025-11-25 (LATEST_PROTOCOL_VERSION = '2025-11-25' in constants.ts), where -32002 is still the correct wire code, and this PR is an automated sync already held pending the companion 2026-07-28 redesign. So nothing breaks today — this is forward-looking companion-work scoping, in the same tier as the other accepted error-code items on this PR.

How to fix (companion-work scope)

  • When the negotiated revision is 2026-07-28, map unknown-resource errors in the resources/read path (mcp.ts:427) to ProtocolErrorCode.InvalidParams (-32602), keeping -32002 for the 2025-11-25 interop window (version-aware emission, mirroring how the other renumbered codes are being handled).
  • Add version-scoping JSDoc to ProtocolErrorCode.ResourceNotFound ("2025-11-25 and earlier; replaced by -32602 in 2026-07-28"), matching the treatment already applied to UrlElicitationRequired/-32042 and the 2026-07-28-annotated members beside it.
  • Optionally note the mapping in the migration/companion checklist so client-side code that special-cases ResourceNotFound knows to also accept -32602 from 2026-07-28 servers.

*/

/**
* Error code returned when the HTTP headers of a request do not match the
* corresponding values in the request body, or required headers are
* missing or malformed.
*
* @category Errors
*/
export const HEADER_MISMATCH = -32020;

/**
* Error code returned when a server requires a client capability that was
* not declared in the request's `clientCapabilities`.
*
* @category Errors
*/
export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32003;
export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32021;

/**
* Error code returned when the request's protocol version is not supported
* by the server.
*
* @category Errors
*/
export const UNSUPPORTED_PROTOCOL_VERSION = -32004;
export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
Comment on lines +405 to +426

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 repartitions the MCP error-code range (-32000..-32019 implementation-defined, -32020..-32099 spec-reserved) and renumbers the spec constants — HEADER_MISMATCH is now -32020, MISSING_REQUIRED_CLIENT_CAPABILITY moves -32003 → -32021, and UNSUPPORTED_PROTOCOL_VERSION moves -32004 → -32022 — so the SDK's ProtocolErrorCode.MissingRequiredClientCapability/UnsupportedProtocolVersion members (enums.ts:19/24) and the public UnsupportedProtocolVersionError class (errors.ts) now carry the wrong wire values for the 2026-07-28 revision they document, and the spec.types.2026-07-28.test.ts assertions pinning -32003/-32004 fail. The new partition also makes prior comment #3433435413 partially stale: the transport's -32000/-32001 codes (e.g. the 404/-32001 'Session not found' response) now sit in the implementation-defined sub-range and no longer collide with HeaderMismatchError, so any ProtocolErrorCode.HeaderMismatch member added by the companion work must use -32_020, not -32_001. The remaining HeaderMismatchError gap (no SDK schema/types/specTypeSchema registration, no sdkTypeChecks or allowlist entry, and the snapshot now exporting 151 types vs the hardcoded toHaveLength(150)) still stands and keeps the 2026-07-28 drift guard red.

Extended reasoning...

What changed

This re-pull introduces an explicit partition of the JSON-RPC implementation-defined error range (spec.types.2026-07-28.ts:383-401): -32000 to -32019 is implementation-defined ("the specification will never define codes in this sub-range"), and -32020 to -32099 is reserved for spec-defined codes, allocated sequentially from -32020. Under that scheme the constants are renumbered: HEADER_MISMATCH = -32020 (line 410), MISSING_REQUIRED_CLIENT_CAPABILITY moves from -32003 to -32021 (line 418), and UNSUPPORTED_PROTOCOL_VERSION moves from -32004 to -32022 (line 426). The old -32002/-32042 codes are documented as "reserved and never reused" legacy values from earlier revisions.

SDK surface that is now stale

The SDK recently added (per the 2026-06-17 status update) ProtocolErrorCode.MissingRequiredClientCapability = -32_003 and ProtocolErrorCode.UnsupportedProtocolVersion = -32_004 (packages/core/src/types/enums.ts:19,24), each JSDoc-documented as "protocol revision 2026-07-28". packages/core/src/types/errors.ts:59-69 builds the publicly exported UnsupportedProtocolVersionError class on the -32_004 enum value (and types.ts:488 documents "the -32004 ... protocol error"), and test/types/errors.test.ts asserts code -32004. After this sync those members carry the wrong wire values for the protocol revision they explicitly claim to implement: a 2026-07-28-compliant peer switching on error.code === -32021/-32022 will not recognise -32003/-32004, and the renumbered spec relegates those values to never-reused legacy status. This is not silent, either — packages/core/test/spec.types.2026-07-28.test.ts:504-507 imports the spec constants and asserts they equal -32003/-32004 and equal the corresponding ProtocolErrorCode members, so this sync directly breaks those assertions (-32003 !== -32021, -32004 !== -32022).

Prior -32001 guidance is now stale

Inline comment #3433435413 was written against the previous revision where HEADER_MISMATCH = -32001, and its main remediation items were (a) resolve the wire-code collision with the Streamable HTTP transport's createJsonErrorResponse(404, -32_001, 'Session not found') (packages/server/src/server/streamableHttp.ts:887) and (b) add ProtocolErrorCode.HeaderMismatch = -32_001. Under the new partition the transport's -32000/-32001 responses sit squarely in the implementation-defined sub-range, so the collision concern is moot — the session-not-found response and its pinned tests do not need re-coding for this reason — and a porter who follows the old guidance literally would bake the wrong wire value (-32_001 instead of -32_020) into the public ProtocolErrorCode enum.

What remains valid from the prior comment

The missing SDK surface for HeaderMismatchError is still real, just against the new value: grep HeaderMismatch across packages/core/src matches only the spec snapshot — there is no ProtocolErrorCode member for -32_020, no HeaderMismatchErrorSchema, no types.ts alias, and no specTypeSchema registration. The drift guard reflects this: spec.types.2026-07-28.test.ts has neither an sdkTypeChecks entry nor a MISSING_SDK_TYPES_2026_07_28 allowlist entry for HeaderMismatchError (only the MissingRequiredClientCapabilityError/UnsupportedProtocolVersionError envelopes are allowlisted at lines 477-478), so the coverage check at line 539 fails on this name, and the snapshot now exports 151 types versus the hardcoded toHaveLength(150) at line 521.

Step-by-step proof

  1. Diff: MISSING_REQUIRED_CLIENT_CAPABILITY changes from -32003 to -32021 and UNSUPPORTED_PROTOCOL_VERSION from -32004 to -32022; HEADER_MISMATCH is added at -32020 with the partition comment declaring -32000..-32019 implementation-defined.
  2. enums.ts:19,24 still ship -32_003/-32_004 documented as 2026-07-28; errors.ts exports UnsupportedProtocolVersionError emitting -32_004 via exports/public/index.ts:97.
  3. spec.types.2026-07-28.test.ts:504-507 asserts ProtocolErrorCode.MissingRequiredClientCapability === MISSING_REQUIRED_CLIENT_CAPABILITY (and likewise for UnsupportedProtocolVersion) → both fail after this sync (-32003 vs -32021, -32004 vs -32022).
  4. Separately, extractExportedTypes picks up HeaderMismatchError; it is in neither sdkTypeChecks nor the allowlist → coverage check fails, and 151 ≠ 150 fails the count check.
  5. A 2026-07-28-compliant server emitting -32021/-32022 is not recognised by SDK code switching on the current enum values; conversely an SDK server emitting -32003/-32004 sends codes the revision now treats as unassigned legacy values.

How to fix (companion work)

Renumber ProtocolErrorCode.MissingRequiredClientCapability to -32_021 and ProtocolErrorCode.UnsupportedProtocolVersion to -32_022 for the 2026-07-28 surface (or add new members if interop with the prior draft values is desired), update the UnsupportedProtocolVersionError/types.ts JSDoc and errors.test.ts, and fix the spec.types.2026-07-28.test.ts constant assertions. When adding the HeaderMismatch surface called for in #3433435413, use -32_020 (not -32_001), add the schema/alias/specTypeSchema registration or an allowlist entry, and bump the export-count assertion to 151. Drop the now-moot 'decide the -32001 session-not-found collision' items from the porting checklist — the transport's implementation-defined codes no longer clash.


/**
* Returned when a server rejects a request because the values in the HTTP
* headers do not match the corresponding values in the request body, or
* because required headers are missing or malformed. For HTTP, the response
* status code MUST be `400 Bad Request`.
*
* @example Header mismatch
* {@includeCode ./examples/HeaderMismatchError/header-mismatch.json}
*
* @category Errors
*/
export interface HeaderMismatchError extends Omit<JSONRPCErrorResponse, 'error'> {
error: Error & {
code: typeof HEADER_MISMATCH;
};
Comment on lines 425 to +442

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 spec now reserves -32001 for the new HeaderMismatchError (HTTP headers don't match the request body / required headers missing — HTTP status MUST be 400), but the Streamable HTTP server transport already emits -32_001 on the wire with different semantics: createJsonErrorResponse(404, -32_001, 'Session not found') at packages/server/src/server/streamableHttp.ts:887 (asserted by server and middleware tests), so a 2026-07-28-compliant client switching on error.code === -32001 will misread the SDK's session-expiry response. The companion work needs to either move session-not-found to a different implementation-specific code or adopt the spec semantics, and also add the missing SDK surface — ProtocolErrorCode has no member for -32001, there is no HeaderMismatchErrorSchema/types alias/specTypeSchema registration, and the new HeaderMismatchError export trips the spec.types.2026-07-28.test.ts coverage check.

Extended reasoning...

What changed in the spec

This sync adds a dedicated error code and envelope for header/body mismatches on the Streamable HTTP transport (spec.types.2026-07-28.ts:390 and ~408-422):

export const HEADER_MISMATCH = -32001;

export interface HeaderMismatchError extends Omit<JSONRPCErrorResponse, 'error'> {
    error: Error & {
        code: typeof HEADER_MISMATCH;
    };
}

The JSDoc is normative: it is returned "when a server rejects a request because the values in the HTTP headers do not match the corresponding values in the request body, or because required headers are missing or malformed", and "For HTTP, the response status code MUST be 400 Bad Request."

Gap 1 — wire-code collision with the SDK's existing -32001 usage

The Streamable HTTP server transport already emits this exact numeric code with completely different semantics. packages/server/src/server/streamableHttp.ts:887 returns:

return this.createJsonErrorResponse(404, -32_001, 'Session not found');

i.e. HTTP 404 + code -32001 = "session expired/not found", and that pairing is pinned by tests (packages/server/test/server/streamableHttp.test.ts:292, packages/middleware/node/test/streamableHttp.test.ts:538 and :934). Until now -32001 was an unclaimed implementation-defined code, so this was fine. Once 2026-07-28 ships, the spec owns -32001 with "header mismatch, MUST be 400" semantics.

Step-by-step failure scenario:

  1. A 2026-07-28-compliant client implements the spec's error handling: on error.code === -32001 it treats the failure as a header/body mismatch (e.g. it re-derives its MCP-Protocol-Version header or x-mcp-header-mirrored values from the body and retries the same session).
  2. That client talks to an SDK server in stateful mode. Its session expires (server restart, eviction). It sends a request with the stale Mcp-Session-Id.
  3. The SDK transport replies HTTP 404 with body { error: { code: -32001, message: 'Session not found' } }.
  4. The client's spec-driven decode path classifies this as a header mismatch rather than session loss, so it does not perform the correct recovery (re-initialize / start a new session); it may retry the same dead session indefinitely or surface a misleading error. The HTTP status disagreement (404 observed vs the spec's "MUST be 400") makes the response doubly inconsistent for any client that validates both.

Why nothing prevents this: streamableHttp.ts does not import spec.types.*, the -32_001 literal is hand-written, and no test compares the transport's implementation-specific codes against the spec's reserved range — so the collision is silent in CI.

Fix direction (companion work decision): either re-code session-not-found to a code the spec has not claimed (keeping the 404), or adopt the spec's -32001/400 semantics for actual header-mismatch conditions and use a different code for session loss. Either way the two server/middleware tests that pin 404 + -32001 + 'Session not found' need updating in the same change.

Gap 2 — missing SDK surface and drift-guard failure

HeaderMismatchError / HEADER_MISMATCH have no SDK counterpart anywhere outside the spec snapshot:

  • packages/core/src/types/enums.ts ProtocolErrorCode has members for -32_002 (ResourceNotFound), -32_003 (MissingRequiredClientCapability), -32_004 (UnsupportedProtocolVersion), and -32_042, but no member for -32_001 — so the companion transport hook that is supposed to emit this error (e.g. when the body _meta protocol version disagrees with the MCP-Protocol-Version header, or when an x-mcp-header-mirrored argument disagrees with the body) has no named constant to use.
  • A grep for HeaderMismatch across packages/core matches only spec.types.2026-07-28.ts — no HeaderMismatchErrorSchema in schemas.ts, no types.ts alias, no specTypeSchema registration.
  • The drift guard is CI-visible: packages/core/test/spec.types.2026-07-28.test.ts extracts every exported type, and HeaderMismatchError is in neither sdkTypeChecks nor the MISSING_SDK_TYPES_2026_07_28 allowlist (which names MissingRequiredClientCapabilityError and UnsupportedProtocolVersionError but not this one), so the "should have comprehensive compatibility tests" check fails on this name, and the new export also contributes to the type-count mismatch against expect(specTypes).toHaveLength(150).

Why this isn't covered by existing comments

None of the existing PR comments mention -32001 or HeaderMismatchError — the error-code comments on this PR cover only -32003/-32004 (and the prior header/body cross-check comment #3239359153 predates this dedicated code and pointed at UnsupportedProtocolVersionError instead). The session-not-found collision in particular is a new wire-compatibility decision that no existing checklist names.

How to fix (companion-work scope)

  • Decide the -32001 collision: re-code the transport's session-not-found response, or align it with the spec's header-mismatch semantics; update packages/server/test/server/streamableHttp.test.ts:292 and packages/middleware/node/test/streamableHttp.test.ts:538/934 accordingly.
  • Add ProtocolErrorCode.HeaderMismatch = -32_001 to enums.ts (mirroring the recently added -32_003/-32_004 members), plus a HeaderMismatchErrorSchema/types alias/specTypeSchema registration, or add HeaderMismatchError to MISSING_SDK_TYPES_2026_07_28 if the SDK chooses to model it as an error class instead.
  • Wire the transport-side emission point: when headers and body disagree (e.g. MCP-Protocol-Version header vs _meta['io.modelcontextprotocol/protocolVersion'], or x-mcp-header mirrors), emit -32001 with HTTP 400 per the new MUST.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Status update — the latest re-pull resolves the wire-code collision (Gap 1) upstream; the SDK-surface gap (Gap 2) remains, with renumbered codes.

Gap 1 (collision with the SDK's -32001 session-not-found) — resolved upstream. The spec no longer claims -32001: HEADER_MISMATCH is now -32020 (spec.types.2026-07-28.ts:410), and the new error-code partitioning comment (lines ~383-401) explicitly reserves -32000 to -32019 as implementation-defined — "the specification will never define codes in this sub-range". The Streamable HTTP transport's createJsonErrorResponse(404, -32_001, 'Session not found') (packages/server/src/server/streamableHttp.ts:887) is therefore no longer in conflict, and no re-coding of session-not-found (or the server/middleware tests that pin it) is needed.

Gap 2 (missing SDK surface + drift guard) — still open, now at -32020.

  • ProtocolErrorCode (packages/core/src/types/enums.ts) still has no member for the header-mismatch code; there is still no HeaderMismatchErrorSchema / types alias / specTypeSchema registration (a grep for HeaderMismatch matches only the spec snapshot).
  • HeaderMismatchError is in neither sdkTypeChecks nor MISSING_SDK_TYPES_2026_07_28 in packages/core/test/spec.types.2026-07-28.test.ts, so the "should have comprehensive compatibility tests" check still fails on this name (and it still contributes to the toHaveLength(150) count mismatch).
  • The transport-side emission point (header↔body mismatch → -32020 with HTTP 400) is still unimplemented.

New wrinkle from the same renumbering: the spec also moved MISSING_REQUIRED_CLIENT_CAPABILITY to -32021 and UNSUPPORTED_PROTOCOL_VERSION to -32022. The recently added ProtocolErrorCode.MissingRequiredClientCapability = -32_003 and UnsupportedProtocolVersion = -32_004 (enums.ts:19/24, documented as protocol revision 2026-07-28) are now stale against this revision — the partitioning note says only -32002 and -32042 remain reserved from earlier revisions. The companion work should re-key those members (or version them) alongside adding HeaderMismatch = -32_020, rather than mirroring the old -32003/-32004 values as this comment originally suggested.

}

/**
* Returned when the request's protocol version is unknown to the server or
Expand Down Expand Up @@ -517,9 +586,9 @@ export interface CancelledNotificationParams extends NotificationParams {
/**
* The ID of the request to cancel.
*
* This MUST correspond to the ID of a request previously issued in the same direction.
* This MUST correspond to the ID of a request the client previously issued.
*/
requestId?: RequestId;
requestId: RequestId;
Comment on lines 586 to +591

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 sync makes CancelledNotificationParams.requestId required ('MUST correspond to the ID of a request the client previously issued'), but the SDK's CancelledNotificationParamsSchema still declares requestId: RequestIdSchema.optional() (packages/core/src/types/schemas.ts:215) with the now-stale 'previously issued in the same direction' JSDoc — so the bidirectional drift-guard checks for CancelledNotificationParams and CancelledNotification in spec.types.2026-07-28.test.ts (lines 117/121) now fail TS2322 in the spec = sdk direction, and neither type is in the MISSING_SDK_TYPES_2026_07_28 allowlist. Note that prior comment #3368827092 concluded 'no SDK change is needed now' when both sides were optional — that conclusion is now stale; the companion work needs to either keep the field optional for 2025-11-25 interop and carve these two entries out of the 2026-07-28 guard, or split a 2026-07-28-strict schema, plus update the stale JSDoc at schemas.ts:213 and the Protocol._oncancel no-op path that exists only for an absent requestId.

Extended reasoning...

What changed in the spec

This re-pull (91b403f8) changes CancelledNotificationParams.requestId from optional (requestId?: RequestId) to required (requestId: RequestId) at spec.types.2026-07-28.ts:586-591, and rewrites the JSDoc from 'previously issued in the same direction' to 'the ID of a request the client previously issued.' The parent CancelledNotification JSDoc (line ~600) is also rewritten: the notification is now client→server for ordinary cancellation, with a narrowly scoped server→client use on stdio solely to terminate a subscriptions/listen stream.

What the SDK still ships

CancelledNotificationParamsSchema at packages/core/src/types/schemas.ts:215 still declares requestId: RequestIdSchema.optional(), and the JSDoc at schemas.ts:213 still reads 'previously issued in the same direction' — wording this diff deletes from the spec. Downstream, Protocol._oncancel (packages/core/src/shared/protocol.ts) contains a no-op branch that exists solely to tolerate an absent requestId (if (!notification.params.requestId) { return; }), a code path that only made sense under the 2025-11-25 task-era optionality.

How the drift guard breaks

packages/core/test/spec.types.2026-07-28.test.ts has explicit bidirectional sdkTypeChecks entries for CancelledNotificationParams (line 117) and CancelledNotification (line 121), each doing sdk = spec; spec = sdk;. Neither name appears in the MISSING_SDK_TYPES_2026_07_28 allowlist, so both checks are live.

Step-by-step proof:

  1. Post-sync, SpecTypes.CancelledNotificationParams.requestId is RequestId (required).
  2. The SDK's Zod-inferred SDKTypes.CancelledNotificationParams.requestId is RequestId | undefined (from .optional() at schemas.ts:215).
  3. The spec = sdk assignment at test line 117 fails with TS2322: requestId may be undefined in the SDK type but is required in the spec type.
  4. The same failure repeats for the nested params of CancelledNotification at test line 121.
  5. So the 2026-07-28 drift-guard test file no longer typechecks against this sync — a CI-visible failure introduced by this diff.

Why the existing porting checklist points away from this

Prior comment #3368827092 (2026-06-07) examined this exact field while both sides were optional and explicitly concluded: 'the bidirectional CancelledNotification assignability check in spec.types.test.ts passes in both directions … No SDK change is appropriate now.' The 2026-06-18 status update #3433356219 notes the field became required only as the resolution of the ServerNotification union-arm reachability discussion; it never names the schemas.ts:215 divergence or the now-failing drift-guard entries. A porter working through the existing comments would therefore treat this field as settled and miss a real TS2322 failure plus an unrecorded companion-work decision.

Impact

CI for the 2026-07-28 drift guard goes red on these two entries (on top of the other documented breakage on this held sync). More importantly, the companion work has a genuine wire-compatibility decision to make that no existing comment records: the 2025-11-25 snapshot (and the deliberately retained 2025-11-25 wire surface from #2248) still permits requestId-less cancellations from task-era peers, so the SDK receiver schema arguably should stay optional for that interop window — but then the 2026-07-28 guard needs a deliberate carve-out, not a silent failure.

How to fix (companion work)

Pick one of:

  • Keep requestId optional in CancelledNotificationParamsSchema for 2025-11-25 interop, and move CancelledNotificationParams/CancelledNotification into the MISSING_SDK_TYPES_2026_07_28 allowlist (or add an explicit carve-out) so the 2026-07-28 guard documents the divergence instead of failing.
  • Split a 2026-07-28-strict schema (required requestId) keyed to the new revision, keeping the lenient schema for the older snapshot.

Either way, also update the stale 'previously issued in the same direction' JSDoc at schemas.ts:213 to the new client-direction wording (plus the stdio subscriptions/listen-termination caveat), and revisit the Protocol._oncancel absent-requestId no-op path — under 2026-07-28 semantics an absent requestId is no longer a valid wire state, so that branch should either be scoped to the 2025-11-25 interop window or removed when the strict schema lands.


/**
* An optional string describing the reason for the cancellation. This MAY be logged or presented to the user.
Expand All @@ -528,7 +597,9 @@ export interface CancelledNotificationParams extends NotificationParams {
}

/**
* This notification can be sent by either side to indicate that it is cancelling a previously-issued request.
* This notification is sent by the client to indicate that it is cancelling a request it previously issued.
*
* On stdio, the server also sends this notification, solely to terminate a {@link SubscriptionsListenRequest | subscriptions/listen} stream: it references the ID of the `subscriptions/listen` request that opened the stream. Servers MUST NOT use this notification to cancel any other request.
*
Comment on lines 597 to 603

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Companion-work scoping note: the rewritten CancelledNotification JSDoc (lines 597–603) adds a normative server→client use on stdio — the server terminates a subscriptions/listen stream by sending notifications/cancelled referencing the ID of the listen request the client issued — but Protocol._oncancel (packages/core/src/shared/protocol.ts:345-352) only looks up _requestHandlerAbortControllers (incoming-request IDs) and never consults _responseHandlers/_progressHandlers, so a cancellation naming the receiver's own outgoing listen request is silently dropped and the pending request would hang until a misleading RequestTimeout. When the companion subscriptions/listen work lands, the cancelled-notification path needs a receiver-side route that settles/aborts the local outstanding listen request (gated per the spec's 'MUST NOT cancel any other request' constraint).

Extended reasoning...

What changed in the spec

This re-pull rewrites the CancelledNotification JSDoc (spec.types.2026-07-28.ts:597-603). The notification is now client→server for ordinary cancellation, and it gains one new normative server→client use: "On stdio, the server also sends this notification, solely to terminate a subscriptions/listen stream: it references the ID of the subscriptions/listen request that opened the stream. Servers MUST NOT use this notification to cancel any other request." In other words, the receiver of the cancellation is now expected to react to a cancellation that names a request the receiver itself issued (its own outgoing listen request) — not a request it is currently handling.

What the SDK does today

Protocol._oncancel (packages/core/src/shared/protocol.ts:345-352) handles every incoming notifications/cancelled by looking up this._requestHandlerAbortControllers.get(params.requestId) — a map keyed exclusively by incoming request IDs the local side is handling (populated in _onrequest at protocol.ts:512). It never consults _responseHandlers / _progressHandlers / _timeoutInfo (the maps tracking the local side's own outgoing requests, protocol.ts:289-291). A cancelled notification whose requestId names one of the receiver's own outstanding outgoing requests therefore finds no abort controller and silently returns: the pending Protocol.request() promise is never settled, no handler is informed, and nothing is cleaned up.

Step-by-step proof (once the companion subscriptions/listen work lands)

  1. An SDK client on stdio opens the notification stream by issuing subscriptions/listen (the 2026-07-28 replacement for the standalone GET stream). The request ID lands in _responseHandlers/_progressHandlers.
  2. The server later shuts the stream down using the only spec-defined mechanism on stdio: it sends notifications/cancelled with the listen request's ID.
  3. The SDK client's default notifications/cancelled handler (registered in the Protocol constructor, protocol.ts:323-325) routes to _oncancel.
  4. _oncancel looks up _requestHandlerAbortControllers.get(requestId) — the listen request is an outgoing request, so there is no entry — and returns.
  5. The client never learns the stream was terminated: the listen request stays pending in _responseHandlers until its timeout fires with a misleading RequestTimeout, the subscription bookkeeping is never torn down, and there is no hook a subscriptions/listen implementation could use to observe the termination.

Why nothing in CI surfaces it and why existing comments don't cover it

shared/protocol.ts does not import the spec snapshot, so the drift guard cannot see this — the same silent companion-work class as the other accepted transport/Protocol-layer findings on this PR. Status update #3433356219 records only the sender-side scoping of this change ('the server-side cancel path is now narrowly scoped to stream termination on stdio') for the ServerNotificationSchema work; #3440566896 covers the requestId optional-vs-required schema/test divergence and the absent-requestId no-op branch in _oncancel; the broader subscriptions/listen comments (#3239359153, #3223937258, #3433435417) cover transport routing, schemas, and subscriptionId stamping. None names the fact that _oncancel has no path for cancellations targeting the receiver's own outgoing requests — which is the mechanism the new normative sentence depends on.

Impact

Until addressed, the only spec-defined way to end a subscriptions/listen stream on stdio is a no-op against the SDK: the listen request hangs, then fails with a timeout error that misattributes the cause, and the listen-stream manager has no termination signal to clean up against.

How to fix (companion work, not this automated sync)

When implementing subscriptions/listen, extend the cancelled-notification handling so that a cancellation referencing one of the local side's outstanding outgoing requests settles/aborts that request (e.g. resolve or reject the _responseHandlers entry and clean up _progressHandlers/_timeoutInfo), or route it to the listen-stream manager — and gate the behavior to the listen request specifically, per the spec's 'Servers MUST NOT use this notification to cancel any other request' constraint.

* The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished.
*
Expand Down Expand Up @@ -569,7 +640,7 @@ export interface DiscoverRequest extends JSONRPCRequest {
*
* @category `server/discover`
*/
export interface DiscoverResult extends Result {
export interface DiscoverResult extends CacheableResult {
/**
* MCP Protocol Versions this server supports. The client should choose a
* version from this list for use in subsequent requests.
Expand Down Expand Up @@ -674,6 +745,9 @@ export interface ClientCapabilities {
* (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are
* per-extension settings objects. An empty object indicates support with no settings.
*
* Keys MUST follow the {@link MetaObject | `_meta` key naming rules}, with a
* mandatory prefix.
*
* @example Extensions — MCP Apps (UI) extension with MIME type support
* {@includeCode ./examples/ClientCapabilities/extensions-ui-mime-types.json}
*/
Expand Down Expand Up @@ -768,6 +842,9 @@ export interface ServerCapabilities {
* (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings
* objects. An empty object indicates support with no settings.
*
* Keys MUST follow the {@link MetaObject | `_meta` key naming rules}, with a
* mandatory prefix.
*
* @example Extensions — Tasks extension support
* {@includeCode ./examples/ServerCapabilities/extensions-tasks.json}
*/
Expand Down Expand Up @@ -989,11 +1066,13 @@ export interface CacheableResult extends Result {
* Indicates the intended scope of the cached response, analogous to HTTP
* `Cache-Control: public` vs `Cache-Control: private`.
*
* - `"public"`: Any client or intermediary (e.g., shared gateway, proxy)
* MAY cache the response and serve it to any user.
* - `"private"`: Only the requesting user's client MAY cache the response.
* Shared caches (e.g., multi-tenant gateways) MUST NOT serve a cached
* copy to a different user.
* - `"public"`: The response does not contain user-specific data. Any
* client or intermediary (e.g., shared gateway, caching proxy) MAY cache
* the response and serve it across authorization contexts.
* - `"private"`: The response MAY be cached and reused only within the
* same authorization context. Caches MUST NOT be shared across
* authorization contexts (e.g., a different access token requires a
* different cache).
*
*/
cacheScope: 'public' | 'private';
Expand Down Expand Up @@ -1134,7 +1213,7 @@ export interface ReadResourceResultResponse extends JSONRPCResultResponse {
}

/**
* An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client.
* An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This is only delivered on a {@link SubscriptionsListenRequest | subscriptions/listen} stream when the client requested it via the `resourcesListChanged` filter field.
*
* @example Resources list changed
* {@includeCode ./examples/ResourceListChangedNotification/resources-list-changed.json}
Expand Down Expand Up @@ -1578,7 +1657,7 @@ export interface EmbeddedResource {
_meta?: MetaObject;
}
/**
* An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client.
* An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This is only delivered on a {@link SubscriptionsListenRequest | subscriptions/listen} stream when the client requested it via the `promptsListChanged` filter field.
*
* @example Prompts list changed
* {@includeCode ./examples/PromptListChangedNotification/prompts-list-changed.json}
Expand Down Expand Up @@ -1720,7 +1799,7 @@ export interface CallToolRequest extends JSONRPCRequest {
}

/**
* An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client.
* An optional notification from the server to the client, informing it that the list of tools it offers has changed. This is only delivered on a {@link SubscriptionsListenRequest | subscriptions/listen} stream when the client requested it via the `toolsListChanged` filter field.
*
* @example Tools list changed
* {@includeCode ./examples/ToolListChangedNotification/tools-list-changed.json}
Expand Down Expand Up @@ -1822,6 +1901,11 @@ export interface Tool extends BaseMetadata, Icons {
* (`if`/`then`/`else`), reference keywords (`$ref`, `$defs`, `$anchor`), and any other
* standard validation or annotation keywords.
*
* Property schemas may carry an `x-mcp-header` annotation to mirror the
* argument value into an HTTP header on the Streamable HTTP transport. See
* the Streamable HTTP transport specification for the validity and
* extraction rules.
*
* Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided.
Comment on lines +1904 to 1909

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Companion-work scoping: this revision adds JSDoc to Tool.inputSchema (spec.types.2026-07-28.ts:1884-1888) introducing the x-mcp-header annotation, which mirrors a tool argument value into an HTTP header on the Streamable HTTP transport — but neither packages/client/src/client/streamableHttp.ts nor packages/server/src/server/streamableHttp.ts implements any argument-to-header mirroring or extraction/validation, and the feature is not named in any of the existing companion-work comments on this PR. Recording it here so the eventual 2026-07-28 transport port doesn't miss this transport-level feature (silent: neither transport file imports the spec snapshot, so there is no CI signal).

Extended reasoning...

What changed in the spec

This sync adds new JSDoc prose to Tool.inputSchema (packages/core/src/types/spec.types.2026-07-28.ts:1884-1888): "Property schemas may carry an x-mcp-header annotation to mirror the argument value into an HTTP header on the Streamable HTTP transport. See the Streamable HTTP transport specification for the validity and extraction rules." This is a transport-level feature of the 2026-07-28 revision: a tool author annotates a property in inputSchema, the client transport mirrors that argument's value into an HTTP header on the tools/call POST, and the server transport extracts/validates it against the body — backed by the new HEADER_MISMATCH = -32001 / HeaderMismatchError envelope this same diff introduces ("the values in the HTTP headers do not match the corresponding values in the request body" → HTTP 400).

Why neither transport supports it

A grep for x-mcp-header across packages/ matches only this spec snapshot. On the client side, packages/client/src/client/streamableHttp.ts builds POST headers exclusively from _commonHeaders() / auth / mcp-session-id / mcp-protocol-version / requestInit.headers (≈ lines 213-253 and the send() path at ≈ 543-552); it never inspects the outgoing tools/call arguments, never consults the tool's inputSchema, and has no notion of per-argument header mirroring. On the server side, packages/server/src/server/streamableHttp.ts has no path that extracts header values for x-mcp-header-annotated arguments or cross-validates them against the request body — and its only -32001 usage today is the unrelated "Session not found" response, so there is no HeaderMismatch plumbing either (the -32001 code collision itself is tracked by a separate finding; this comment is about the argument-mirroring feature, a distinct surface).

Why this is silent and not covered by the existing checklist

This is the same silent-transport-divergence class already accepted repeatedly on this PR (#3239359153 GET→subscriptions/listen + HTTP-400 MUSTs, #3246176899 session bootstrap, #3256562384 opt-in notification defaults): the requirement lives in JSDoc prose, not types, and neither transport file imports spec.types.ts, so tsc, spec.types.test.ts, and the specTypeSchema allowlist guard all stay green for these files. None of the 40+ existing comments mention x-mcp-header — the closest transport-layer comments (#3239359153, #3246176899) cover the GET endpoint, protocol-version header validation, and session bootstrap, not argument-to-header mirroring. A porter working through the existing checklist could complete every enumerated transport item and still ship a Streamable HTTP transport pair with no support for this 2026-07-28 feature.

Step-by-step proof

  1. A tool author registers a tool whose inputSchema declares properties: { tenantId: { type: 'string', 'x-mcp-header': 'X-Tenant-Id' } } — valid per the new JSDoc.
  2. A spec-compliant client calling that tool mirrors the tenantId argument into an X-Tenant-Id header on the tools/call POST. The SDK client transport, by contrast, sends only its common/auth/session/protocol-version headers — the annotated argument is never mirrored, so the request from an SDK client is missing the header a spec-compliant server (or intermediary routing on it) expects.
  3. Conversely, a spec-compliant server receiving a tools/call from a spec-compliant client is expected to apply the transport spec's validity/extraction rules — including rejecting a header↔body mismatch with HeaderMismatchError (-32001, HTTP 400). The SDK server transport performs no such extraction or validation, so mismatched or spoofed header values pass through silently.
  4. Nothing in CI flags either gap, because the transports have no compile-time link to the spec snapshot and the rule is prose, not a type shape.

Impact and how to fix

This is not a defect introduced by this automated sync — the PR is already held pending the companion 2026-07-28 redesign — but it is a transport-level feature the existing companion-work comments do not enumerate. The companion port should: (a) on the client transport, when sending tools/call, consult the tool's inputSchema (or an equivalent registration-time map) for x-mcp-header annotations and mirror the corresponding argument values into POST headers per the transport spec's validity rules; (b) on the server transport, extract those headers, cross-validate them against the body arguments, and reject mismatches/missing/malformed headers with the -32001 HeaderMismatchError envelope as HTTP 400 (which dovetails with the error-code→HTTP-status hook already called for in #3239359153); and (c) add the corresponding ProtocolErrorCode / schema plumbing tracked in the sibling -32001 finding.

*/
inputSchema: { $schema?: string; type: 'object'; [key: string]: unknown };
Expand Down Expand Up @@ -2533,7 +2617,9 @@ export interface PromptReference extends BaseMetadata {
*/
export interface ListRootsRequest {
method: 'roots/list';
params?: RequestParams;
params?: {
_meta?: MetaObject;
};
}

/**
Expand Down Expand Up @@ -2643,12 +2729,6 @@ export interface ElicitRequestURLParams {
*/
message: string;

/**
* The ID of the elicitation, which must be unique within the context of the server.
* The client MUST treat this ID as an opaque value.
*/
elicitationId: string;

/**
* The URL that the user should navigate to.
*
Comment on lines 2729 to 2734

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 deletes the entire URL-elicitation completion surface — the required elicitationId field is removed from ElicitRequestURLParams and ElicitationCompleteNotification is deleted outright (and dropped from ServerNotification) — but the SDK still ships both: ElicitRequestURLParamsSchema (schemas.ts:1994) still requires elicitationId, and the ElicitationComplete schemas/types/ServerNotificationSchema membership all survive. As a result the 2026-07-28 drift guard breaks: the bidirectional check at spec.types.2026-07-28.test.ts:301 fails for ElicitRequestURLParams, and the hard reference to SpecTypes.ElicitationCompleteNotification at lines 361-366 is now an unconditional TS2694 compile error. Companion work needs to drop elicitationId from the draft-spec schema surface and decide retain-for-2025-11-25-snapshot vs. remove for the ElicitationComplete artifacts (this supersedes prior comments #3239359156 and #3371043783, which were written while the notification still existed in the spec).

Extended reasoning...

What changed in the spec

This re-pull (91b403f) removes the correlation/completion machinery for URL-mode elicitation in two coordinated deletions:

  1. ElicitRequestURLParams.elicitationId is deleted (this hunk, ~lines 2709-2714 of the new file). The URL-elicitation payload is now just { mode, message, url } — there is no longer a server-assigned ID the client echoes back.
  2. ElicitationCompleteNotification is deleted entirely (the interface and its notifications/elicitation/complete method, ~lines 3023-3028 pre-diff) and removed from the ServerNotification union. A grep of the post-sync spec.types.2026-07-28.ts returns zero occurrences of either elicitationId or ElicitationCompleteNotification.

These are two halves of one surface: elicitationId existed solely so the (now-deleted) completion notification could reference which out-of-band elicitation finished.

What the SDK still ships

  • packages/core/src/types/schemas.ts:1994ElicitRequestURLParamsSchema still declares elicitationId: z.string() as a required field (and ElicitationCompleteNotificationParamsSchema at schemas.ts:2021/2025 also requires it).
  • schemas.ts:2021-2036ElicitationCompleteNotificationParamsSchema / ElicitationCompleteNotificationSchema still exist, and ElicitationCompleteNotificationSchema is still a member of ServerNotificationSchema (schemas.ts:2244).
  • types.ts:374-375 — the inferred type aliases survive; specTypeSchema.ts:63-64 — both schemas remain registered in the spec-schema allowlist.

How it breaks the drift guard

packages/core/test/spec.types.2026-07-28.test.ts is the drift guard for this snapshot:

  1. TS2694 compile error. Lines 361-366 hard-reference SpecTypes.ElicitationCompleteNotification in the sdkTypeChecks map. With the export deleted, this is an unconditional "Namespace has no exported member" error — the test file no longer compiles against this sync.
  2. ElicitRequestURLParams bidirectional check fails. Line 301 runs sdk = spec; spec = sdk; for ElicitRequestURLParams, and this type is not in the MISSING_SDK_TYPES_2026_07_28 allowlist. The sdk = spec direction now fails with TS2741: the spec value lacks the elicitationId the SDK type requires.

(One nuance: ServerNotification itself is in MISSING_SDK_TYPES_2026_07_28, so the union mismatch does not surface as a separate test failure — the breakage above is the concrete CI signal.)

Step-by-step proof

  1. Spec post-sync: ElicitRequestURLParams = { mode: 'url'; message: string; url: string } — no elicitationId.
  2. SDK: ElicitRequestURLParamsSchema infers { mode: 'url'; message: string; url: string; elicitationId: string; ... } with elicitationId required.
  3. spec.types.2026-07-28.test.ts:301 does sdk = spec → TS2741 (elicitationId missing in spec type but required in SDK type).
  4. Independently, lines 361-363 reference SpecTypes.ElicitationCompleteNotification → TS2694, since the export no longer exists.
  5. At runtime, a draft-spec-valid URL-elicitation InputRequest payload omitting elicitationId is rejected by the SDK's Zod validator — the over-strict-Zod class this repo has hit before (fix(core): allow additional JSON Schema properties in elicitInput requestedSchema #1768/fix(core): allow extra JSON Schema keys in elicit primitive schemas #1849/fix: allow any JSON Schema type for outputSchema #1169).

Why this isn't covered by existing comments

Prior comment #3239359156 flagged the delivery-channel gap for ElicitationCompleteNotification while the type still existed in the spec — the deletion makes that moot. #3371043783 flagged only a missing Params test entry on the assumption "the SDK side is already complete" — the situation has inverted: the spec dropped the type, and the SDK artifacts are now the leftover, not the gap. No existing comment notes the elicitationId field removal at all.

How to fix (companion work)

  • Drop elicitationId from ElicitRequestURLParamsSchema for the draft/2026-07-28 surface (or scope the requirement to the 2025-11-25 snapshot, where the field still exists).
  • Decide retain-vs-remove for the ElicitationComplete schemas: the 2025-11-25 snapshot (spec.types.2025-11-25.ts) still defines the notification, so the likely answer is keep the SDK schemas for that interop window but remove/relocate the sdkTypeChecks entry at spec.types.2026-07-28.test.ts:361-366 (and adjust the ElicitRequestURLParams entry at line 301) so the 2026-07-28 drift guard compiles and passes.
  • Update the ServerNotificationSchema membership decision in line with whichever spec revision the SDK union is meant to track.

Comment on lines 2729 to 2734

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 sync deletes the URL-elicitation completion flow from the 2026-07-28 revision (elicitationId removed from ElicitRequestURLParams, ElicitationCompleteNotification deleted and dropped from ServerNotification), but the SDK's public runtime surface for that flow survives unannotated and is named by no existing comment: Server.createElicitationCompletionNotifier() (server.ts:588-606, no @deprecated/revision-scoping JSDoc unlike the SEP-2577 siblings in the same file), the assertNotificationCapability case for notifications/elicitation/complete (server.ts:301-308), elicitInput() URL mode still forcing callers to mint an elicitationId that has no meaning in 2026-07-28, and examples/{server,client}/src/elicitationUrlExample.ts building their flow on it. Companion work should version-scope or deprecate the notifier API and capability-gate case, make elicitationId optional/revision-dependent in the URL-mode params, and migrate the examples — the schema/test surface is already covered by #3433435404; this is the runtime sibling, kept (not removed) for the 2025-11-25 interop window.

Extended reasoning...

What changed in the spec

This re-pull (e3f281c7) removes the URL-elicitation completion machinery from the 2026-07-28 revision: the required elicitationId field is deleted from ElicitRequestURLParams (the post-sync interface at spec.types.2026-07-28.ts:2721-2738 is just { mode, message, url }), ElicitationCompleteNotification (notifications/elicitation/complete) is deleted outright, and it is dropped from the ServerNotification union. Under 2026-07-28 there is therefore no protocol method for signalling out-of-band elicitation completion and no ID with which to correlate it.

What the SDK still ships at runtime (not covered by the existing schema/test comment)

Inline comment #3433435404 already covers the schema/test surface for this deletion (ElicitRequestURLParamsSchema at schemas.ts:1994, the ElicitationComplete schemas at schemas.ts:2021-2036, types.ts aliases, specTypeSchema.ts registration, ServerNotificationSchema membership, and the spec.types.2026-07-28.test.ts TS2741/TS2694 failures). What no existing comment names is the live, non-deprecated public server runtime API that emits this flow on the wire:

  1. Server.createElicitationCompletionNotifier(elicitationId, options)packages/server/src/server/server.ts:588-606. A public method with no @deprecated and no protocol-revision scoping in its JSDoc — in contrast to the SEP-2577-deprecated siblings in the same file (createMessage()/listRoots() etc., which carry "@deprecated Deprecated as of protocol version 2026-07-28"). It gates on _clientCapabilities.elicitation.url and emits { method: 'notifications/elicitation/complete', params: { elicitationId } } — a notification method the 2026-07-28 revision no longer defines, and for which the new revision provides no delivery channel (the SubscriptionFilter opt-in allowlist has no field for it, and the standalone GET stream is replaced by subscriptions/listen).
  2. The assertNotificationCapability() case 'notifications/elicitation/complete' — server.ts:301-308 — the only capability gate for that method; it keys the deleted notification off _clientCapabilities.elicitation.url, a capability that says nothing about completion notifications in the new revision.
  3. server.elicitInput() URL mode — server.ts:528-538 sends the caller-supplied ElicitRequestURLParams on the wire, and because the SDK type/schema still requires elicitationId, every server author is forced to mint and transmit an ID that has no meaning in 2026-07-28 and whose completion-signal channel no longer exists. (The schema-side relaxation is in #3433435404; the API-shape consequence for elicitInput callers is not.)
  4. Examples: examples/server/src/elicitationUrlExample.ts builds its whole flow on createElicitationCompletionNotifier + elicitationId tracking (lines 55-107, 159-205), and examples/client/src/elicitationUrlExample.ts:571-581 registers a notifications/elicitation/complete handler keyed by elicitationId. (#3440492937 mentions these example apps only in the -32042/UrlElicitationRequiredError context — not the completion-notifier API.)

Why this is silent

server.ts and the example apps have no compile-time link to spec.types.2026-07-28.ts; the only CI signal from this deletion is the test-file breakage already enumerated in #3433435404. A porter who fixes the schemas/tests per that comment ships an SDK that still publicly exposes, capability-gates, and demonstrates an emission path for a notification method the new revision deleted — the same silent-runtime-surface class as the accepted -32042 (#3216586348), session-bootstrap (#3246176899), and logLevel-default (#3256562384) findings, which this PR's review trail consistently treats as distinct from their schema-side siblings.

Step-by-step scenario

  1. The companion 2026-07-28 work lands following the existing checklist: ElicitRequestURLParamsSchema drops elicitationId, the ElicitationComplete schemas are scoped to the 2025-11-25 snapshot, and the drift-guard test is fixed.
  2. A server author following the still-published, still-undeprecated API calls server.createElicitationCompletionNotifier(id) after a URL elicitation against a 2026-07-28 peer.
  3. The capability gate passes (the client declared elicitation.url), and the server emits notifications/elicitation/complete — a method that does not exist in the negotiated revision. A strict 2026-07-28 client has no schema for it and, per the new opt-in subscription model, should never receive an unsolicited notification it didn't subscribe to. The intended replacement signal (re-poll / retry-with-inputResponses under the InputRequiredResult flow) is never produced.

How to fix (companion-work scope)

The SDK's wire surface deliberately targets 2025-11-25 (where this flow still exists), so removal is not the ask — version-scoping/deprecation is, mirroring how the SEP-2577 tier and the -32042 mechanism (#3440492937) were handled:

  • Add revision-scoping or @deprecated JSDoc to Server.createElicitationCompletionNotifier() noting it is a 2025-11-25-only mechanism, and decide its behaviour when the negotiated revision is 2026-07-28 (no-op vs. throw).
  • Mirror that decision in the assertNotificationCapability case at server.ts:301-308.
  • Make elicitationId optional / revision-dependent in the elicitInput() URL-mode parameter type so 2026-07-28 servers are not forced to mint a dead field (dovetails with the schemas change in #3433435404).
  • Migrate or fork examples/{server,client}/src/elicitationUrlExample.ts (and any docs referencing the completion notification) to the 2026-07-28 completion model when that work lands.

Filed as a nit in the same companion-work-scoping tier as the other accepted silent-divergence findings on this held PR; it does not independently break CI.

Expand Down Expand Up @@ -2963,24 +3043,6 @@ export interface ElicitResult {
content?: { [key: string]: string | number | boolean | string[] };
}

/**
* An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request.
*
* @example Elicitation complete
* {@includeCode ./examples/ElicitationCompleteNotification/elicitation-complete.json}
*
* @category `notifications/elicitation/complete`
*/
export interface ElicitationCompleteNotification extends JSONRPCNotification {
method: 'notifications/elicitation/complete';
params: {
/**
* The ID of the elicitation that completed.
*/
elicitationId: string;
};
}

/* Client messages */
/** @internal */
export type ClientRequest =
Expand All @@ -2996,7 +3058,7 @@ export type ClientRequest =
| ListToolsRequest;

/** @internal */
export type ClientNotification = CancelledNotification | ProgressNotification;
export type ClientNotification = CancelledNotification;

/** @internal */
export type ClientResult = EmptyResult;
Expand All @@ -3012,7 +3074,6 @@ export type ServerNotification =
| ResourceListChangedNotification
| ToolListChangedNotification
| PromptListChangedNotification
| ElicitationCompleteNotification
| SubscriptionsAcknowledgedNotification;

/** @internal */
Expand Down