feat: allow overriding protocol versions in Client, Server, and Transport (#1448)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
92f5ce59ba
commit
160902eb2e
6 changed files with 119 additions and 9 deletions
66
examples/server/src/customProtocolVersion.ts
Normal file
66
examples/server/src/customProtocolVersion.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Example: Custom Protocol Version Support
|
||||
*
|
||||
* This demonstrates how to support protocol versions not yet in the SDK.
|
||||
* First version in the list is used as fallback when client requests
|
||||
* an unsupported version.
|
||||
*
|
||||
* Run with: pnpm tsx src/customProtocolVersion.ts
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createServer } from 'node:http';
|
||||
|
||||
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/server';
|
||||
import { McpServer, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server';
|
||||
|
||||
// Add support for a newer protocol version (first in list is fallback)
|
||||
const CUSTOM_VERSIONS = ['2026-01-01', ...SUPPORTED_PROTOCOL_VERSIONS];
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: 'custom-protocol-server', version: '1.0.0' },
|
||||
{
|
||||
supportedProtocolVersions: CUSTOM_VERSIONS,
|
||||
capabilities: { tools: {} }
|
||||
}
|
||||
);
|
||||
|
||||
// Register a tool that shows the protocol configuration
|
||||
server.registerTool(
|
||||
'get-protocol-info',
|
||||
{
|
||||
title: 'Protocol Info',
|
||||
description: 'Returns protocol version configuration',
|
||||
inputSchema: {}
|
||||
},
|
||||
async (): Promise<CallToolResult> => ({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({ supportedVersions: CUSTOM_VERSIONS }, null, 2)
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
// Create transport - server passes versions automatically during connect()
|
||||
const transport = new NodeStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID()
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
// Simple HTTP server
|
||||
const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000;
|
||||
|
||||
createServer(async (req, res) => {
|
||||
if (req.url === '/mcp') {
|
||||
await transport.handleRequest(req, res);
|
||||
} else {
|
||||
res.writeHead(404).end('Not Found');
|
||||
}
|
||||
}).listen(PORT, () => {
|
||||
console.log(`MCP server with custom protocol versions on port ${PORT}`);
|
||||
console.log(`Supported versions: ${CUSTOM_VERSIONS.join(', ')}`);
|
||||
});
|
||||
|
|
@ -60,8 +60,7 @@ import {
|
|||
mergeCapabilities,
|
||||
Protocol,
|
||||
ReadResourceResultSchema,
|
||||
safeParse,
|
||||
SUPPORTED_PROTOCOL_VERSIONS
|
||||
safeParse
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
|
||||
|
|
@ -476,7 +475,7 @@ export class Client<
|
|||
{
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: LATEST_PROTOCOL_VERSION,
|
||||
protocolVersion: this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION,
|
||||
capabilities: this._capabilities,
|
||||
clientInfo: this._clientInfo
|
||||
}
|
||||
|
|
@ -489,7 +488,7 @@ export class Client<
|
|||
throw new Error(`Server sent invalid initialize result: ${result}`);
|
||||
}
|
||||
|
||||
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
||||
if (!this._supportedProtocolVersions.includes(result.protocolVersion)) {
|
||||
throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import {
|
|||
ListTasksResultSchema,
|
||||
McpError,
|
||||
RELATED_TASK_META_KEY,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
TaskStatusNotificationSchema
|
||||
} from '../types/types.js';
|
||||
import type { AnySchema, SchemaOutput } from '../util/zodCompat.js';
|
||||
|
|
@ -63,6 +64,14 @@ export type ProgressCallback = (progress: Progress) => void;
|
|||
* Additional initialization options.
|
||||
*/
|
||||
export type ProtocolOptions = {
|
||||
/**
|
||||
* Protocol versions supported. First version is preferred (sent by client,
|
||||
* used as fallback by server). Passed to transport during connect().
|
||||
*
|
||||
* @default SUPPORTED_PROTOCOL_VERSIONS
|
||||
*/
|
||||
supportedProtocolVersions?: string[];
|
||||
|
||||
/**
|
||||
* Whether to restrict emitted requests to only those that the remote side has indicated that they can handle, through their advertised capabilities.
|
||||
*
|
||||
|
|
@ -342,6 +351,8 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
|
||||
private _requestResolvers: Map<RequestId, (response: JSONRPCResultResponse | Error) => void> = new Map();
|
||||
|
||||
protected _supportedProtocolVersions: string[];
|
||||
|
||||
/**
|
||||
* Callback for when the connection is closed for any reason.
|
||||
*
|
||||
|
|
@ -367,6 +378,8 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
fallbackNotificationHandler?: (notification: Notification) => Promise<void>;
|
||||
|
||||
constructor(private _options?: ProtocolOptions) {
|
||||
this._supportedProtocolVersions = _options?.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
|
||||
|
||||
this.setNotificationHandler('notifications/cancelled', notification => {
|
||||
this._oncancel(notification);
|
||||
});
|
||||
|
|
@ -633,6 +646,9 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
|
|||
}
|
||||
};
|
||||
|
||||
// Pass supported protocol versions to transport for header validation
|
||||
transport.setSupportedProtocolVersions?.(this._supportedProtocolVersions);
|
||||
|
||||
await this._transport.start();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,4 +125,10 @@ export interface Transport {
|
|||
* Sets the protocol version used for the connection (called when the initialize response is received).
|
||||
*/
|
||||
setProtocolVersion?: (version: string) => void;
|
||||
|
||||
/**
|
||||
* Sets the supported protocol versions for header validation (called during connect).
|
||||
* This allows the server to pass its supported versions to the transport.
|
||||
*/
|
||||
setSupportedProtocolVersions?: (versions: string[]) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ import {
|
|||
McpError,
|
||||
mergeCapabilities,
|
||||
Protocol,
|
||||
safeParse,
|
||||
SUPPORTED_PROTOCOL_VERSIONS
|
||||
safeParse
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import { ExperimentalServerTasks } from '../experimental/tasks/server.js';
|
||||
|
|
@ -435,7 +434,9 @@ export class Server<
|
|||
this._clientCapabilities = request.params.capabilities;
|
||||
this._clientVersion = request.params.clientInfo;
|
||||
|
||||
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION;
|
||||
const protocolVersion = this._supportedProtocolVersions.includes(requestedVersion)
|
||||
? requestedVersion
|
||||
: (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION);
|
||||
|
||||
return {
|
||||
protocolVersion,
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ export interface WebStandardStreamableHTTPServerTransportOptions {
|
|||
* client reconnection timing for polling behavior.
|
||||
*/
|
||||
retryInterval?: number;
|
||||
|
||||
/**
|
||||
* List of protocol versions that this transport will accept.
|
||||
* Used to validate the mcp-protocol-version header in incoming requests.
|
||||
*
|
||||
* Note: When using Server.connect(), the server automatically passes its
|
||||
* supportedProtocolVersions to the transport, so you typically don't need
|
||||
* to set this option directly.
|
||||
*
|
||||
* @default SUPPORTED_PROTOCOL_VERSIONS
|
||||
*/
|
||||
supportedProtocolVersions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -220,6 +232,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
private _allowedOrigins?: string[];
|
||||
private _enableDnsRebindingProtection: boolean;
|
||||
private _retryInterval?: number;
|
||||
private _supportedProtocolVersions: string[];
|
||||
|
||||
sessionId?: string;
|
||||
onclose?: () => void;
|
||||
|
|
@ -236,6 +249,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
this._allowedOrigins = options.allowedOrigins;
|
||||
this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
|
||||
this._retryInterval = options.retryInterval;
|
||||
this._supportedProtocolVersions = options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,6 +263,14 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
this._started = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the supported protocol versions for header validation.
|
||||
* Called by the server during connect() to pass its supported versions.
|
||||
*/
|
||||
setSupportedProtocolVersions(versions: string[]): void {
|
||||
this._supportedProtocolVersions = versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a JSON error response
|
||||
*/
|
||||
|
|
@ -848,11 +870,11 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
private validateProtocolVersion(req: Request): Response | undefined {
|
||||
const protocolVersion = req.headers.get('mcp-protocol-version');
|
||||
|
||||
if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
|
||||
if (protocolVersion !== null && !this._supportedProtocolVersions.includes(protocolVersion)) {
|
||||
return this.createJsonErrorResponse(
|
||||
400,
|
||||
-32_000,
|
||||
`Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`
|
||||
`Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${this._supportedProtocolVersions.join(', ')})`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
Loading…
Reference in a new issue