Skip to content

feat(server): retain the negotiated protocol version and expose getNegotiatedProtocolVersion()#2230

Merged
felixweinberger merged 4 commits into
mainfrom
fweinberger/m2-server-negotiated-version
Jun 1, 2026
Merged

feat(server): retain the negotiated protocol version and expose getNegotiatedProtocolVersion()#2230
felixweinberger merged 4 commits into
mainfrom
fweinberger/m2-server-negotiated-version

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger commented Jun 1, 2026

🗺️ 2026-07-28 Spec Implementation — milestone tracker · This PR: M2 (1/2): Server retains the negotiated protocol version
The server now retains the protocol version it negotiates at initialize and exposes it via getNegotiatedProtocolVersion(), matching the client-side getter.

Motivation and Context

Today Server._oninitialize computes the negotiated protocol version, sends it to the client, and then discards it — it calls this.transport?.setProtocolVersion?.(), which no server transport implements, and stores nothing. As a result, server code (and anything built on top of it) cannot know which protocol version a connection is operating under, while Client has retained this via getNegotiatedProtocolVersion() all along.

This closes that gap. It is also the first step of the per-request envelope work for the 2026-07-28 spec release (#2184): exposing the governing protocol version on the request handler context requires the server to actually know it.

How Has This Been Tested?

  • 5 new unit tests in @modelcontextprotocol/server: undefined before initialize; the negotiated version after initialize (requested-version-supported and fallback cases); re-initialization behavior matching the existing _clientCapabilities lifecycle.
  • 1 new e2e requirement (typescript:server:get-negotiated-protocol-version) with a scenario asserting the getter returns the actually-negotiated version (including a client pinned to an older supported version), green on all transports.
  • Full e2e matrix: 0 unexpected failures. Typecheck/lint clean.

Breaking Changes

None. Additive only.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

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

Part of the version-negotiation groundwork for the 2026-07-28 spec implementation (tracking: #2184). The follow-up PR exposes the version (and per-request client facts) on the request handler context.

…gotiatedProtocolVersion()

Server._oninitialize computed the negotiated protocol version, passed it to
the transport, and discarded it - server code had no way to learn which
protocol version was negotiated, while Client already retains it and exposes
getNegotiatedProtocolVersion().

Store the negotiated version on the Server and add a matching public getter.
It returns undefined before initialization and follows the same lifecycle as
getClientCapabilities()/getClientVersion(): set on each initialize handshake,
overwritten by re-initialization on a new connection.
Add the typescript:server:get-negotiated-protocol-version requirement
(stateful transports only, like the other server-state accessors) and a
lifecycle scenario: undefined before initialize, and after initialize it
reports the version the server actually negotiated - the client pins an
older supported version so the negotiated version differs from the
server's default.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 1, 2026

⚠️ No Changeset found

Latest commit: a4d98c5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a 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@2230

@modelcontextprotocol/codemod

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: a4d98c5

Comment on lines +462 to +464
getNegotiatedProtocolVersion(): string | undefined {
return this._negotiatedProtocolVersion;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

note: this is equivalent to what Client does:

return this._negotiatedProtocolVersion;

@felixweinberger felixweinberger linked an issue Jun 1, 2026 that may be closed by this pull request
@felixweinberger felixweinberger marked this pull request as ready for review June 1, 2026 15:25
@felixweinberger felixweinberger requested a review from a team as a code owner June 1, 2026 15:25
this._clientCapabilities = request.params.capabilities;
this._clientVersion = request.params.clientInfo;

const protocolVersion = this._supportedProtocolVersions.includes(requestedVersion)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the idea here is that you would say initialize with some newer version, the server doesn't support it and responds instead with its latest supported version, and then the client either goes with that or just doesn't send the next message if it doesn't support that message is that right?

checking my understanding because I was initially thinking there should be an error if requestedVersion wasn't in supported, but I realized this is the negotiation stage, not the "enforcement" stage.

Copy link
Copy Markdown
Contributor Author

@felixweinberger felixweinberger Jun 1, 2026

Choose a reason for hiding this comment

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

Yes that's also my understanding of how negotiation is supposed to work - this PR doesn't change that yet to support 2026, instead it just makes it possible that we can even inspect whatever was negotiated on server right now.

The actual negotiation logic is upcoming in a later milestone, see this sequence: https://gist.github.com/felixweinberger/722c541d7ed89069598ffb6ce64992e0

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.

LGTM — small additive getter mirroring the existing client-side getNegotiatedProtocolVersion(), with solid unit and e2e coverage.

Extended reasoning...

Overview

This PR adds a single private field (_negotiatedProtocolVersion) on Server, populates it in _oninitialize right where the negotiated version is already computed, and exposes it via a new getNegotiatedProtocolVersion() getter that mirrors the existing client-side getter (packages/client/src/client/client.ts:562) in both shape and JSDoc. The remaining changes are tests: four new unit tests in packages/server/test/server/server.test.ts (undefined before init, requested-version-supported, older-supported, and unsupported-fallback cases) plus one new e2e requirement and scenario in test/e2e that pins the client to an older supported version and asserts both ends agree on the negotiated version.

Security risks

None. The change stores and returns a string the server already computes and sends to the client during the initialize handshake; there is no new input parsing, auth surface, or data exposure beyond what is already on the wire.

Level of scrutiny

Moderate-low. It does add one method to the public Server API, which normally warrants maintainer judgment, but the addition is symmetric with an existing Client API, has a concrete consumer (the per-request envelope work for the 2026-07-28 spec, #2184), and the lifecycle of the new field exactly matches the existing _clientCapabilities/_clientVersion pattern in the same method. There are no behavior changes to existing code paths — the line is inserted before the existing transport?.setProtocolVersion?.() call without altering it.

Other factors

The bug-hunting system found no issues, and I found none on inspection. Test coverage is thorough for the size of the change (unit tests drive the raw initialize handshake over InMemoryTransport and parse the result against InitializeResultSchema; the e2e scenario verifies the negotiated value differs from the default to prove it is not a constant). The changeset bot has already flagged the missing changeset, so the maintainer can decide whether to add one here or with the follow-up PR; the PR description's mention of a fifth re-initialization unit test reflects a test that was dropped in a later commit, which is a cosmetic description drift only.

@felixweinberger felixweinberger merged commit 86276ed into main Jun 1, 2026
25 checks passed
@felixweinberger felixweinberger deleted the fweinberger/m2-server-negotiated-version branch June 1, 2026 15:56
@claude claude Bot mentioned this pull request Jun 1, 2026
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.

Implement SEP-2575: Make MCP Stateless

2 participants