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
3 changed files with 490 additions and 878 deletions

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 {

View file

@ -17,7 +17,7 @@ beforeEach(() => {
outputBuffer = new ReadBuffer();
output = new Writable({
write(chunk, encoding, callback) {
write(chunk, _encoding, callback) {
outputBuffer.append(chunk);
callback();
}