Skip to content

feat(core,client,server): stateful protocol version constants; initialize negotiates stateful versions only#2235

Open
felixweinberger wants to merge 2 commits into
fweinberger/m2-ctx-envelopefrom
fweinberger/m3-draft-version-guards
Open

feat(core,client,server): stateful protocol version constants; initialize negotiates stateful versions only#2235
felixweinberger wants to merge 2 commits into
fweinberger/m2-ctx-envelopefrom
fweinberger/m3-draft-version-guards

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger commented Jun 1, 2026

🗺️ Milestone tracker · M3: draft version constants + lifecycle guards · stacked on #2231

Adds STATEFUL_PROTOCOL_VERSIONS (and DRAFT_PROTOCOL_VERSIONS) and guards so initialize only negotiates stateful protocol versions (2025-11-25 and older).

Motivation and Context

The 2026 revision negotiates versions per-request, never via initialize — permanently. Without these guards, a post-2025-11-25 version listed in supportedProtocolVersions gets requested and accepted through the handshake.

How Has This Been Tested?

Unit tests for all four guard corners (client request selection, draft-only rejection, server accept, server fallback, draft in the initialize result). e2e wire tap on all five transports proves the draft string never appears in the handshake. Full matrix 0 unexpected failures.

Breaking Changes

None.

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Draft-only supported lists keep the existing ?? LATEST_PROTOCOL_VERSION fallback for now; the -32004 answer ships with the version-error PR. Part of #2184.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 1, 2026

🦋 Changeset detected

Latest commit: 9306136

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

This PR includes changesets to release 3 packages
Name Type
@modelcontextprotocol/core Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/server Patch

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

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 1, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@2235

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2235

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@2235

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-legacy@2235

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@2235

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2235

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@2235

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@2235

commit: 9306136

Add STATEFUL_PROTOCOL_VERSIONS — the closed list of protocol versions
negotiated via the initialize handshake; every revision after 2025-11-25
is stateless and negotiates per-request — with an
isStatefulProtocolVersion() predicate on the internal barrel, plus
DRAFT_PROTOCOL_VERSION_2026 ('DRAFT-2026-v1', mirroring the draft
specification schema) and DRAFT_PROTOCOL_VERSIONS.
SUPPORTED_PROTOCOL_VERSIONS is unchanged. Unit tests pin the draft wire
literal and its stateless classification against the generated draft
spec schema.
…itialize

Protocol revisions after 2025-11-25 are stateless and never negotiated
via the initialize handshake. Both sides now run the unchanged handshake
logic against the stateful subset of supportedProtocolVersions:

- Client: requests the first stateful supported version, rejects in
  connect() when the subset is empty, and rejects an initialize result
  whose version is outside the subset.
- Server: accepts the requested version only when it is in its own
  subset, otherwise falls back to the subset's first entry.

Covered by unit tests on both sides and an e2e requirement
(lifecycle:version:initialize-stateful-versions-only) that wire-taps a
handshake where both sides list the draft revision and asserts no
post-2025-11-25 version string appears.
@felixweinberger felixweinberger force-pushed the fweinberger/m3-draft-version-guards branch from d5a52bb to 9306136 Compare June 1, 2026 22:53
@felixweinberger felixweinberger changed the title feat(core,client,server): draft protocol version constants; initialize never negotiates draft versions feat(core,client,server): stateful protocol version constants; initialize negotiates stateful versions only Jun 1, 2026
@felixweinberger felixweinberger marked this pull request as ready for review June 1, 2026 23:22
@felixweinberger felixweinberger requested a review from a team as a code owner June 1, 2026 23:22
Comment on lines +5 to +10
/**
* Protocol versions that negotiate via the initialize handshake (the stateful model). Closed by
* design: every revision after 2025-11-25 is stateless and negotiates per-request, never via
* initialize. Hardcoded — do not derive from {@linkcode SUPPORTED_PROTOCOL_VERSIONS}.
*/
export const STATEFUL_PROTOCOL_VERSIONS = ['2024-10-07', '2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25'];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 STATEFUL_PROTOCOL_VERSIONS is ordered oldest-first while the sibling SUPPORTED_PROTOCOL_VERSIONS is newest-first, and order is semantically meaningful when these arrays are passed as supportedProtocolVersions (the client requests, and the server falls back to, the first stateful entry). Since the new client error message explicitly tells users to include versions from STATEFUL_PROTOCOL_VERSIONS in supportedProtocolVersions, a user passing this constant verbatim will silently negotiate 2024-10-07 instead of 2025-11-25 — consider reversing the array to match the newest-first convention, or documenting that it is a membership set, not a preference-ordered list.

Extended reasoning...

What the bug is. The new public constant STATEFUL_PROTOCOL_VERSIONS (packages/core/src/types/constants.ts:10) is hardcoded oldest-first: ['2024-10-07', '2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25']. The sibling constant on the line above, SUPPORTED_PROTOCOL_VERSIONS, follows the opposite, newest-first convention: [LATEST_PROTOCOL_VERSION, '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']. Internally the SDK only consumes the new constant via .includes() in isStatefulProtocolVersion(), so no shipped behavior is wrong — the issue is that the constant's order becomes load-bearing the moment a user passes it as supportedProtocolVersions, and this PR actively steers users toward doing exactly that.\n\nWhy order matters. This PR's own JSDoc on ProtocolOptions.supportedProtocolVersions (packages/core/src/shared/protocol.ts) says: "The first stateful entry … is preferred: sent by the client at initialize, used as the server's fallback." Concretely, Client.connect() computes statefulVersions = this._supportedProtocolVersions.filter(isStatefulProtocolVersion) and requests statefulVersions[0] (packages/client/src/client/client.ts:495-496), and Server._oninitialize() falls back to statefulVersions[0] when the requested version is unsupported (packages/server/src/server/server.ts:436-438). So the first element of whatever array the user supplies decides the negotiated version.\n\nThe code path that triggers it. The new client error message reads: "Include at least one version from STATEFUL_PROTOCOL_VERSIONS in supportedProtocolVersions." The natural reaction to that error — or simply the natural reading of the docs added in this PR, which discuss STATEFUL_PROTOCOL_VERSIONS directly in the context of supportedProtocolVersions — is new Client(info, { supportedProtocolVersions: STATEFUL_PROTOCOL_VERSIONS }). Nothing in the JSDoc on the constant warns that it is not preference-ordered.\n\nStep-by-step proof. 1) A user hits the new error (or reads the docs) and writes new Client({ name: 'c', version: '1.0.0' }, { supportedProtocolVersions: STATEFUL_PROTOCOL_VERSIONS }). 2) In connect(), every entry passes isStatefulProtocolVersion, so statefulVersions equals the constant verbatim and requestedProtocolVersion = statefulVersions[0] = '2024-10-07'. 3) The default SDK server has '2024-10-07' in its supported list, so statefulVersions.includes('2024-10-07') is true and it echoes '2024-10-07' back. 4) The client accepts it and getNegotiatedProtocolVersion() returns '2024-10-07' — the connection silently runs on the oldest protocol revision instead of 2025-11-25, with no error or warning anywhere. A server configured with supportedProtocolVersions: STATEFUL_PROTOCOL_VERSIONS likewise falls back to '2024-10-07' for any unsupported requested version.\n\nWhy nothing prevents it. The negotiation still produces a valid version, all guards added in this PR pass, and every existing test passes — the downgrade is invisible unless the user inspects the negotiated version. That is also why this is a nit rather than a blocking bug: no in-repo code passes the constant positionally, the SDK's own behavior is correct, and the misuse requires a particular (if well-advertised) user choice.\n\nHow to fix. Reverse the array to newest-first (['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']) so it matches the SUPPORTED_PROTOCOL_VERSIONS convention and is safe to pass verbatim — this is zero-risk since the SDK only uses it for membership checks. Alternatively (or additionally), add a JSDoc note that the constant is a membership set and not suitable as a preference-ordered supportedProtocolVersions value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant