feat(server): retain the negotiated protocol version and expose getNegotiatedProtocolVersion() (#2230)
This commit is contained in:
parent
71dcc70411
commit
86276ed073
4 changed files with 133 additions and 2 deletions
|
|
@ -98,6 +98,7 @@ export type ServerOptions = ProtocolOptions & {
|
|||
export class Server extends Protocol<ServerContext> {
|
||||
private _clientCapabilities?: ClientCapabilities;
|
||||
private _clientVersion?: Implementation;
|
||||
private _negotiatedProtocolVersion?: string;
|
||||
private _capabilities: ServerCapabilities;
|
||||
private _instructions?: string;
|
||||
private _jsonSchemaValidator: jsonSchemaValidator;
|
||||
|
|
@ -428,6 +429,7 @@ export class Server extends Protocol<ServerContext> {
|
|||
? requestedVersion
|
||||
: (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION);
|
||||
|
||||
this._negotiatedProtocolVersion = protocolVersion;
|
||||
this.transport?.setProtocolVersion?.(protocolVersion);
|
||||
|
||||
return {
|
||||
|
|
@ -452,6 +454,15 @@ export class Server extends Protocol<ServerContext> {
|
|||
return this._clientVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* After initialization has completed, this will be populated with the protocol version negotiated
|
||||
* with the client (the version the server responded with during the initialize handshake), or
|
||||
* `undefined` before initialization.
|
||||
*/
|
||||
getNegotiatedProtocolVersion(): string | undefined {
|
||||
return this._negotiatedProtocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current server capabilities.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,53 @@
|
|||
import type { JSONRPCMessage } from '@modelcontextprotocol/core';
|
||||
import { InMemoryTransport, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core';
|
||||
import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core';
|
||||
import {
|
||||
InitializeResultSchema,
|
||||
InMemoryTransport,
|
||||
isJSONRPCResultResponse,
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
SUPPORTED_PROTOCOL_VERSIONS
|
||||
} from '@modelcontextprotocol/core';
|
||||
import { Server } from '../../src/server/server.js';
|
||||
|
||||
/** An older protocol version the server supports out of the box. */
|
||||
const OLDER_SUPPORTED_VERSION = '2025-03-26';
|
||||
|
||||
/** A protocol version the server does not support. */
|
||||
const UNSUPPORTED_VERSION = '1999-01-01';
|
||||
|
||||
/**
|
||||
* Connects the server to a fresh linked in-memory transport pair and drives the
|
||||
* initialize handshake from the client side, requesting `requestedVersion`.
|
||||
* Returns the protocol version the server responded with.
|
||||
*/
|
||||
async function initializeServer(server: Server, requestedVersion: string): Promise<string> {
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await server.connect(serverTransport);
|
||||
|
||||
const responsePromise = new Promise<JSONRPCMessage>(resolve => {
|
||||
clientTransport.onmessage = msg => resolve(msg);
|
||||
});
|
||||
await clientTransport.start();
|
||||
|
||||
const initializeRequest: JSONRPCRequest = {
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: requestedVersion,
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test-client', version: '1.0.0' }
|
||||
}
|
||||
};
|
||||
await clientTransport.send(initializeRequest);
|
||||
|
||||
const response = await responsePromise;
|
||||
if (!isJSONRPCResultResponse(response)) {
|
||||
throw new Error(`Expected a result response to initialize, got: ${JSON.stringify(response)}`);
|
||||
}
|
||||
return InitializeResultSchema.parse(response.result).protocolVersion;
|
||||
}
|
||||
|
||||
describe('Server', () => {
|
||||
describe('_oninitialize', () => {
|
||||
it('should propagate negotiated protocol version to transport', async () => {
|
||||
|
|
@ -39,4 +85,49 @@ describe('Server', () => {
|
|||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNegotiatedProtocolVersion', () => {
|
||||
it('returns undefined before initialization', () => {
|
||||
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
|
||||
|
||||
expect(server.getNegotiatedProtocolVersion()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the requested version after initialize when the server supports it', async () => {
|
||||
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
|
||||
|
||||
const respondedVersion = await initializeServer(server, LATEST_PROTOCOL_VERSION);
|
||||
|
||||
expect(respondedVersion).toBe(LATEST_PROTOCOL_VERSION);
|
||||
expect(server.getNegotiatedProtocolVersion()).toBe(LATEST_PROTOCOL_VERSION);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('returns the older version when the client requests an older supported version', async () => {
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).toContain(OLDER_SUPPORTED_VERSION);
|
||||
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
|
||||
|
||||
const respondedVersion = await initializeServer(server, OLDER_SUPPORTED_VERSION);
|
||||
|
||||
expect(respondedVersion).toBe(OLDER_SUPPORTED_VERSION);
|
||||
expect(server.getNegotiatedProtocolVersion()).toBe(OLDER_SUPPORTED_VERSION);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('returns the fallback version when the client requests an unsupported version', async () => {
|
||||
expect(SUPPORTED_PROTOCOL_VERSIONS).not.toContain(UNSUPPORTED_VERSION);
|
||||
const server = new Server({ name: 'test', version: '1.0.0' }, { capabilities: {} });
|
||||
|
||||
const respondedVersion = await initializeServer(server, UNSUPPORTED_VERSION);
|
||||
|
||||
// The server falls back to its latest supported version and the getter reflects
|
||||
// the version it actually responded with, not the one the client asked for.
|
||||
expect(respondedVersion).toBe(LATEST_PROTOCOL_VERSION);
|
||||
expect(server.getNegotiatedProtocolVersion()).toBe(LATEST_PROTOCOL_VERSION);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@ export const REQUIREMENTS: Record<string, Requirement> = {
|
|||
transports: STATEFUL_TRANSPORTS,
|
||||
note: 'Under stateless hosting each request is served by a new server instance, so state set up earlier in the session cannot be observed.'
|
||||
},
|
||||
'typescript:server:get-negotiated-protocol-version': {
|
||||
source: 'sdk',
|
||||
behavior:
|
||||
'After initialize, Server.getNegotiatedProtocolVersion() returns the protocol version the server responded with; before initialize it returns undefined. Matches the Client-side getter.',
|
||||
transports: STATEFUL_TRANSPORTS,
|
||||
note: 'Under stateless hosting each request is served by a new server instance, so state set up earlier in the session cannot be observed.'
|
||||
},
|
||||
|
||||
// Protocol primitives: cancellation, timeout, progress, errors, _meta
|
||||
|
||||
|
|
|
|||
|
|
@ -471,6 +471,28 @@ verifies('typescript:server:get-client-capabilities', async ({ transport }: Test
|
|||
expect(observed?.second).toBe(observed?.after);
|
||||
});
|
||||
|
||||
verifies('typescript:server:get-negotiated-protocol-version', async ({ transport }: TestArgs) => {
|
||||
let observed: { before: string | undefined; after: string | undefined } | undefined;
|
||||
const makeServer = () => {
|
||||
const s = minimalServer();
|
||||
const before = s.server.getNegotiatedProtocolVersion();
|
||||
s.server.oninitialized = () => {
|
||||
observed = { before, after: s.server.getNegotiatedProtocolVersion() };
|
||||
};
|
||||
return s;
|
||||
};
|
||||
// Pin the client to an older supported version so the negotiated version differs from the
|
||||
// server's default — proving the getter reports the actually-negotiated version, not a constant.
|
||||
const client = new Client({ name: 'version-probe-client', version: '0.0.0' }, { supportedProtocolVersions: [OLDER_SUPPORTED_VERSION] });
|
||||
|
||||
await using _ = await wire(transport, makeServer, client);
|
||||
|
||||
expect(observed?.before).toBeUndefined();
|
||||
expect(observed?.after).toBe(OLDER_SUPPORTED_VERSION);
|
||||
// Parity with the client-side getter: both ends agree on the negotiated version.
|
||||
expect(client.getNegotiatedProtocolVersion()).toBe(OLDER_SUPPORTED_VERSION);
|
||||
});
|
||||
|
||||
verifies('lifecycle:version:custom-supported-versions', async ({ transport }: TestArgs) => {
|
||||
// The server's first entry is the latest version, so the older negotiated version can only come from honoring the client's request.
|
||||
const makeServer = () =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue