Skip to content

Commit 86276ed

Browse files
feat(server): retain the negotiated protocol version and expose getNegotiatedProtocolVersion() (#2230)
1 parent 71dcc70 commit 86276ed

4 files changed

Lines changed: 133 additions & 2 deletions

File tree

packages/server/src/server/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export type ServerOptions = ProtocolOptions & {
9898
export class Server extends Protocol<ServerContext> {
9999
private _clientCapabilities?: ClientCapabilities;
100100
private _clientVersion?: Implementation;
101+
private _negotiatedProtocolVersion?: string;
101102
private _capabilities: ServerCapabilities;
102103
private _instructions?: string;
103104
private _jsonSchemaValidator: jsonSchemaValidator;
@@ -428,6 +429,7 @@ export class Server extends Protocol<ServerContext> {
428429
? requestedVersion
429430
: (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION);
430431

432+
this._negotiatedProtocolVersion = protocolVersion;
431433
this.transport?.setProtocolVersion?.(protocolVersion);
432434

433435
return {
@@ -452,6 +454,15 @@ export class Server extends Protocol<ServerContext> {
452454
return this._clientVersion;
453455
}
454456

457+
/**
458+
* After initialization has completed, this will be populated with the protocol version negotiated
459+
* with the client (the version the server responded with during the initialize handshake), or
460+
* `undefined` before initialization.
461+
*/
462+
getNegotiatedProtocolVersion(): string | undefined {
463+
return this._negotiatedProtocolVersion;
464+
}
465+
455466
/**
456467
* Returns the current server capabilities.
457468
*/

packages/server/test/server/server.test.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
1-
import type { JSONRPCMessage } from '@modelcontextprotocol/core';
2-
import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core';
1+
import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core';
2+
import {
3+
InitializeResultSchema,
4+
InMemoryTransport,
5+
isJSONRPCResultResponse,
6+
LATEST_PROTOCOL_VERSION,
7+
SUPPORTED_PROTOCOL_VERSIONS
8+
} from '@modelcontextprotocol/core';
39
import { Server } from '../../src/server/server.js';
410

11+
/** An older protocol version the server supports out of the box. */
12+
const OLDER_SUPPORTED_VERSION = '2025-03-26';
13+
14+
/** A protocol version the server does not support. */
15+
const UNSUPPORTED_VERSION = '1999-01-01';
16+
17+
/**
18+
* Connects the server to a fresh linked in-memory transport pair and drives the
19+
* initialize handshake from the client side, requesting `requestedVersion`.
20+
* Returns the protocol version the server responded with.
21+
*/
22+
async function initializeServer(server: Server, requestedVersion: string): Promise<string> {
23+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
24+
25+
await server.connect(serverTransport);
26+
27+
const responsePromise = new Promise<JSONRPCMessage>(resolve => {
28+
clientTransport.onmessage = msg => resolve(msg);
29+
});
30+
await clientTransport.start();
31+
32+
const initializeRequest: JSONRPCRequest = {
33+
jsonrpc: '2.0',
34+
id: 1,
35+
method: 'initialize',
36+
params: {
37+
protocolVersion: requestedVersion,
38+
capabilities: {},
39+
clientInfo: { name: 'test-client', version: '1.0.0' }
40+
}
41+
};
42+
await clientTransport.send(initializeRequest);
43+
44+
const response = await responsePromise;
45+
if (!isJSONRPCResultResponse(response)) {
46+
throw new Error(`Expected a result response to initialize, got: ${JSON.stringify(response)}`);
47+
}
48+
return InitializeResultSchema.parse(response.result).protocolVersion;
49+
}
50+
551
describe('Server', () => {
652
describe('_oninitialize', () => {
753
it('should propagate negotiated protocol version to transport', async () => {
@@ -39,4 +85,49 @@ describe('Server', () => {
3985
await server.close();
4086
});
4187
});
88+
89+
describe('getNegotiatedProtocolVersion', () => {
90+
it('returns undefined before initialization', () => {
91+
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
92+
93+
expect(server.getNegotiatedProtocolVersion()).toBeUndefined();
94+
});
95+
96+
it('returns the requested version after initialize when the server supports it', async () => {
97+
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
98+
99+
const respondedVersion = await initializeServer(server, LATEST_PROTOCOL_VERSION);
100+
101+
expect(respondedVersion).toBe(LATEST_PROTOCOL_VERSION);
102+
expect(server.getNegotiatedProtocolVersion()).toBe(LATEST_PROTOCOL_VERSION);
103+
104+
await server.close();
105+
});
106+
107+
it('returns the older version when the client requests an older supported version', async () => {
108+
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(OLDER_SUPPORTED_VERSION);
109+
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
110+
111+
const respondedVersion = await initializeServer(server, OLDER_SUPPORTED_VERSION);
112+
113+
expect(respondedVersion).toBe(OLDER_SUPPORTED_VERSION);
114+
expect(server.getNegotiatedProtocolVersion()).toBe(OLDER_SUPPORTED_VERSION);
115+
116+
await server.close();
117+
});
118+
119+
it('returns the fallback version when the client requests an unsupported version', async () => {
120+
expect(SUPPORTED_PROTOCOL_VERSIONS).not.toContain(UNSUPPORTED_VERSION);
121+
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
122+
123+
const respondedVersion = await initializeServer(server, UNSUPPORTED_VERSION);
124+
125+
// The server falls back to its latest supported version and the getter reflects
126+
// the version it actually responded with, not the one the client asked for.
127+
expect(respondedVersion).toBe(LATEST_PROTOCOL_VERSION);
128+
expect(server.getNegotiatedProtocolVersion()).toBe(LATEST_PROTOCOL_VERSION);
129+
130+
await server.close();
131+
});
132+
});
42133
});

test/e2e/requirements.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ export const REQUIREMENTS: Record<string, Requirement> = {
107107
transports: STATEFUL_TRANSPORTS,
108108
note: 'Under stateless hosting each request is served by a new server instance, so state set up earlier in the session cannot be observed.'
109109
},
110+
'typescript:server:get-negotiated-protocol-version': {
111+
source: 'sdk',
112+
behavior:
113+
'After initialize, Server.getNegotiatedProtocolVersion() returns the protocol version the server responded with; before initialize it returns undefined. Matches the Client-side getter.',
114+
transports: STATEFUL_TRANSPORTS,
115+
note: 'Under stateless hosting each request is served by a new server instance, so state set up earlier in the session cannot be observed.'
116+
},
110117

111118
// Protocol primitives: cancellation, timeout, progress, errors, _meta
112119

test/e2e/scenarios/lifecycle.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,28 @@ verifies('typescript:server:get-client-capabilities', async ({ transport }: Test
471471
expect(observed?.second).toBe(observed?.after);
472472
});
473473

474+
verifies('typescript:server:get-negotiated-protocol-version', async ({ transport }: TestArgs) => {
475+
let observed: { before: string | undefined; after: string | undefined } | undefined;
476+
const makeServer = () => {
477+
const s = minimalServer();
478+
const before = s.server.getNegotiatedProtocolVersion();
479+
s.server.oninitialized = () => {
480+
observed = { before, after: s.server.getNegotiatedProtocolVersion() };
481+
};
482+
return s;
483+
};
484+
// Pin the client to an older supported version so the negotiated version differs from the
485+
// server's default — proving the getter reports the actually-negotiated version, not a constant.
486+
const client = new Client({ name: 'version-probe-client', version: '0.0.0' }, { supportedProtocolVersions: [OLDER_SUPPORTED_VERSION] });
487+
488+
await using _ = await wire(transport, makeServer, client);
489+
490+
expect(observed?.before).toBeUndefined();
491+
expect(observed?.after).toBe(OLDER_SUPPORTED_VERSION);
492+
// Parity with the client-side getter: both ends agree on the negotiated version.
493+
expect(client.getNegotiatedProtocolVersion()).toBe(OLDER_SUPPORTED_VERSION);
494+
});
495+
474496
verifies('lifecycle:version:custom-supported-versions', async ({ transport }: TestArgs) => {
475497
// The server's first entry is the latest version, so the older negotiated version can only come from honoring the client's request.
476498
const makeServer = () =>

0 commit comments

Comments
 (0)