Skip to content

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

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

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: stateful version constants + lifecycle guards · stacked on #2231

Adds STATEFUL_PROTOCOL_VERSIONS + DRAFT_PROTOCOL_VERSIONS and restricts the initialize handshake to known stateful versions: revisions after 2025-11-25 never negotiate via initialize, in either direction.

Motivation and Context

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

How Has This Been Tested?

Unit tests for all guard corners (client request selection, no-stateful-version rejection, server accept, server fallback, the initialize result). e2e wire tap on all five transports proves no post-2025 version string appears in the handshake, order-agnostic. Full matrix 0 unexpected failures.

Breaking Changes

supportedProtocolVersions entries outside STATEFUL_PROTOCOL_VERSIONS no longer participate in the initialize handshake (custom/future strings were previously sent as-is; a list with no stateful version now throws on connect()). Documented in migration.md; the customProtocolVersion example is updated to the new semantics.

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

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

Stateful lists keep the existing ?? LATEST_PROTOCOL_VERSION last-resort fallback; the proper version-error answer ships with the envelope PR. Part of #2184.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 1, 2026

🦋 Changeset detected

Latest commit: 38f2f09

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: 38f2f09

@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 thread packages/core/src/types/constants.ts Outdated
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.
@felixweinberger felixweinberger force-pushed the fweinberger/m3-draft-version-guards branch from 9306136 to 57aa16c Compare June 2, 2026 08:22
Comment thread packages/server/src/server/server.ts
…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 57aa16c to 38f2f09 Compare June 2, 2026 10:11
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Thanks — the newest-first ordering, migration.md/migration-SKILL.md entries, and the rewritten example all address my earlier comments, and I found no new issues in this revision. Since this changes initialize negotiation semantics and adds new public constants (a declared breaking behavior change), I'll leave final sign-off to a maintainer.

Extended reasoning...

Overview

This PR adds STATEFUL_PROTOCOL_VERSIONS, DRAFT_PROTOCOL_VERSION_2026, and DRAFT_PROTOCOL_VERSIONS constants to packages/core/src/types/constants.ts (exported from the public barrel), and restricts the initialize handshake to known stateful versions on both sides: Client.connect() now requests the first stateful entry of supportedProtocolVersions (throwing if none exists) and rejects a result outside the stateful subset, while Server._oninitialize() accepts/falls back only within the stateful subset. It also touches docs (client.md, server.md, migration.md, migration-SKILL.md), rewrites the customProtocolVersion.ts example to the new semantics, and adds unit and e2e coverage for the new guards.

Resolution of prior feedback

Both issues I raised on earlier revisions have been addressed: STATEFUL_PROTOCOL_VERSIONS is now ordered newest-first (matching the SUPPORTED_PROTOCOL_VERSIONS convention, so passing it verbatim prefers 2025-11-25), and the breaking behavior change is now declared in the PR body, documented in both migration files, and the contradicted example has been rewritten to pin a subset of the stateful versions instead of demonstrating the removed escape hatch. The bug hunting system found no new issues in the current revision.

Security risks

No direct security exposure: the change is to version-negotiation logic, not auth, crypto, or input handling. The only behavioral risk is availability-flavored — clients configured with only custom/future version strings now fail at connect() — and that is intentional, tested, and documented.

Level of scrutiny

This is a deliberate breaking change to a public option's semantics (supportedProtocolVersions) plus three new public exports, framed as part of the M3 milestone toward per-request negotiation for post-2025 revisions. Per the repo's review conventions, new public API surface and behavior changes warrant maintainer judgment on design (e.g., whether the closed hardcoded list and the connect()-throw behavior are the right interim semantics, and whether a patch-level changeset is appropriate for a declared breaking change), so this is not a candidate for shadow approval despite the clean correctness picture.

Other factors

Test coverage is solid: unit tests for client request selection, no-stateful-version rejection, server accept/fallback, the draft-constant pinning against the spec schema, and an order-agnostic e2e wire tap across transports asserting the draft string never appears in the handshake. The implementation is small and consistent between client and server, and the docs/changeset prose now matches the shipped behavior.

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