fix: replace schema-based handlers with string method names (#1446)

This commit is contained in:
Matt 2026-02-02 23:57:07 +00:00 • committed by GitHub
commit f6e8204b86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 490 additions and 878 deletions

View file

@ -148,19 +148,19 @@ When a request arrives from the remote side:
- Looks up handler in `_requestHandlers` map (keyed by method name)
- Creates `RequestHandlerExtra` with `signal`, `sessionId`, `sendNotification`, `sendRequest`
- Invokes handler, sends JSON-RPC response back via transport
4. **Handler** was registered via `setRequestHandler(Schema, handler)`
4. **Handler** was registered via `setRequestHandler('method', handler)`
### Handler Registration
```typescript
// In Client (for server→client requests like sampling, elicitation)
client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
client.setRequestHandler('sampling/createMessage', async (request, extra) => {
// Handle sampling request from server
return { role: "assistant", content: {...}, model: "..." };
});
// In Server (for client→server requests like tools/call)
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
server.setRequestHandler('tools/call', async (request, extra) => {
// Handle tool call from client
return { content: [...] };
});
@ -207,7 +207,7 @@ const result = await server.createMessage({
});
// Client must have registered handler:
client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
client.setRequestHandler('sampling/createMessage', async (request, extra) => {
// Client-side LLM call
return { role: "assistant", content: {...} };
});
@ -218,7 +218,7 @@ client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => {
### Request Handler Registration (Low-Level Server)
```typescript
server.setRequestHandler(SomeRequestSchema, async (request, extra) => {
server.setRequestHandler('tools/call', async (request, extra) => {
// extra contains sessionId, authInfo, sendNotification, etc.
return {
/* result */

View file

@ -198,11 +198,50 @@ app.use(hostHeaderValidation(['example.com']));
The server package now exports framework-agnostic alternatives: `validateHostHeader()`, `localhostAllowedHostnames()`, `hostHeaderValidationResponse()`.
## 9. Client Behavioral Changes
## 9. `setRequestHandler` / `setNotificationHandler` API
The low-level handler registration methods now take a method string instead of a Zod schema.
```typescript
// v1: schema-based
server.setRequestHandler(InitializeRequestSchema, async (request) => { ... });
server.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { ... });
// v2: method string
server.setRequestHandler('initialize', async (request) => { ... });
server.setNotificationHandler('notifications/message', (notification) => { ... });
```
Schema to method string mapping:
| v1 Schema | v2 Method String |
|-----------|-----------------|
| `InitializeRequestSchema` | `'initialize'` |
| `CallToolRequestSchema` | `'tools/call'` |
| `ListToolsRequestSchema` | `'tools/list'` |
| `ListPromptsRequestSchema` | `'prompts/list'` |
| `GetPromptRequestSchema` | `'prompts/get'` |
| `ListResourcesRequestSchema` | `'resources/list'` |
| `ReadResourceRequestSchema` | `'resources/read'` |
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
| `ElicitRequestSchema` | `'elicitation/create'` |
| `SetLevelRequestSchema` | `'logging/setLevel'` |
| `PingRequestSchema` | `'ping'` |
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
| `ProgressNotificationSchema` | `'notifications/progress'` |
| `CancelledNotificationSchema` | `'notifications/cancelled'` |
| `InitializedNotificationSchema` | `'notifications/initialized'` |
Request/notification params remain fully typed. Remove unused schema imports after migration.
## 10. Client Behavioral Changes
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.
## 10. Migration Steps (apply in this order)
## 11. Migration Steps (apply in this order)
1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages
2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport`

View file

@ -237,6 +237,66 @@ app.use(hostHeaderValidation(['example.com']));
Note: the v2 signature takes a plain `string[]` instead of an options object.
### `setRequestHandler` and `setNotificationHandler` use method strings
The low-level `setRequestHandler` and `setNotificationHandler` methods on `Client`, `Server`, and `Protocol` now take a method string instead of a Zod schema.
**Before (v1):**
```typescript
import { Server, InitializeRequestSchema, LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/server/index.js';
const server = new Server({ name: 'my-server', version: '1.0.0' });
// Request handler with schema
server.setRequestHandler(InitializeRequestSchema, async (request) => {
return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
});
// Notification handler with schema
server.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
console.log(notification.params.data);
});
```
**After (v2):**
```typescript
import { Server } from '@modelcontextprotocol/server';
const server = new Server({ name: 'my-server', version: '1.0.0' });
// Request handler with method string
server.setRequestHandler('initialize', async (request) => {
return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
});
// Notification handler with method string
server.setNotificationHandler('notifications/message', (notification) => {
console.log(notification.params.data);
});
```
The request and notification parameters remain fully typed via `RequestTypeMap` and `NotificationTypeMap`. You no longer need to import the individual `*RequestSchema` or `*NotificationSchema` constants for handler registration.
Common method string replacements:
| Schema (v1) | Method string (v2) |
|-------------|-------------------|
| `InitializeRequestSchema` | `'initialize'` |
| `CallToolRequestSchema` | `'tools/call'` |
| `ListToolsRequestSchema` | `'tools/list'` |
| `ListPromptsRequestSchema` | `'prompts/list'` |
| `GetPromptRequestSchema` | `'prompts/get'` |
| `ListResourcesRequestSchema` | `'resources/list'` |
| `ReadResourceRequestSchema` | `'resources/read'` |
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
| `ElicitRequestSchema` | `'elicitation/create'` |
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
### Client list methods return empty results for missing capabilities
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, and `listTools()` now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation.

View file

@ -21,8 +21,6 @@ import type {
import {
CallToolResultSchema,
Client,
ElicitationCompleteNotificationSchema,
ElicitRequestSchema,
ErrorCode,
getDisplayName,
ListToolsResultSchema,
@ -560,10 +558,10 @@ async function connect(url?: string): Promise<void> {
console.log('👤 Client created');
// Set up elicitation request handler with proper validation
client.setRequestHandler(ElicitRequestSchema, elicitationRequestHandler);
client.setRequestHandler('elicitation/create', elicitationRequestHandler);
// Set up notification handler for elicitation completion
client.setNotificationHandler(ElicitationCompleteNotificationSchema, notification => {
client.setNotificationHandler('notifications/elicitation/complete', notification => {
const { elicitationId } = notification.params;
const pending = pendingURLElicitations.get(elicitationId);
if (pending) {

View file

@ -1,10 +1,5 @@
import type { CallToolRequest, CallToolResult } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
LoggingMessageNotificationSchema,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { CallToolResultSchema, Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Multiple Clients MCP Example
@ -42,7 +37,7 @@ async function createAndRunClient(config: ClientConfig): Promise<{ id: string; r
};
// Set up client-specific notification handler
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
console.log(`[${config.id}] Notification: ${notification.params.data}`);
});

View file

@ -1,11 +1,5 @@
import type { CallToolResult, ListToolsRequest } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { CallToolResultSchema, Client, ListToolsResultSchema, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Parallel Tool Calls MCP Client
@ -44,7 +38,7 @@ async function main(): Promise<void> {
console.log('Successfully connected to MCP server');
// Set up notification handler with caller identification
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
console.log(`Notification: ${notification.params.data}`);
});

View file

@ -12,18 +12,15 @@ import type {
import {
CallToolResultSchema,
Client,
ElicitRequestSchema,
ErrorCode,
getDisplayName,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
McpError,
ReadResourceResultSchema,
RELATED_TASK_META_KEY,
ResourceListChangedNotificationSchema,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { Ajv } from 'ajv';
@ -271,7 +268,7 @@ async function connect(url?: string): Promise<void> {
};
// Set up elicitation request handler with proper validation
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
if (request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
@ -496,14 +493,14 @@ async function connect(url?: string): Promise<void> {
});
// Set up notification handlers
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
notificationCount++;
console.log(`\nNotification #${notificationCount}: ${notification.params.level} - ${notification.params.data}`);
// Re-display the prompt
process.stdout.write('> ');
});
client.setNotificationHandler(ResourceListChangedNotificationSchema, async _ => {
client.setNotificationHandler('notifications/resources/list_changed', async _ => {
console.log(`\nResource list changed notification received!`);
try {
if (!client) {

View file

@ -10,15 +10,7 @@
import { createInterface } from 'node:readline';
import type { CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
import {
CallToolResultSchema,
Client,
CreateMessageRequestSchema,
ElicitRequestSchema,
ErrorCode,
McpError,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { CallToolResultSchema, Client, ErrorCode, McpError, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
// Create readline interface for user input
const readline = createInterface({
@ -43,7 +35,7 @@ async function elicitationCallback(params: {
mode?: string;
message: string;
requestedSchema?: object;
}): Promise<{ action: string; content?: Record<string, unknown> }> {
}): Promise<{ action: 'accept' | 'cancel' | 'decline'; content?: Record<string, string | number | boolean | string[]> }> {
console.log(`\n[Elicitation] Server asks: ${params.message}`);
// Simple terminal prompt for y/n
@ -102,7 +94,7 @@ async function run(url: string): Promise<void> {
);
// Set up elicitation request handler
client.setRequestHandler(ElicitRequestSchema, async request => {
client.setRequestHandler('elicitation/create', async request => {
if (request.params.mode && request.params.mode !== 'form') {
throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
@ -110,7 +102,7 @@ async function run(url: string): Promise<void> {
});
// Set up sampling request handler
client.setRequestHandler(CreateMessageRequestSchema, async request => {
client.setRequestHandler('sampling/createMessage', async request => {
return samplingCallback(request.params) as unknown as ReturnType<typeof samplingCallback>;
});

View file

@ -12,12 +12,7 @@
* Run with: pnpm tsx src/ssePollingClient.ts
* Requires: ssePollingExample.ts server running on port 3001
*/
import {
CallToolResultSchema,
Client,
LoggingMessageNotificationSchema,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { CallToolResultSchema, Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const SERVER_URL = 'http://localhost:3001/mcp';
@ -60,7 +55,7 @@ async function main(): Promise<void> {
});
// Set up notification handler to receive progress updates
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
const data = notification.params.data;
console.log(`[Notification] ${data}`);
});

View file

@ -3,7 +3,6 @@ import {
CallToolResultSchema,
Client,
ListToolsResultSchema,
LoggingMessageNotificationSchema,
SSEClientTransport,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
@ -39,7 +38,7 @@ async function main(): Promise<void> {
transport = connection.transport;
// Set up notification handler
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
client.setNotificationHandler('notifications/message', notification => {
console.log(`Notification: ${notification.params.level} - ${notification.params.data}`);
});

View file

@ -35,16 +35,7 @@ import type {
TextContent,
Tool
} from '@modelcontextprotocol/server';
import {
CallToolRequestSchema,
GetTaskPayloadRequestSchema,
GetTaskRequestSchema,
InMemoryTaskStore,
isTerminal,
ListToolsRequestSchema,
RELATED_TASK_META_KEY,
Server
} from '@modelcontextprotocol/server';
import { InMemoryTaskStore, isTerminal, RELATED_TASK_META_KEY, Server } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
// ============================================================================
@ -486,7 +477,7 @@ const createServer = (): Server => {
);
// Register tools
server.setRequestHandler(ListToolsRequestSchema, async (): Promise<{ tools: Tool[] }> => {
server.setRequestHandler('tools/list', async (): Promise<{ tools: Tool[] }> => {
return {
tools: [
{
@ -516,7 +507,7 @@ const createServer = (): Server => {
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
server.setRequestHandler('tools/call', async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
const { name, arguments: args } = request.params;
const taskParams = (request.params._meta?.task || request.params.task) as { ttl?: number; pollInterval?: number } | undefined;
@ -616,7 +607,7 @@ const createServer = (): Server => {
});
// Handle tasks/get
server.setRequestHandler(GetTaskRequestSchema, async (request): Promise<GetTaskResult> => {
server.setRequestHandler('tasks/get', async (request): Promise<GetTaskResult> => {
const { taskId } = request.params;
const task = await taskStore.getTask(taskId);
if (!task) {
@ -626,7 +617,7 @@ const createServer = (): Server => {
});
// Handle tasks/result
server.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra): Promise<GetTaskPayloadResult> => {
server.setRequestHandler('tasks/result', async (request, extra): Promise<GetTaskPayloadResult> => {
const { taskId } = request.params;
console.log(`[Server] tasks/result called for task ${taskId}`);
return taskResultHandler.handle(taskId, server, extra.sessionId ?? '');

View file

@ -1,5 +1,4 @@
import type {
AnyObjectSchema,
CallToolRequest,
ClientCapabilities,
ClientNotification,
@ -20,20 +19,20 @@ import type {
ListToolsRequest,
LoggingLevel,
Notification,
NotificationMethod,
ProtocolOptions,
ReadResourceRequest,
Request,
RequestHandlerExtra,
RequestMethod,
RequestOptions,
RequestTypeMap,
Result,
SchemaOutput,
ServerCapabilities,
SubscribeRequest,
Tool,
Transport,
UnsubscribeRequest,
ZodV3Internal,
ZodV4Internal
UnsubscribeRequest
} from '@modelcontextprotocol/core';
import {
AjvJsonSchemaValidator,
@ -49,10 +48,8 @@ import {
ElicitResultSchema,
EmptyResultSchema,
ErrorCode,
getObjectShape,
GetPromptResultSchema,
InitializeResultSchema,
isZ4Schema,
LATEST_PROTOCOL_VERSION,
ListChangedOptionsBaseSchema,
ListPromptsResultSchema,
@ -61,13 +58,10 @@ import {
ListToolsResultSchema,
McpError,
mergeCapabilities,
PromptListChangedNotificationSchema,
Protocol,
ReadResourceResultSchema,
ResourceListChangedNotificationSchema,
safeParse,
SUPPORTED_PROTOCOL_VERSIONS,
ToolListChangedNotificationSchema
SUPPORTED_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/core';
import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
@ -281,21 +275,21 @@ export class Client<
*/
private _setupListChangedHandlers(config: ListChangedHandlers): void {
if (config.tools && this._serverCapabilities?.tools?.listChanged) {
this._setupListChangedHandler('tools', ToolListChangedNotificationSchema, config.tools, async () => {
this._setupListChangedHandler('tools', 'notifications/tools/list_changed', config.tools, async () => {
const result = await this.listTools();
return result.tools;
});
}
if (config.prompts && this._serverCapabilities?.prompts?.listChanged) {
this._setupListChangedHandler('prompts', PromptListChangedNotificationSchema, config.prompts, async () => {
this._setupListChangedHandler('prompts', 'notifications/prompts/list_changed', config.prompts, async () => {
const result = await this.listPrompts();
return result.prompts;
});
}
if (config.resources && this._serverCapabilities?.resources?.listChanged) {
this._setupListChangedHandler('resources', ResourceListChangedNotificationSchema, config.resources, async () => {
this._setupListChangedHandler('resources', 'notifications/resources/list_changed', config.resources, async () => {
const result = await this.listResources();
return result.resources;
});
@ -334,38 +328,16 @@ export class Client<
/**
* Override request handler registration to enforce client-side validation for elicitation.
*/
public override setRequestHandler<T extends AnyObjectSchema>(
requestSchema: T,
public override setRequestHandler<M extends RequestMethod>(
method: M,
handler: (
request: SchemaOutput<T>,
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
) => ClientResult | ResultT | Promise<ClientResult | ResultT>
): void {
const shape = getObjectShape(requestSchema);
const methodSchema = shape?.method;
if (!methodSchema) {
throw new Error('Schema is missing a method literal');
}
// Extract literal value using type-safe property access
let methodValue: unknown;
if (isZ4Schema(methodSchema)) {
const v4Schema = methodSchema as unknown as ZodV4Internal;
const v4Def = v4Schema._zod?.def;
methodValue = v4Def?.value ?? v4Schema.value;
} else {
const v3Schema = methodSchema as unknown as ZodV3Internal;
const legacyDef = v3Schema._def;
methodValue = legacyDef?.value ?? v3Schema.value;
}
if (typeof methodValue !== 'string') {
throw new TypeError('Schema method literal must be a string');
}
const method = methodValue;
if (method === 'elicitation/create') {
const wrappedHandler = async (
request: SchemaOutput<T>,
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
): Promise<ClientResult | ResultT> => {
const validatedRequest = safeParse(ElicitRequestSchema, request);
@ -433,12 +405,12 @@ export class Client<
};
// Install the wrapped handler
return super.setRequestHandler(requestSchema, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(method, wrappedHandler as unknown as typeof handler);
}
if (method === 'sampling/createMessage') {
const wrappedHandler = async (
request: SchemaOutput<T>,
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ClientRequest | RequestT, ClientNotification | NotificationT>
): Promise<ClientResult | ResultT> => {
const validatedRequest = safeParse(CreateMessageRequestSchema, request);
@ -479,11 +451,11 @@ export class Client<
};
// Install the wrapped handler
return super.setRequestHandler(requestSchema, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(method, wrappedHandler as unknown as typeof handler);
}
// Other handlers use default behavior
return super.setRequestHandler(requestSchema, handler);
return super.setRequestHandler(method, handler);
}
protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
@ -898,7 +870,7 @@ export class Client<
*/
private _setupListChangedHandler<T>(
listType: string,
notificationSchema: { shape: { method: { value: string } } },
notificationMethod: NotificationMethod,
options: ListChangedOptions<T>,
fetcher: () => Promise<T[]>
): void {
@ -949,7 +921,7 @@ export class Client<
};
// Register notification handler
this.setNotificationHandler(notificationSchema as AnyObjectSchema, handler);
this.setNotificationHandler(notificationMethod, handler);
}
async sendRootsListChanged() {

View file

@ -111,7 +111,7 @@ describe('SSEClientTransport', () => {
// Create a server that returns 403
await resourceServer.close();
resourceServer = createServer((req, res) => {
resourceServer = createServer((_req, res) => {
res.writeHead(403);
res.end();
});

View file

@ -14,6 +14,8 @@ import type {
JSONRPCResultResponse,
MessageExtraInfo,
Notification,
NotificationMethod,
NotificationTypeMap,
Progress,
ProgressNotification,
RelatedTaskMetadata,
@ -21,6 +23,8 @@ import type {
RequestId,
RequestInfo,
RequestMeta,
RequestMethod,
RequestTypeMap,
Result,
ServerCapabilities,
Task,
@ -28,30 +32,25 @@ import type {
TaskStatusNotification
} from '../types/types.js';
import {
CancelledNotificationSchema,
CancelTaskRequestSchema,
CancelTaskResultSchema,
CreateTaskResultSchema,
ErrorCode,
GetTaskPayloadRequestSchema,
GetTaskRequestSchema,
getNotificationSchema,
getRequestSchema,
GetTaskResultSchema,
isJSONRPCErrorResponse,
isJSONRPCNotification,
isJSONRPCRequest,
isJSONRPCResultResponse,
isTaskAugmentedRequestParams,
ListTasksRequestSchema,
ListTasksResultSchema,
McpError,
PingRequestSchema,
ProgressNotificationSchema,
RELATED_TASK_META_KEY,
TaskStatusNotificationSchema
} from '../types/types.js';
import type { AnyObjectSchema, AnySchema, SchemaOutput } from '../util/zodCompat.js';
import type { AnySchema, SchemaOutput } from '../util/zodCompat.js';
import { safeParse } from '../util/zodCompat.js';
import { getMethodLiteral, parseWithCompat } from '../util/zodJsonSchemaCompat.js';
import { parseWithCompat } from '../util/zodJsonSchemaCompat.js';
import type { ResponseMessage } from './responseMessage.js';
import type { Transport, TransportSendOptions } from './transport.js';
@ -368,16 +367,16 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
fallbackNotificationHandler?: (notification: Notification) => Promise<void>;
constructor(private _options?: ProtocolOptions) {
this.setNotificationHandler(CancelledNotificationSchema, notification => {
this.setNotificationHandler('notifications/cancelled', notification => {
this._oncancel(notification);
});
this.setNotificationHandler(ProgressNotificationSchema, notification => {
this._onprogress(notification as unknown as ProgressNotification);
this.setNotificationHandler('notifications/progress', notification => {
this._onprogress(notification);
});
this.setRequestHandler(
PingRequestSchema,
'ping',
// Automatic pong by default.
_request => ({}) as SendResultT
);
@ -386,7 +385,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
this._taskStore = _options?.taskStore;
this._taskMessageQueue = _options?.taskMessageQueue;
if (this._taskStore) {
this.setRequestHandler(GetTaskRequestSchema, async (request, extra) => {
this.setRequestHandler('tasks/get', async (request, extra) => {
const task = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
if (!task) {
throw new McpError(ErrorCode.InvalidParams, 'Failed to retrieve task: Task not found');
@ -394,13 +393,12 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
// Per spec: tasks/get responses SHALL NOT include related-task metadata
// as the taskId parameter is the source of truth
// @ts-expect-error SendResultT cannot contain GetTaskResult, but we include it in our derived types everywhere else
return {
...task
} as SendResultT;
} as unknown as SendResultT;
});
this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
this.setRequestHandler('tasks/result', async (request, extra) => {
const handleTaskResult = async (): Promise<SendResultT> => {
const taskId = request.params.taskId;
@ -478,7 +476,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
taskId: taskId
}
}
} as SendResultT;
} as unknown as SendResultT;
}
return await handleTaskResult();
@ -487,15 +485,14 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
return await handleTaskResult();
});
this.setRequestHandler(ListTasksRequestSchema, async (request, extra) => {
this.setRequestHandler('tasks/list', async (request, extra) => {
try {
const { tasks, nextCursor } = await this._taskStore!.listTasks(request.params?.cursor, extra.sessionId);
// @ts-expect-error SendResultT cannot contain ListTasksResult, but we include it in our derived types everywhere else
return {
tasks,
nextCursor,
_meta: {}
} as SendResultT;
} as unknown as SendResultT;
} catch (error) {
throw new McpError(
ErrorCode.InvalidParams,
@ -504,7 +501,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
}
});
this.setRequestHandler(CancelTaskRequestSchema, async (request, extra) => {
this.setRequestHandler('tasks/cancel', async (request, extra) => {
try {
// Get the current task to check if it's in a terminal state, in case the implementation is not atomic
const task = await this._taskStore!.getTask(request.params.taskId, extra.sessionId);
@ -1401,18 +1398,18 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
*
* Note that this will replace any previous request handler for the same method.
*/
setRequestHandler<T extends AnyObjectSchema>(
requestSchema: T,
setRequestHandler<M extends RequestMethod>(
method: M,
handler: (
request: SchemaOutput<T>,
request: RequestTypeMap[M],
extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
) => SendResultT | Promise<SendResultT>
): void {
const method = getMethodLiteral(requestSchema);
this.assertRequestHandlerCapability(method);
const schema = getRequestSchema(method);
this._requestHandlers.set(method, (request, extra) => {
const parsed = parseWithCompat(requestSchema, request) as SchemaOutput<T>;
const parsed = parseWithCompat(schema, request) as RequestTypeMap[M];
return Promise.resolve(handler(parsed, extra));
});
}
@ -1420,14 +1417,14 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
/**
* Removes the request handler for the given method.
*/
removeRequestHandler(method: string): void {
removeRequestHandler(method: RequestMethod): void {
this._requestHandlers.delete(method);
}
/**
* Asserts that a request handler has not already been set for the given method, in preparation for a new one being automatically installed.
*/
assertCanSetRequestHandler(method: string): void {
assertCanSetRequestHandler(method: RequestMethod): void {
if (this._requestHandlers.has(method)) {
throw new Error(`A request handler for ${method} already exists, which would be overridden`);
}
@ -1438,13 +1435,14 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
*
* Note that this will replace any previous notification handler for the same method.
*/
setNotificationHandler<T extends AnyObjectSchema>(
notificationSchema: T,
handler: (notification: SchemaOutput<T>) => void | Promise<void>
setNotificationHandler<M extends NotificationMethod>(
method: M,
handler: (notification: NotificationTypeMap[M]) => void | Promise<void>
): void {
const method = getMethodLiteral(notificationSchema);
const schema = getNotificationSchema(method);
this._notificationHandlers.set(method, notification => {
const parsed = parseWithCompat(notificationSchema, notification) as SchemaOutput<T>;
const parsed = parseWithCompat(schema, notification) as NotificationTypeMap[M];
return Promise.resolve(handler(parsed));
});
}
@ -1452,7 +1450,7 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
/**
* Removes the notification handler for the given method.
*/
removeNotificationHandler(method: string): void {
removeNotificationHandler(method: NotificationMethod): void {
this._notificationHandlers.delete(method);
}

View file

@ -2605,3 +2605,41 @@ export type ClientResult = Infer<typeof ClientResultSchema>;
export type ServerRequest = Infer<typeof ServerRequestSchema>;
export type ServerNotification = Infer<typeof ServerNotificationSchema>;
export type ServerResult = Infer<typeof ServerResultSchema>;
/* Protocol type maps */
type MethodToTypeMap<U> = {
[T in U as T extends { method: infer M extends string } ? M : never]: T;
};
export type RequestMethod = ClientRequest['method'] | ServerRequest['method'];
export type NotificationMethod = ClientNotification['method'] | ServerNotification['method'];
export type RequestTypeMap = MethodToTypeMap<ClientRequest | ServerRequest>;
export type NotificationTypeMap = MethodToTypeMap<ClientNotification | ServerNotification>;
/* Runtime schema lookup */
type RequestSchemaType = (typeof ClientRequestSchema.options)[number] | (typeof ServerRequestSchema.options)[number];
type NotificationSchemaType = (typeof ClientNotificationSchema.options)[number] | (typeof ServerNotificationSchema.options)[number];
function buildSchemaMap<T extends { shape: { method: { value: string } } }>(schemas: readonly T[]): Record<string, T> {
const map: Record<string, T> = {};
for (const schema of schemas) {
const method = schema.shape.method.value;
map[method] = schema;
}
return map;
}
const requestSchemas = buildSchemaMap([...ClientRequestSchema.options, ...ServerRequestSchema.options] as const) as Record<
RequestMethod,
RequestSchemaType
>;
const notificationSchemas = buildSchemaMap([...ClientNotificationSchema.options, ...ServerNotificationSchema.options] as const) as Record<
NotificationMethod,
NotificationSchemaType
>;
export function getRequestSchema<M extends RequestMethod>(method: M) {
return requestSchemas[method];
}
export function getNotificationSchema<M extends NotificationMethod>(method: M) {
return notificationSchemas[method];
}

View file

@ -9,8 +9,8 @@ import type * as z4c from 'zod/v4/core';
import * as z4mini from 'zod/v4-mini';
import { zodToJsonSchema } from 'zod-to-json-schema';
import type { AnyObjectSchema, AnySchema } from './zodCompat.js';
import { getLiteralValue, getObjectShape, isZ4Schema, safeParse } from './zodCompat.js';
import type { AnyObjectSchema, AnySchema, SchemaOutput } from './zodCompat.js';
import { isZ4Schema, safeParse } from './zodCompat.js';
type JsonSchema = Record<string, unknown>;
@ -44,25 +44,10 @@ export function toJsonSchemaCompat(schema: AnyObjectSchema, opts?: CommonOpts):
}) as JsonSchema;
}
export function getMethodLiteral(schema: AnyObjectSchema): string {
const shape = getObjectShape(schema);
const methodSchema = shape?.method as AnySchema | undefined;
if (!methodSchema) {
throw new Error('Schema is missing a method literal');
}
const value = getLiteralValue(methodSchema);
if (typeof value !== 'string') {
throw new TypeError('Schema method literal must be a string');
}
return value;
}
export function parseWithCompat(schema: AnySchema, data: unknown): unknown {
export function parseWithCompat<T extends AnySchema>(schema: T, data: unknown): SchemaOutput<T> {
const result = safeParse(schema, data);
if (!result.success) {
throw result.error;
}
return result.data;
return result.data as SchemaOutput<T>;
}

View file

@ -29,7 +29,7 @@ import type {
Task,
TaskCreationParams
} from '../../src/types/types.js';
import { CallToolRequestSchema, ErrorCode, McpError, RELATED_TASK_META_KEY } from '../../src/types/types.js';
import { ErrorCode, McpError, RELATED_TASK_META_KEY } from '../../src/types/types.js';
// Type helper for accessing private/protected Protocol properties in tests
interface TestProtocol {
@ -1266,13 +1266,13 @@ describe('Task-based execution', () => {
await protocol.connect(transport);
protocol.setRequestHandler(CallToolRequestSchema, async request => {
protocol.setRequestHandler('tools/call', async request => {
// Tool implementor can access task creation parameters from request.params.task
expect(request.params.task).toEqual({
ttl: 60000,
pollInterval: 1000
});
return { result: 'success' };
return { content: [{ type: 'text', text: 'success' }] };
});
transport.onmessage?.({
@ -2009,7 +2009,7 @@ describe('Task-based execution', () => {
await serverProtocol.connect(serverTransport);
// Set up a handler that uses sendRequest and sendNotification
serverProtocol.setRequestHandler(CallToolRequestSchema, async (_request, extra) => {
serverProtocol.setRequestHandler('tools/call', async (_request, extra) => {
// Send a notification using the extra.sendNotification
await extra.sendNotification({
method: 'notifications/message',
@ -2093,15 +2093,11 @@ describe('Request Cancellation vs Task Cancellation', () => {
// Set up a request handler that checks if it was aborted
let wasAborted = false;
const TestRequestSchema = z.object({
method: z.literal('test/longRunning'),
params: z.optional(z.record(z.string(), z.unknown()))
});
protocol.setRequestHandler(TestRequestSchema, async (_request, extra) => {
protocol.setRequestHandler('ping', async (_request, extra) => {
// Simulate a long-running operation
await new Promise(resolve => setTimeout(resolve, 100));
wasAborted = extra.signal.aborted;
return { _meta: {} } as Result;
return {};
});
// Simulate an incoming request
@ -2110,7 +2106,7 @@ describe('Request Cancellation vs Task Cancellation', () => {
transport.onmessage({
jsonrpc: '2.0',
id: requestId,
method: 'test/longRunning',
method: 'ping',
params: {}
});
}
@ -2308,28 +2304,24 @@ describe('Request Cancellation vs Task Cancellation', () => {
// Set up a request handler
let requestCompleted = false;
const TestMethodSchema = z.object({
method: z.literal('test/method'),
params: z.optional(z.record(z.string(), z.unknown()))
});
protocol.setRequestHandler(TestMethodSchema, async () => {
protocol.setRequestHandler('ping', async () => {
await new Promise(resolve => setTimeout(resolve, 50));
requestCompleted = true;
return { _meta: {} } as Result;
return {};
});
// Create a task
// Create a task (simulating a long-running tools/call)
const task = await taskStore.createTask({ ttl: 60000 }, 'req-1', {
method: 'test/method',
params: {}
method: 'tools/call',
params: { name: 'long-running-tool', arguments: {} }
});
// Start a request
// Start an unrelated ping request
if (transport.onmessage) {
transport.onmessage({
jsonrpc: '2.0',
id: 123,
method: 'test/method',
method: 'ping',
params: {}
});
}
@ -2488,7 +2480,7 @@ describe('Progress notification support for tasks', () => {
await protocol.connect(transport);
// Set up a request handler that will complete the task
protocol.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
protocol.setRequestHandler('tools/call', async (_request, extra) => {
if (extra.taskStore) {
const task = await extra.taskStore.createTask({ ttl: 60000 });
@ -3686,17 +3678,8 @@ describe('Message Interception', () => {
await protocol.connect(transport);
// Set up a request handler that returns a result
const TestRequestSchema = z.object({
method: z.literal('test/taskRequest'),
params: z
.object({
_meta: z.optional(z.record(z.string(), z.unknown()))
})
.passthrough()
});
protocol.setRequestHandler(TestRequestSchema, async () => {
return { content: 'test result' } as Result;
protocol.setRequestHandler('ping', async () => {
return {};
});
// Simulate an incoming request with relatedTask metadata
@ -3705,7 +3688,7 @@ describe('Message Interception', () => {
transport.onmessage?.({
jsonrpc: '2.0',
id: requestId,
method: 'test/taskRequest',
method: 'ping',
params: {
_meta: {
'io.modelcontextprotocol/related-task': { taskId }
@ -3725,7 +3708,7 @@ describe('Message Interception', () => {
expect(queuedMessage!.type).toBe('response');
if (queuedMessage!.type === 'response') {
expect(queuedMessage!.message.id).toBe(requestId);
expect(queuedMessage!.message.result).toEqual({ content: 'test result' });
expect(queuedMessage!.message.result).toEqual({});
}
});
@ -3733,16 +3716,7 @@ describe('Message Interception', () => {
await protocol.connect(transport);
// Set up a request handler that throws an error
const TestRequestSchema = z.object({
method: z.literal('test/taskRequestError'),
params: z
.object({
_meta: z.optional(z.record(z.string(), z.unknown()))
})
.passthrough()
});
protocol.setRequestHandler(TestRequestSchema, async () => {
protocol.setRequestHandler('ping', async () => {
throw new McpError(ErrorCode.InternalError, 'Test error message');
});
@ -3752,7 +3726,7 @@ describe('Message Interception', () => {
transport.onmessage?.({
jsonrpc: '2.0',
id: requestId,
method: 'test/taskRequestError',
method: 'ping',
params: {
_meta: {
'io.modelcontextprotocol/related-task': { taskId }
@ -3815,13 +3789,8 @@ describe('Message Interception', () => {
const sendSpy = vi.spyOn(transport, 'send');
// Set up a request handler
const TestRequestSchema = z.object({
method: z.literal('test/normalRequest'),
params: z.optional(z.record(z.string(), z.unknown()))
});
protocol.setRequestHandler(TestRequestSchema, async () => {
return { content: 'normal result' } as Result;
protocol.setRequestHandler('tools/call', async () => {
return { content: [{ type: 'text', text: 'done' }] };
});
// Simulate an incoming request WITHOUT relatedTask metadata
@ -3829,8 +3798,8 @@ describe('Message Interception', () => {
transport.onmessage?.({
jsonrpc: '2.0',
id: requestId,
method: 'test/normalRequest',
params: {}
method: 'tools/call',
params: { name: 'test-tool' }
});
// Wait for the handler to complete
@ -3841,7 +3810,7 @@ describe('Message Interception', () => {
expect.objectContaining({
jsonrpc: '2.0',
id: requestId,
result: { content: 'normal result' }
result: { content: [{ type: 'text', text: 'done' }] }
})
);
});

View file

@ -1,9 +1,8 @@
import { beforeEach, describe, expect, test } from 'vitest';
import * as z from 'zod/v4';
import { Protocol } from '../../src/shared/protocol.js';
import type { Transport } from '../../src/shared/transport.js';
import type { JSONRPCMessage, Notification, Request, Result } from '../../src/types/types.js';
import type { EmptyResult, JSONRPCMessage, Notification, Request, Result } from '../../src/types/types.js';
// Mock Transport class
class MockTransport implements Transport {
@ -48,54 +47,24 @@ describe('Protocol transport handling bug', () => {
test('should send response to the correct transport when multiple clients are connected', async () => {
// Set up a request handler that simulates processing time
let resolveHandler: (value: Result) => void;
const handlerPromise = new Promise<Result>(resolve => {
let resolveHandler: (value: EmptyResult) => void;
const handlerPromise = new Promise<EmptyResult>(resolve => {
resolveHandler = resolve;
});
const TestRequestSchema = z.object({
method: z.literal('test/method'),
params: z
.object({
from: z.string()
})
.optional()
});
protocol.setRequestHandler(TestRequestSchema, async request => {
console.log(`Processing request from ${request.params?.from}`);
return handlerPromise;
});
protocol.setRequestHandler('ping', async () => handlerPromise);
// Client A connects and sends a request
await protocol.connect(transportA);
const requestFromA = {
jsonrpc: '2.0' as const,
method: 'test/method',
params: { from: 'clientA' },
id: 1
};
// Simulate client A sending a request
transportA.onmessage?.(requestFromA);
transportA.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 1 });
// While A's request is being processed, client B connects
// This overwrites the transport reference in the protocol
await protocol.connect(transportB);
const requestFromB = {
jsonrpc: '2.0' as const,
method: 'test/method',
params: { from: 'clientB' },
id: 2
};
// Client B sends its own request
transportB.onmessage?.(requestFromB);
transportB.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 2 });
// Now complete A's request
resolveHandler!({ data: 'responseForA' } as Result);
resolveHandler!({});
// Wait for async operations to complete
await new Promise(resolve => setTimeout(resolve, 10));
@ -104,70 +73,33 @@ describe('Protocol transport handling bug', () => {
console.log('Transport A received:', transportA.sentMessages);
console.log('Transport B received:', transportB.sentMessages);
// FIXED: Each transport now receives its own response
// Transport A should receive response for request ID 1
expect(transportA.sentMessages.length).toBe(1);
expect(transportA.sentMessages[0]).toMatchObject({
jsonrpc: '2.0',
id: 1,
result: { data: 'responseForA' }
});
expect(transportA.sentMessages).toHaveLength(1);
expect(transportA.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 1, result: {} });
// Transport B should only receive its own response (when implemented)
expect(transportB.sentMessages.length).toBe(1);
expect(transportB.sentMessages[0]).toMatchObject({
jsonrpc: '2.0',
id: 2,
result: { data: 'responseForA' } // Same handler result in this test
});
// Transport B should receive response for request ID 2
expect(transportB.sentMessages).toHaveLength(1);
expect(transportB.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 2, result: {} });
});
test('demonstrates the timing issue with multiple rapid connections', async () => {
const delays: number[] = [];
const results: { transport: string; response: JSONRPCMessage[] }[] = [];
const DelayedRequestSchema = z.object({
method: z.literal('test/delayed'),
params: z
.object({
delay: z.number(),
client: z.string()
})
.optional()
});
// Set up handler with variable delay
protocol.setRequestHandler(DelayedRequestSchema, async (request, extra) => {
const delay = request.params?.delay || 0;
delays.push(delay);
// Set up handler with variable delay based on request id
protocol.setRequestHandler('ping', async (_request, extra) => {
const delay = extra.requestId === 1 ? 50 : 10;
await new Promise(resolve => setTimeout(resolve, delay));
return {
processedBy: `handler-${extra.requestId}`,
delay: delay
} as Result;
return {};
});
// Rapid succession of connections and requests
await protocol.connect(transportA);
transportA.onmessage?.({
jsonrpc: '2.0' as const,
method: 'test/delayed',
params: { delay: 50, client: 'A' },
id: 1
});
transportA.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 1 });
// Connect B while A is processing
setTimeout(async () => {
await protocol.connect(transportB);
transportB.onmessage?.({
jsonrpc: '2.0' as const,
method: 'test/delayed',
params: { delay: 10, client: 'B' },
id: 2
});
transportB.onmessage?.({ jsonrpc: '2.0', method: 'ping', id: 2 });
}, 10);
// Wait for all processing
@ -183,8 +115,7 @@ describe('Protocol transport handling bug', () => {
console.log('Timing test results:', results);
// FIXED: Each transport receives its own responses
expect(transportA.sentMessages.length).toBe(1);
expect(transportB.sentMessages.length).toBe(1);
expect(transportA.sentMessages).toHaveLength(1);
expect(transportB.sentMessages).toHaveLength(1);
});
});

View file

@ -36,23 +36,14 @@ import type {
import {
assertCompleteRequestPrompt,
assertCompleteRequestResourceTemplate,
CallToolRequestSchema,
CompleteRequestSchema,
ErrorCode,
getLiteralValue,
getObjectShape,
getParseErrorMessage,
GetPromptRequestSchema,
getSchemaDescription,
isSchemaOptional,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
McpError,
normalizeObjectSchema,
objectFromShape,
ReadResourceRequestSchema,
safeParseAsync,
toJsonSchemaCompat,
UriTemplate,
@ -128,8 +119,8 @@ export class McpServer {
return;
}
this.server.assertCanSetRequestHandler(getMethodValue(ListToolsRequestSchema));
this.server.assertCanSetRequestHandler(getMethodValue(CallToolRequestSchema));
this.server.assertCanSetRequestHandler('tools/list');
this.server.assertCanSetRequestHandler('tools/call');
this.server.registerCapabilities({
tools: {
@ -138,7 +129,7 @@ export class McpServer {
});
this.server.setRequestHandler(
ListToolsRequestSchema,
'tools/list',
(): ListToolsResult => ({
tools: Object.entries(this._registeredTools)
.filter(([, tool]) => tool.enabled)
@ -176,7 +167,7 @@ export class McpServer {
})
);
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
this.server.setRequestHandler('tools/call', async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
try {
const tool = this._registeredTools[request.params.name];
if (!tool) {
@ -404,13 +395,13 @@ export class McpServer {
return;
}
this.server.assertCanSetRequestHandler(getMethodValue(CompleteRequestSchema));
this.server.assertCanSetRequestHandler('completion/complete');
this.server.registerCapabilities({
completions: {}
});
this.server.setRequestHandler(CompleteRequestSchema, async (request): Promise<CompleteResult> => {
this.server.setRequestHandler('completion/complete', async (request): Promise<CompleteResult> => {
switch (request.params.ref.type) {
case 'ref/prompt': {
assertCompleteRequestPrompt(request);
@ -490,9 +481,9 @@ export class McpServer {
return;
}
this.server.assertCanSetRequestHandler(getMethodValue(ListResourcesRequestSchema));
this.server.assertCanSetRequestHandler(getMethodValue(ListResourceTemplatesRequestSchema));
this.server.assertCanSetRequestHandler(getMethodValue(ReadResourceRequestSchema));
this.server.assertCanSetRequestHandler('resources/list');
this.server.assertCanSetRequestHandler('resources/templates/list');
this.server.assertCanSetRequestHandler('resources/read');
this.server.registerCapabilities({
resources: {
@ -500,7 +491,7 @@ export class McpServer {
}
});
this.server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => {
this.server.setRequestHandler('resources/list', async (_request, extra) => {
const resources = Object.entries(this._registeredResources)
.filter(([_, resource]) => resource.enabled)
.map(([uri, resource]) => ({
@ -528,7 +519,7 @@ export class McpServer {
return { resources: [...resources, ...templateResources] };
});
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
this.server.setRequestHandler('resources/templates/list', async () => {
const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({
name,
uriTemplate: template.resourceTemplate.uriTemplate.toString(),
@ -538,7 +529,7 @@ export class McpServer {
return { resourceTemplates };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
this.server.setRequestHandler('resources/read', async (request, extra) => {
const uri = new URL(request.params.uri);
// First check for exact resource match
@ -571,8 +562,8 @@ export class McpServer {
return;
}
this.server.assertCanSetRequestHandler(getMethodValue(ListPromptsRequestSchema));
this.server.assertCanSetRequestHandler(getMethodValue(GetPromptRequestSchema));
this.server.assertCanSetRequestHandler('prompts/list');
this.server.assertCanSetRequestHandler('prompts/get');
this.server.registerCapabilities({
prompts: {
@ -581,7 +572,7 @@ export class McpServer {
});
this.server.setRequestHandler(
ListPromptsRequestSchema,
'prompts/list',
(): ListPromptsResult => ({
prompts: Object.entries(this._registeredPrompts)
.filter(([, prompt]) => prompt.enabled)
@ -596,7 +587,7 @@ export class McpServer {
})
);
this.server.setRequestHandler(GetPromptRequestSchema, async (request, extra): Promise<GetPromptResult> => {
this.server.setRequestHandler('prompts/get', async (request, extra): Promise<GetPromptResult> => {
const prompt = this._registeredPrompts[request.params.name];
if (!prompt) {
throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
@ -1263,22 +1254,6 @@ function promptArgumentsFromSchema(schema: AnyObjectSchema): PromptArgument[] {
});
}
function getMethodValue(schema: AnyObjectSchema): string {
const shape = getObjectShape(schema);
const methodSchema = shape?.method as AnySchema | undefined;
if (!methodSchema) {
throw new Error('Schema is missing a method literal');
}
// Extract literal value - works for both v3 and v4
const value = getLiteralValue(methodSchema);
if (typeof value === 'string') {
return value;
}
throw new Error('Schema method literal must be a string');
}
function createCompletionResult(suggestions: string[]): CompleteResult {
return {
completion: {

View file

@ -1,5 +1,4 @@
import type {
AnyObjectSchema,
ClientCapabilities,
CreateMessageRequest,
CreateMessageRequestParamsBase,
@ -22,18 +21,17 @@ import type {
ProtocolOptions,
Request,
RequestHandlerExtra,
RequestMethod,
RequestOptions,
RequestTypeMap,
ResourceUpdatedNotification,
Result,
SchemaOutput,
ServerCapabilities,
ServerNotification,
ServerRequest,
ServerResult,
ToolResultContent,
ToolUseContent,
ZodV3Internal,
ZodV4Internal
ToolUseContent
} from '@modelcontextprotocol/core';
import {
AjvJsonSchemaValidator,
@ -47,10 +45,6 @@ import {
ElicitResultSchema,
EmptyResultSchema,
ErrorCode,
getObjectShape,
InitializedNotificationSchema,
InitializeRequestSchema,
isZ4Schema,
LATEST_PROTOCOL_VERSION,
ListRootsResultSchema,
LoggingLevelSchema,
@ -58,7 +52,6 @@ import {
mergeCapabilities,
Protocol,
safeParse,
SetLevelRequestSchema,
SUPPORTED_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/core';
@ -162,11 +155,11 @@ export class Server<
this._instructions = options?.instructions;
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request));
this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.());
this.setRequestHandler('initialize', request => this._oninitialize(request));
this.setNotificationHandler('notifications/initialized', () => this.oninitialized?.());
if (this._capabilities.logging) {
this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => {
this.setRequestHandler('logging/setLevel', async (request, extra) => {
const transportSessionId: string | undefined =
extra.sessionId || (extra.requestInfo?.headers.get('mcp-session-id') as string) || undefined;
const { level } = request.params;
@ -222,39 +215,16 @@ export class Server<
/**
* Override request handler registration to enforce server-side validation for tools/call.
*/
public override setRequestHandler<T extends AnyObjectSchema>(
requestSchema: T,
public override setRequestHandler<M extends RequestMethod>(
method: M,
handler: (
request: SchemaOutput<T>,
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
) => ServerResult | ResultT | Promise<ServerResult | ResultT>
): void {
const shape = getObjectShape(requestSchema);
const methodSchema = shape?.method;
if (!methodSchema) {
throw new Error('Schema is missing a method literal');
}
// Extract literal value using type-safe property access
let methodValue: unknown;
if (isZ4Schema(methodSchema)) {
const v4Schema = methodSchema as unknown as ZodV4Internal;
const v4Def = v4Schema._zod?.def;
methodValue = v4Def?.value ?? v4Schema.value;
} else {
const v3Schema = methodSchema as unknown as ZodV3Internal;
const legacyDef = v3Schema._def;
methodValue = legacyDef?.value ?? v3Schema.value;
}
if (typeof methodValue !== 'string') {
throw new TypeError('Schema method literal must be a string');
}
const method = methodValue;
if (method === 'tools/call') {
const wrappedHandler = async (
request: SchemaOutput<T>,
request: RequestTypeMap[M],
extra: RequestHandlerExtra<ServerRequest | RequestT, ServerNotification | NotificationT>
): Promise<ServerResult | ResultT> => {
const validatedRequest = safeParse(CallToolRequestSchema, request);
@ -293,11 +263,11 @@ export class Server<
};
// Install the wrapped handler
return super.setRequestHandler(requestSchema, wrappedHandler as unknown as typeof handler);
return super.setRequestHandler(method, wrappedHandler as unknown as typeof handler);
}
// Other handlers use default behavior
return super.setRequestHandler(requestSchema, handler);
return super.setRequestHandler(method, handler);
}
protected assertCapabilityForMethod(method: RequestT['method']): void {

Some files were not shown because too many files have changed in this diff Show more