feat(server): retain the negotiated protocol version and expose getNegotiatedProtocolVersion()#2230
Conversation
…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.
|
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
…e-initialization test
| getNegotiatedProtocolVersion(): string | undefined { | ||
| return this._negotiatedProtocolVersion; | ||
| } |
There was a problem hiding this comment.
note: this is equivalent to what Client does:
| this._clientCapabilities = request.params.capabilities; | ||
| this._clientVersion = request.params.clientInfo; | ||
|
|
||
| const protocolVersion = this._supportedProtocolVersions.includes(requestedVersion) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Motivation and Context
Today
Server._oninitializecomputes the negotiated protocol version, sends it to the client, and then discards it — it callsthis.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, whileClienthas retained this viagetNegotiatedProtocolVersion()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?
@modelcontextprotocol/server: undefined before initialize; the negotiated version after initialize (requested-version-supported and fallback cases); re-initialization behavior matching the existing_clientCapabilitieslifecycle.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.Breaking Changes
None. Additive only.
Types of changes
Checklist
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.