feat(core,client,server): stateful protocol version constants; initialize negotiates stateful versions only#2235
Conversation
🦋 Changeset detectedLatest commit: 38f2f09 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
d5a52bb to
9306136
Compare
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.
9306136 to
57aa16c
Compare
…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.
57aa16c to
38f2f09
Compare
There was a problem hiding this comment.
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.
Adds
STATEFUL_PROTOCOL_VERSIONS+DRAFT_PROTOCOL_VERSIONSand restricts theinitializehandshake to known stateful versions: revisions after 2025-11-25 never negotiate viainitialize, 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 insupportedProtocolVersionsgets 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
supportedProtocolVersionsentries outsideSTATEFUL_PROTOCOL_VERSIONSno longer participate in theinitializehandshake (custom/future strings were previously sent as-is; a list with no stateful version now throws onconnect()). Documented in migration.md; thecustomProtocolVersionexample is updated to the new semantics.Types of changes
Checklist
Additional context
Stateful lists keep the existing
?? LATEST_PROTOCOL_VERSIONlast-resort fallback; the proper version-error answer ships with the envelope PR. Part of #2184.