From a677d44c45d71eb879d36324916cca502f19cf42 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 00:02:56 +0530 Subject: [PATCH 01/22] style: standardize string literals and formatting in mcp.ts and add to gitignore --- .gitignore | 2 + packages/server/src/server/mcp.ts | 1073 +++++++++++++++++++---------- 2 files changed, 714 insertions(+), 361 deletions(-) diff --git a/.gitignore b/.gitignore index a1b83bc4f..cc0a52b82 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,5 @@ dist/ # IDE .idea/ + +ahammednibras8 \ No newline at end of file diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 8564212c1..e200cd691 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -31,8 +31,8 @@ import type { ToolExecution, Transport, Variables, - ZodRawShapeCompat -} from '@modelcontextprotocol/core'; + ZodRawShapeCompat, +} from "@modelcontextprotocol/core"; import { assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, @@ -56,15 +56,15 @@ import { safeParseAsync, toJsonSchemaCompat, UriTemplate, - validateAndWarnToolName -} from '@modelcontextprotocol/core'; -import { ZodOptional } from 'zod'; + validateAndWarnToolName, +} from "@modelcontextprotocol/core"; +import { ZodOptional } from "zod"; -import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; -import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.js'; -import { getCompleter, isCompletable } from './completable.js'; -import type { ServerOptions } from './server.js'; -import { Server } from './server.js'; +import type { ToolTaskHandler } from "../experimental/tasks/interfaces.js"; +import { ExperimentalMcpServerTasks } from "../experimental/tasks/mcp-server.js"; +import { getCompleter, isCompletable } from "./completable.js"; +import type { ServerOptions } from "./server.js"; +import { Server } from "./server.js"; /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. @@ -99,7 +99,7 @@ export class McpServer { get experimental(): { tasks: ExperimentalMcpServerTasks } { if (!this._experimental) { this._experimental = { - tasks: new ExperimentalMcpServerTasks(this) + tasks: new ExperimentalMcpServerTasks(this), }; } return this._experimental; @@ -128,13 +128,17 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler(getMethodValue(ListToolsRequestSchema)); - this.server.assertCanSetRequestHandler(getMethodValue(CallToolRequestSchema)); + this.server.assertCanSetRequestHandler( + getMethodValue(ListToolsRequestSchema), + ); + this.server.assertCanSetRequestHandler( + getMethodValue(CallToolRequestSchema), + ); this.server.registerCapabilities({ tools: { - listChanged: true - } + listChanged: true, + }, }); this.server.setRequestHandler( @@ -148,90 +152,132 @@ export class McpServer { title: tool.title, description: tool.description, inputSchema: (() => { - const obj = normalizeObjectSchema(tool.inputSchema); + const obj = normalizeObjectSchema( + tool.inputSchema, + ); return obj ? (toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: 'input' - }) as Tool['inputSchema']) + strictUnions: true, + pipeStrategy: "input", + }) as Tool["inputSchema"]) : EMPTY_OBJECT_JSON_SCHEMA; })(), annotations: tool.annotations, execution: tool.execution, - _meta: tool._meta + _meta: tool._meta, }; if (tool.outputSchema) { - const obj = normalizeObjectSchema(tool.outputSchema); + const obj = normalizeObjectSchema( + tool.outputSchema, + ); if (obj) { - toolDefinition.outputSchema = toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: 'output' - }) as Tool['outputSchema']; + toolDefinition.outputSchema = + toJsonSchemaCompat(obj, { + strictUnions: true, + pipeStrategy: "output", + }) as Tool["outputSchema"]; } } return toolDefinition; - }) - }) + }), + }), ); - this.server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise => { - try { - const tool = this._registeredTools[request.params.name]; - if (!tool) { - throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`); - } - if (!tool.enabled) { - throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`); - } + this.server.setRequestHandler( + CallToolRequestSchema, + async ( + request, + extra, + ): Promise => { + try { + const tool = this._registeredTools[request.params.name]; + if (!tool) { + throw new McpError( + ErrorCode.InvalidParams, + `Tool ${request.params.name} not found`, + ); + } + if (!tool.enabled) { + throw new McpError( + ErrorCode.InvalidParams, + `Tool ${request.params.name} disabled`, + ); + } - const isTaskRequest = !!request.params.task; - const taskSupport = tool.execution?.taskSupport; - const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler); + const isTaskRequest = !!request.params.task; + const taskSupport = tool.execution?.taskSupport; + const isTaskHandler = "createTask" in + (tool.handler as AnyToolHandler); + + // Validate task hint configuration + if ( + (taskSupport === "required" || + taskSupport === "optional") && !isTaskHandler + ) { + throw new McpError( + ErrorCode.InternalError, + `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`, + ); + } - // Validate task hint configuration - if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) { - throw new McpError( - ErrorCode.InternalError, - `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask` - ); - } + // Handle taskSupport 'required' without task augmentation + if (taskSupport === "required" && !isTaskRequest) { + throw new McpError( + ErrorCode.MethodNotFound, + `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`, + ); + } - // Handle taskSupport 'required' without task augmentation - if (taskSupport === 'required' && !isTaskRequest) { - throw new McpError( - ErrorCode.MethodNotFound, - `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')` - ); - } + // Handle taskSupport 'optional' without task augmentation - automatic polling + if ( + taskSupport === "optional" && !isTaskRequest && + isTaskHandler + ) { + return await this.handleAutomaticTaskPolling( + tool, + request, + extra, + ); + } - // Handle taskSupport 'optional' without task augmentation - automatic polling - if (taskSupport === 'optional' && !isTaskRequest && isTaskHandler) { - return await this.handleAutomaticTaskPolling(tool, request, extra); - } + // Normal execution path + const args = await this.validateToolInput( + tool, + request.params.arguments, + request.params.name, + ); + const result = await this.executeToolHandler( + tool, + args, + extra, + ); - // Normal execution path - const args = await this.validateToolInput(tool, request.params.arguments, request.params.name); - const result = await this.executeToolHandler(tool, args, extra); + // Return CreateTaskResult immediately for task requests + if (isTaskRequest) { + return result; + } - // Return CreateTaskResult immediately for task requests - if (isTaskRequest) { + // Validate output schema for non-task requests + await this.validateToolOutput( + tool, + result, + request.params.name, + ); return result; - } - - // Validate output schema for non-task requests - await this.validateToolOutput(tool, result, request.params.name); - return result; - } catch (error) { - if (error instanceof McpError) { - if (error.code === ErrorCode.UrlElicitationRequired) { - throw error; // Return the error to the caller without wrapping in CallToolResult + } catch (error) { + if (error instanceof McpError) { + if (error.code === ErrorCode.UrlElicitationRequired) { + throw error; // Return the error to the caller without wrapping in CallToolResult + } } + return this.createToolError( + error instanceof Error ? error.message : String(error), + ); } - return this.createToolError(error instanceof Error ? error.message : String(error)); - } - }); + }, + ); this._toolHandlersInitialized = true; } @@ -246,11 +292,11 @@ export class McpServer { return { content: [ { - type: 'text', - text: errorMessage - } + type: "text", + text: errorMessage, + }, ], - isError: true + isError: true, }; } @@ -259,11 +305,10 @@ export class McpServer { */ private async validateToolInput< Tool extends RegisteredTool, - Args extends Tool['inputSchema'] extends infer InputSchema - ? InputSchema extends AnySchema - ? SchemaOutput - : undefined + Args extends Tool["inputSchema"] extends infer InputSchema + ? InputSchema extends AnySchema ? SchemaOutput : undefined + : undefined, >(tool: Tool, args: Args, toolName: string): Promise { if (!tool.inputSchema) { return undefined as Args; @@ -275,9 +320,14 @@ export class McpServer { const schemaToParse = inputObj ?? (tool.inputSchema as AnySchema); const parseResult = await safeParseAsync(schemaToParse, args); if (!parseResult.success) { - const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; + const error = "error" in parseResult + ? parseResult.error + : "Unknown error"; const errorMessage = getParseErrorMessage(error); - throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`); + throw new McpError( + ErrorCode.InvalidParams, + `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`, + ); } return parseResult.data as unknown as Args; @@ -286,13 +336,17 @@ export class McpServer { /** * Validates tool output against the tool's output schema. */ - private async validateToolOutput(tool: RegisteredTool, result: CallToolResult | CreateTaskResult, toolName: string): Promise { + private async validateToolOutput( + tool: RegisteredTool, + result: CallToolResult | CreateTaskResult, + toolName: string, + ): Promise { if (!tool.outputSchema) { return; } // Only validate CallToolResult, not CreateTaskResult - if (!('content' in result)) { + if (!("content" in result)) { return; } @@ -303,19 +357,26 @@ export class McpServer { if (!result.structuredContent) { throw new McpError( ErrorCode.InvalidParams, - `Output validation error: Tool ${toolName} has an output schema but no structured content was provided` + `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`, ); } // if the tool has an output schema, validate structured content - const outputObj = normalizeObjectSchema(tool.outputSchema) as AnyObjectSchema; - const parseResult = await safeParseAsync(outputObj, result.structuredContent); + const outputObj = normalizeObjectSchema( + tool.outputSchema, + ) as AnyObjectSchema; + const parseResult = await safeParseAsync( + outputObj, + result.structuredContent, + ); if (!parseResult.success) { - const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; + const error = "error" in parseResult + ? parseResult.error + : "Unknown error"; const errorMessage = getParseErrorMessage(error); throw new McpError( ErrorCode.InvalidParams, - `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}` + `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`, ); } } @@ -326,25 +387,33 @@ export class McpServer { private async executeToolHandler( tool: RegisteredTool, args: unknown, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ): Promise { - const handler = tool.handler as AnyToolHandler; - const isTaskHandler = 'createTask' in handler; + const handler = tool.handler as AnyToolHandler< + ZodRawShapeCompat | undefined + >; + const isTaskHandler = "createTask" in handler; if (isTaskHandler) { if (!extra.taskStore) { - throw new Error('No task store provided.'); + throw new Error("No task store provided."); } const taskExtra = { ...extra, taskStore: extra.taskStore }; if (tool.inputSchema) { - const typedHandler = handler as ToolTaskHandler; + const typedHandler = handler as ToolTaskHandler< + ZodRawShapeCompat + >; // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve(typedHandler.createTask(args as any, taskExtra)); + return await Promise.resolve( + typedHandler.createTask(args as any, taskExtra), + ); } else { const typedHandler = handler as ToolTaskHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve((typedHandler.createTask as any)(taskExtra)); + return await Promise.resolve( + (typedHandler.createTask as any)(taskExtra), + ); } } @@ -365,32 +434,53 @@ export class McpServer { private async handleAutomaticTaskPolling( tool: RegisteredTool, request: RequestT, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ): Promise { if (!extra.taskStore) { - throw new Error('No task store provided for task-capable tool.'); + throw new Error("No task store provided for task-capable tool."); } // Validate input and create task - const args = await this.validateToolInput(tool, request.params.arguments, request.params.name); - const handler = tool.handler as ToolTaskHandler; + const args = await this.validateToolInput( + tool, + request.params.arguments, + request.params.name, + ); + const handler = tool.handler as ToolTaskHandler< + ZodRawShapeCompat | undefined + >; const taskExtra = { ...extra, taskStore: extra.taskStore }; const createTaskResult: CreateTaskResult = args // undefined only if tool.inputSchema is undefined - ? await Promise.resolve((handler as ToolTaskHandler).createTask(args, taskExtra)) - : // eslint-disable-next-line @typescript-eslint/no-explicit-any - await Promise.resolve(((handler as ToolTaskHandler).createTask as any)(taskExtra)); + ? await Promise.resolve( + (handler as ToolTaskHandler).createTask( + args, + taskExtra, + ), + ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + : await Promise.resolve( + ((handler as ToolTaskHandler).createTask as any)( + taskExtra, + ), + ); // Poll until completion const taskId = createTaskResult.task.taskId; let task = createTaskResult.task; const pollInterval = task.pollInterval ?? 5000; - while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') { - await new Promise(resolve => setTimeout(resolve, pollInterval)); + while ( + task.status !== "completed" && task.status !== "failed" && + task.status !== "cancelled" + ) { + await new Promise((resolve) => setTimeout(resolve, pollInterval)); const updatedTask = await extra.taskStore.getTask(taskId); if (!updatedTask) { - throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`); + throw new McpError( + ErrorCode.InternalError, + `Task ${taskId} not found during polling`, + ); } task = updatedTask; } @@ -406,38 +496,61 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler(getMethodValue(CompleteRequestSchema)); + this.server.assertCanSetRequestHandler( + getMethodValue(CompleteRequestSchema), + ); this.server.registerCapabilities({ - completions: {} + completions: {}, }); - this.server.setRequestHandler(CompleteRequestSchema, async (request): Promise => { - switch (request.params.ref.type) { - case 'ref/prompt': - assertCompleteRequestPrompt(request); - return this.handlePromptCompletion(request, request.params.ref); - - case 'ref/resource': - assertCompleteRequestResourceTemplate(request); - return this.handleResourceCompletion(request, request.params.ref); - - default: - throw new McpError(ErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`); - } - }); + this.server.setRequestHandler( + CompleteRequestSchema, + async (request): Promise => { + switch (request.params.ref.type) { + case "ref/prompt": + assertCompleteRequestPrompt(request); + return this.handlePromptCompletion( + request, + request.params.ref, + ); + + case "ref/resource": + assertCompleteRequestResourceTemplate(request); + return this.handleResourceCompletion( + request, + request.params.ref, + ); + + default: + throw new McpError( + ErrorCode.InvalidParams, + `Invalid completion reference: ${request.params.ref}`, + ); + } + }, + ); this._completionHandlerInitialized = true; } - private async handlePromptCompletion(request: CompleteRequestPrompt, ref: PromptReference): Promise { + private async handlePromptCompletion( + request: CompleteRequestPrompt, + ref: PromptReference, + ): Promise { const prompt = this._registeredPrompts[ref.name]; if (!prompt) { - throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} not found`); + throw new McpError( + ErrorCode.InvalidParams, + `Prompt ${ref.name} not found`, + ); } if (!prompt.enabled) { - throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} disabled`); + throw new McpError( + ErrorCode.InvalidParams, + `Prompt ${ref.name} disabled`, + ); } if (!prompt.argsSchema) { @@ -454,15 +567,20 @@ export class McpServer { if (!completer) { return EMPTY_COMPLETION_RESULT; } - const suggestions = await completer(request.params.argument.value, request.params.context); + const suggestions = await completer( + request.params.argument.value, + request.params.context, + ); return createCompletionResult(suggestions); } private async handleResourceCompletion( request: CompleteRequestResourceTemplate, - ref: ResourceTemplateReference + ref: ResourceTemplateReference, ): Promise { - const template = Object.values(this._registeredResourceTemplates).find(t => t.resourceTemplate.uriTemplate.toString() === ref.uri); + const template = Object.values(this._registeredResourceTemplates).find( + (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri, + ); if (!template) { if (this._registeredResources[ref.uri]) { @@ -470,15 +588,23 @@ export class McpServer { return EMPTY_COMPLETION_RESULT; } - throw new McpError(ErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`); + throw new McpError( + ErrorCode.InvalidParams, + `Resource template ${request.params.ref.uri} not found`, + ); } - const completer = template.resourceTemplate.completeCallback(request.params.argument.name); + const completer = template.resourceTemplate.completeCallback( + request.params.argument.name, + ); if (!completer) { return EMPTY_COMPLETION_RESULT; } - const suggestions = await completer(request.params.argument.value, request.params.context); + const suggestions = await completer( + request.params.argument.value, + request.params.context, + ); return createCompletionResult(suggestions); } @@ -489,76 +615,111 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler(getMethodValue(ListResourcesRequestSchema)); - this.server.assertCanSetRequestHandler(getMethodValue(ListResourceTemplatesRequestSchema)); - this.server.assertCanSetRequestHandler(getMethodValue(ReadResourceRequestSchema)); + this.server.assertCanSetRequestHandler( + getMethodValue(ListResourcesRequestSchema), + ); + this.server.assertCanSetRequestHandler( + getMethodValue(ListResourceTemplatesRequestSchema), + ); + this.server.assertCanSetRequestHandler( + getMethodValue(ReadResourceRequestSchema), + ); this.server.registerCapabilities({ resources: { - listChanged: true - } + listChanged: true, + }, }); - this.server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => { - const resources = Object.entries(this._registeredResources) - .filter(([_, resource]) => resource.enabled) - .map(([uri, resource]) => ({ - uri, - name: resource.name, - ...resource.metadata - })); - - const templateResources: Resource[] = []; - for (const template of Object.values(this._registeredResourceTemplates)) { - if (!template.resourceTemplate.listCallback) { - continue; - } + this.server.setRequestHandler( + ListResourcesRequestSchema, + async (request, extra) => { + const resources = Object.entries(this._registeredResources) + .filter(([_, resource]) => resource.enabled) + .map(([uri, resource]) => ({ + uri, + name: resource.name, + ...resource.metadata, + })); + + const templateResources: Resource[] = []; + for ( + const template of Object.values( + this._registeredResourceTemplates, + ) + ) { + if (!template.resourceTemplate.listCallback) { + continue; + } - const result = await template.resourceTemplate.listCallback(extra); - for (const resource of result.resources) { - templateResources.push({ - ...template.metadata, - // the defined resource metadata should override the template metadata if present - ...resource - }); + const result = await template.resourceTemplate.listCallback( + extra, + ); + for (const resource of result.resources) { + templateResources.push({ + ...template.metadata, + // the defined resource metadata should override the template metadata if present + ...resource, + }); + } } - } - return { resources: [...resources, ...templateResources] }; - }); - - this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { - const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({ - name, - uriTemplate: template.resourceTemplate.uriTemplate.toString(), - ...template.metadata - })); + return { resources: [...resources, ...templateResources] }; + }, + ); - return { resourceTemplates }; - }); + this.server.setRequestHandler( + ListResourceTemplatesRequestSchema, + async () => { + const resourceTemplates = Object.entries( + this._registeredResourceTemplates, + ).map(([name, template]) => ({ + name, + uriTemplate: template.resourceTemplate.uriTemplate + .toString(), + ...template.metadata, + })); - this.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => { - const uri = new URL(request.params.uri); + return { resourceTemplates }; + }, + ); - // First check for exact resource match - const resource = this._registeredResources[uri.toString()]; - if (resource) { - if (!resource.enabled) { - throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} disabled`); + this.server.setRequestHandler( + ReadResourceRequestSchema, + async (request, extra) => { + const uri = new URL(request.params.uri); + + // First check for exact resource match + const resource = this._registeredResources[uri.toString()]; + if (resource) { + if (!resource.enabled) { + throw new McpError( + ErrorCode.InvalidParams, + `Resource ${uri} disabled`, + ); + } + return resource.readCallback(uri, extra); } - return resource.readCallback(uri, extra); - } - // Then check templates - for (const template of Object.values(this._registeredResourceTemplates)) { - const variables = template.resourceTemplate.uriTemplate.match(uri.toString()); - if (variables) { - return template.readCallback(uri, variables, extra); + // Then check templates + for ( + const template of Object.values( + this._registeredResourceTemplates, + ) + ) { + const variables = template.resourceTemplate.uriTemplate + .match(uri.toString()); + if (variables) { + return template.readCallback(uri, variables, extra); + } } - } - throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); - }); + throw new McpError( + ErrorCode.InvalidParams, + `Resource ${uri} not found`, + ); + }, + ); this._resourceHandlersInitialized = true; } @@ -570,13 +731,17 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler(getMethodValue(ListPromptsRequestSchema)); - this.server.assertCanSetRequestHandler(getMethodValue(GetPromptRequestSchema)); + this.server.assertCanSetRequestHandler( + getMethodValue(ListPromptsRequestSchema), + ); + this.server.assertCanSetRequestHandler( + getMethodValue(GetPromptRequestSchema), + ); this.server.registerCapabilities({ prompts: { - listChanged: true - } + listChanged: true, + }, }); this.server.setRequestHandler( @@ -589,40 +754,63 @@ export class McpServer { name, title: prompt.title, description: prompt.description, - arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : undefined + arguments: prompt.argsSchema + ? promptArgumentsFromSchema(prompt.argsSchema) + : undefined, }; - }) - }) + }), + }), ); - this.server.setRequestHandler(GetPromptRequestSchema, async (request, extra): Promise => { - const prompt = this._registeredPrompts[request.params.name]; - if (!prompt) { - throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} not found`); - } - - if (!prompt.enabled) { - throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`); - } + this.server.setRequestHandler( + GetPromptRequestSchema, + async (request, extra): Promise => { + const prompt = this._registeredPrompts[request.params.name]; + if (!prompt) { + throw new McpError( + ErrorCode.InvalidParams, + `Prompt ${request.params.name} not found`, + ); + } - if (prompt.argsSchema) { - const argsObj = normalizeObjectSchema(prompt.argsSchema) as AnyObjectSchema; - const parseResult = await safeParseAsync(argsObj, request.params.arguments); - if (!parseResult.success) { - const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; - const errorMessage = getParseErrorMessage(error); - throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`); + if (!prompt.enabled) { + throw new McpError( + ErrorCode.InvalidParams, + `Prompt ${request.params.name} disabled`, + ); } - const args = parseResult.data; - const cb = prompt.callback as PromptCallback; - return await Promise.resolve(cb(args, extra)); - } else { - const cb = prompt.callback as PromptCallback; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve((cb as any)(extra)); - } - }); + if (prompt.argsSchema) { + const argsObj = normalizeObjectSchema( + prompt.argsSchema, + ) as AnyObjectSchema; + const parseResult = await safeParseAsync( + argsObj, + request.params.arguments, + ); + if (!parseResult.success) { + const error = "error" in parseResult + ? parseResult.error + : "Unknown error"; + const errorMessage = getParseErrorMessage(error); + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`, + ); + } + + const args = parseResult.data; + const cb = prompt.callback as PromptCallback< + PromptArgsRawShape + >; + return await Promise.resolve(cb(args, extra)); + } else { + const cb = prompt.callback as PromptCallback; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return await Promise.resolve((cb as any)(extra)); + } + }, + ); this._promptHandlersInitialized = true; } @@ -631,19 +819,32 @@ export class McpServer { * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. * @deprecated Use `registerResource` instead. */ - resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; + resource( + name: string, + uri: string, + readCallback: ReadResourceCallback, + ): RegisteredResource; /** * Registers a resource `name` at a fixed URI with metadata, which will use the given callback to respond to read requests. * @deprecated Use `registerResource` instead. */ - resource(name: string, uri: string, metadata: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; + resource( + name: string, + uri: string, + metadata: ResourceMetadata, + readCallback: ReadResourceCallback, + ): RegisteredResource; /** * Registers a resource `name` with a template pattern, which will use the given callback to respond to read requests. * @deprecated Use `registerResource` instead. */ - resource(name: string, template: ResourceTemplate, readCallback: ReadResourceTemplateCallback): RegisteredResourceTemplate; + resource( + name: string, + template: ResourceTemplate, + readCallback: ReadResourceTemplateCallback, + ): RegisteredResourceTemplate; /** * Registers a resource `name` with a template pattern and metadata, which will use the given callback to respond to read requests. @@ -653,20 +854,28 @@ export class McpServer { name: string, template: ResourceTemplate, metadata: ResourceMetadata, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate; - resource(name: string, uriOrTemplate: string | ResourceTemplate, ...rest: unknown[]): RegisteredResource | RegisteredResourceTemplate { + resource( + name: string, + uriOrTemplate: string | ResourceTemplate, + ...rest: unknown[] + ): RegisteredResource | RegisteredResourceTemplate { let metadata: ResourceMetadata | undefined; - if (typeof rest[0] === 'object') { + if (typeof rest[0] === "object") { metadata = rest.shift() as ResourceMetadata; } - const readCallback = rest[0] as ReadResourceCallback | ReadResourceTemplateCallback; + const readCallback = rest[0] as + | ReadResourceCallback + | ReadResourceTemplateCallback; - if (typeof uriOrTemplate === 'string') { + if (typeof uriOrTemplate === "string") { if (this._registeredResources[uriOrTemplate]) { - throw new Error(`Resource ${uriOrTemplate} is already registered`); + throw new Error( + `Resource ${uriOrTemplate} is already registered`, + ); } const registeredResource = this._createRegisteredResource( @@ -674,7 +883,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback, ); this.setResourceRequestHandlers(); @@ -682,16 +891,19 @@ export class McpServer { return registeredResource; } else { if (this._registeredResourceTemplates[name]) { - throw new Error(`Resource template ${name} is already registered`); + throw new Error( + `Resource template ${name} is already registered`, + ); } - const registeredResourceTemplate = this._createRegisteredResourceTemplate( - name, - undefined, - uriOrTemplate, - metadata, - readCallback as ReadResourceTemplateCallback - ); + const registeredResourceTemplate = this + ._createRegisteredResourceTemplate( + name, + undefined, + uriOrTemplate, + metadata, + readCallback as ReadResourceTemplateCallback, + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -703,22 +915,29 @@ export class McpServer { * Registers a resource with a config object and callback. * For static resources, use a URI string. For dynamic resources, use a ResourceTemplate. */ - registerResource(name: string, uriOrTemplate: string, config: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; + registerResource( + name: string, + uriOrTemplate: string, + config: ResourceMetadata, + readCallback: ReadResourceCallback, + ): RegisteredResource; registerResource( name: string, uriOrTemplate: ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate; registerResource( name: string, uriOrTemplate: string | ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceCallback | ReadResourceTemplateCallback + readCallback: ReadResourceCallback | ReadResourceTemplateCallback, ): RegisteredResource | RegisteredResourceTemplate { - if (typeof uriOrTemplate === 'string') { + if (typeof uriOrTemplate === "string") { if (this._registeredResources[uriOrTemplate]) { - throw new Error(`Resource ${uriOrTemplate} is already registered`); + throw new Error( + `Resource ${uriOrTemplate} is already registered`, + ); } const registeredResource = this._createRegisteredResource( @@ -726,7 +945,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceCallback + readCallback as ReadResourceCallback, ); this.setResourceRequestHandlers(); @@ -734,16 +953,19 @@ export class McpServer { return registeredResource; } else { if (this._registeredResourceTemplates[name]) { - throw new Error(`Resource template ${name} is already registered`); + throw new Error( + `Resource template ${name} is already registered`, + ); } - const registeredResourceTemplate = this._createRegisteredResourceTemplate( - name, - (config as BaseMetadata).title, - uriOrTemplate, - config, - readCallback as ReadResourceTemplateCallback - ); + const registeredResourceTemplate = this + ._createRegisteredResourceTemplate( + name, + (config as BaseMetadata).title, + uriOrTemplate, + config, + readCallback as ReadResourceTemplateCallback, + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -756,7 +978,7 @@ export class McpServer { title: string | undefined, uri: string, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceCallback + readCallback: ReadResourceCallback, ): RegisteredResource { const registeredResource: RegisteredResource = { name, @@ -767,18 +989,31 @@ export class McpServer { disable: () => registeredResource.update({ enabled: false }), enable: () => registeredResource.update({ enabled: true }), remove: () => registeredResource.update({ uri: null }), - update: updates => { - if (typeof updates.uri !== 'undefined' && updates.uri !== uri) { + update: (updates) => { + if (typeof updates.uri !== "undefined" && updates.uri !== uri) { delete this._registeredResources[uri]; - if (updates.uri) this._registeredResources[updates.uri] = registeredResource; + if (updates.uri) { + this._registeredResources[updates.uri] = + registeredResource; + } + } + if (typeof updates.name !== "undefined") { + registeredResource.name = updates.name; + } + if (typeof updates.title !== "undefined") { + registeredResource.title = updates.title; + } + if (typeof updates.metadata !== "undefined") { + registeredResource.metadata = updates.metadata; + } + if (typeof updates.callback !== "undefined") { + registeredResource.readCallback = updates.callback; + } + if (typeof updates.enabled !== "undefined") { + registeredResource.enabled = updates.enabled; } - if (typeof updates.name !== 'undefined') registeredResource.name = updates.name; - if (typeof updates.title !== 'undefined') registeredResource.title = updates.title; - if (typeof updates.metadata !== 'undefined') registeredResource.metadata = updates.metadata; - if (typeof updates.callback !== 'undefined') registeredResource.readCallback = updates.callback; - if (typeof updates.enabled !== 'undefined') registeredResource.enabled = updates.enabled; this.sendResourceListChanged(); - } + }, }; this._registeredResources[uri] = registeredResource; return registeredResource; @@ -789,7 +1024,7 @@ export class McpServer { title: string | undefined, template: ResourceTemplate, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceTemplateCallback + readCallback: ReadResourceTemplateCallback, ): RegisteredResourceTemplate { const registeredResourceTemplate: RegisteredResourceTemplate = { resourceTemplate: template, @@ -797,27 +1032,45 @@ export class McpServer { metadata, readCallback, enabled: true, - disable: () => registeredResourceTemplate.update({ enabled: false }), + disable: () => + registeredResourceTemplate.update({ enabled: false }), enable: () => registeredResourceTemplate.update({ enabled: true }), remove: () => registeredResourceTemplate.update({ name: null }), - update: updates => { - if (typeof updates.name !== 'undefined' && updates.name !== name) { + update: (updates) => { + if ( + typeof updates.name !== "undefined" && updates.name !== name + ) { delete this._registeredResourceTemplates[name]; - if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate; + if (updates.name) { + this._registeredResourceTemplates[updates.name] = + registeredResourceTemplate; + } + } + if (typeof updates.title !== "undefined") { + registeredResourceTemplate.title = updates.title; + } + if (typeof updates.template !== "undefined") { + registeredResourceTemplate.resourceTemplate = + updates.template; + } + if (typeof updates.metadata !== "undefined") { + registeredResourceTemplate.metadata = updates.metadata; + } + if (typeof updates.callback !== "undefined") { + registeredResourceTemplate.readCallback = updates.callback; + } + if (typeof updates.enabled !== "undefined") { + registeredResourceTemplate.enabled = updates.enabled; } - if (typeof updates.title !== 'undefined') registeredResourceTemplate.title = updates.title; - if (typeof updates.template !== 'undefined') registeredResourceTemplate.resourceTemplate = updates.template; - if (typeof updates.metadata !== 'undefined') registeredResourceTemplate.metadata = updates.metadata; - if (typeof updates.callback !== 'undefined') registeredResourceTemplate.readCallback = updates.callback; - if (typeof updates.enabled !== 'undefined') registeredResourceTemplate.enabled = updates.enabled; this.sendResourceListChanged(); - } + }, }; this._registeredResourceTemplates[name] = registeredResourceTemplate; // If the resource template has any completion callbacks, enable completions capability const variableNames = template.uriTemplate.variableNames; - const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v)); + const hasCompleter = Array.isArray(variableNames) && + variableNames.some((v) => !!template.completeCallback(v)); if (hasCompleter) { this.setCompletionRequestHandler(); } @@ -830,36 +1083,57 @@ export class McpServer { title: string | undefined, description: string | undefined, argsSchema: PromptArgsRawShape | undefined, - callback: PromptCallback + callback: PromptCallback, ): RegisteredPrompt { const registeredPrompt: RegisteredPrompt = { title, description, - argsSchema: argsSchema === undefined ? undefined : objectFromShape(argsSchema), + argsSchema: argsSchema === undefined + ? undefined + : objectFromShape(argsSchema), callback, enabled: true, disable: () => registeredPrompt.update({ enabled: false }), enable: () => registeredPrompt.update({ enabled: true }), remove: () => registeredPrompt.update({ name: null }), - update: updates => { - if (typeof updates.name !== 'undefined' && updates.name !== name) { + update: (updates) => { + if ( + typeof updates.name !== "undefined" && updates.name !== name + ) { delete this._registeredPrompts[name]; - if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt; + if (updates.name) { + this._registeredPrompts[updates.name] = + registeredPrompt; + } + } + if (typeof updates.title !== "undefined") { + registeredPrompt.title = updates.title; + } + if (typeof updates.description !== "undefined") { + registeredPrompt.description = updates.description; + } + if (typeof updates.argsSchema !== "undefined") { + registeredPrompt.argsSchema = objectFromShape( + updates.argsSchema, + ); + } + if (typeof updates.callback !== "undefined") { + registeredPrompt.callback = updates.callback; + } + if (typeof updates.enabled !== "undefined") { + registeredPrompt.enabled = updates.enabled; } - if (typeof updates.title !== 'undefined') registeredPrompt.title = updates.title; - if (typeof updates.description !== 'undefined') registeredPrompt.description = updates.description; - if (typeof updates.argsSchema !== 'undefined') registeredPrompt.argsSchema = objectFromShape(updates.argsSchema); - if (typeof updates.callback !== 'undefined') registeredPrompt.callback = updates.callback; - if (typeof updates.enabled !== 'undefined') registeredPrompt.enabled = updates.enabled; this.sendPromptListChanged(); - } + }, }; this._registeredPrompts[name] = registeredPrompt; // If any argument uses a Completable schema, enable completions capability if (argsSchema) { - const hasCompletable = Object.values(argsSchema).some(field => { - const inner: unknown = field instanceof ZodOptional ? field._def?.innerType : field; + const hasCompletable = Object.values(argsSchema).some((field) => { + const inner: unknown = field instanceof ZodOptional + ? field._def?.innerType + : field; return isCompletable(inner); }); if (hasCompletable) { @@ -879,7 +1153,7 @@ export class McpServer { annotations: ToolAnnotations | undefined, execution: ToolExecution | undefined, _meta: Record | undefined, - handler: AnyToolHandler + handler: AnyToolHandler, ): RegisteredTool { // Validate tool name according to SEP specification validateAndWarnToolName(name); @@ -897,24 +1171,48 @@ export class McpServer { disable: () => registeredTool.update({ enabled: false }), enable: () => registeredTool.update({ enabled: true }), remove: () => registeredTool.update({ name: null }), - update: updates => { - if (typeof updates.name !== 'undefined' && updates.name !== name) { - if (typeof updates.name === 'string') { + update: (updates) => { + if ( + typeof updates.name !== "undefined" && updates.name !== name + ) { + if (typeof updates.name === "string") { validateAndWarnToolName(updates.name); } delete this._registeredTools[name]; - if (updates.name) this._registeredTools[updates.name] = registeredTool; + if (updates.name) { + this._registeredTools[updates.name] = registeredTool; + } + } + if (typeof updates.title !== "undefined") { + registeredTool.title = updates.title; + } + if (typeof updates.description !== "undefined") { + registeredTool.description = updates.description; + } + if (typeof updates.paramsSchema !== "undefined") { + registeredTool.inputSchema = objectFromShape( + updates.paramsSchema, + ); + } + if (typeof updates.outputSchema !== "undefined") { + registeredTool.outputSchema = objectFromShape( + updates.outputSchema, + ); + } + if (typeof updates.callback !== "undefined") { + registeredTool.handler = updates.callback; + } + if (typeof updates.annotations !== "undefined") { + registeredTool.annotations = updates.annotations; + } + if (typeof updates._meta !== "undefined") { + registeredTool._meta = updates._meta; + } + if (typeof updates.enabled !== "undefined") { + registeredTool.enabled = updates.enabled; } - if (typeof updates.title !== 'undefined') registeredTool.title = updates.title; - if (typeof updates.description !== 'undefined') registeredTool.description = updates.description; - if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = objectFromShape(updates.paramsSchema); - if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = objectFromShape(updates.outputSchema); - if (typeof updates.callback !== 'undefined') registeredTool.handler = updates.callback; - if (typeof updates.annotations !== 'undefined') registeredTool.annotations = updates.annotations; - if (typeof updates._meta !== 'undefined') registeredTool._meta = updates._meta; - if (typeof updates.enabled !== 'undefined') registeredTool.enabled = updates.enabled; this.sendToolListChanged(); - } + }, }; this._registeredTools[name] = registeredTool; @@ -947,7 +1245,7 @@ export class McpServer { tool( name: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -963,7 +1261,7 @@ export class McpServer { name: string, description: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -974,7 +1272,7 @@ export class McpServer { name: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -986,7 +1284,7 @@ export class McpServer { description: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool; /** @@ -1006,7 +1304,7 @@ export class McpServer { // Support for this style is frozen as of protocol version 2025-03-26. Future additions // to tool definition should *NOT* be added. - if (typeof rest[0] === 'string') { + if (typeof rest[0] === "string") { description = rest.shift() as string; } @@ -1020,12 +1318,15 @@ export class McpServer { inputSchema = rest.shift() as ZodRawShapeCompat; // Check if the next arg is potentially annotations - if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !isZodRawShapeCompat(rest[0])) { + if ( + rest.length > 1 && typeof rest[0] === "object" && + rest[0] !== null && !isZodRawShapeCompat(rest[0]) + ) { // Case: tool(name, paramsSchema, annotations, cb) // Or: tool(name, description, paramsSchema, annotations, cb) annotations = rest.shift() as ToolAnnotations; } - } else if (typeof firstArg === 'object' && firstArg !== null) { + } else if (typeof firstArg === "object" && firstArg !== null) { // Not a ZodRawShapeCompat, so must be annotations in this position // Case: tool(name, annotations, cb) // Or: tool(name, description, annotations, cb) @@ -1041,16 +1342,19 @@ export class McpServer { inputSchema, outputSchema, annotations, - { taskSupport: 'forbidden' }, + { taskSupport: "forbidden" }, undefined, - callback + callback, ); } /** * Registers a tool with a config object and callback. */ - registerTool( + registerTool< + OutputArgs extends ZodRawShapeCompat | AnySchema, + InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, + >( name: string, config: { title?: string; @@ -1060,13 +1364,20 @@ export class McpServer { annotations?: ToolAnnotations; _meta?: Record; }, - cb: ToolCallback + cb: ToolCallback, ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } - const { title, description, inputSchema, outputSchema, annotations, _meta } = config; + const { + title, + description, + inputSchema, + outputSchema, + annotations, + _meta, + } = config; return this._createRegisteredTool( name, @@ -1075,9 +1386,9 @@ export class McpServer { inputSchema, outputSchema, annotations, - { taskSupport: 'forbidden' }, + { taskSupport: "forbidden" }, _meta, - cb as ToolCallback + cb as ToolCallback, ); } @@ -1091,13 +1402,21 @@ export class McpServer { * Registers a zero-argument prompt `name` (with a description) which will run the given function when the client calls it. * @deprecated Use `registerPrompt` instead. */ - prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; + prompt( + name: string, + description: string, + cb: PromptCallback, + ): RegisteredPrompt; /** * Registers a prompt `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. * @deprecated Use `registerPrompt` instead. */ - prompt(name: string, argsSchema: Args, cb: PromptCallback): RegisteredPrompt; + prompt( + name: string, + argsSchema: Args, + cb: PromptCallback, + ): RegisteredPrompt; /** * Registers a prompt `name` (with a description) accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. @@ -1107,7 +1426,7 @@ export class McpServer { name: string, description: string, argsSchema: Args, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt; prompt(name: string, ...rest: unknown[]): RegisteredPrompt { @@ -1116,7 +1435,7 @@ export class McpServer { } let description: string | undefined; - if (typeof rest[0] === 'string') { + if (typeof rest[0] === "string") { description = rest.shift() as string; } @@ -1126,7 +1445,13 @@ export class McpServer { } const cb = rest[0] as PromptCallback; - const registeredPrompt = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb); + const registeredPrompt = this._createRegisteredPrompt( + name, + undefined, + description, + argsSchema, + cb, + ); this.setPromptRequestHandlers(); this.sendPromptListChanged(); @@ -1144,7 +1469,7 @@ export class McpServer { description?: string; argsSchema?: Args; }, - cb: PromptCallback + cb: PromptCallback, ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); @@ -1157,7 +1482,7 @@ export class McpServer { title, description, argsSchema, - cb as PromptCallback + cb as PromptCallback, ); this.setPromptRequestHandlers(); @@ -1181,7 +1506,10 @@ export class McpServer { * @param params * @param sessionId optional for stateless and backward compatibility */ - async sendLoggingMessage(params: LoggingMessageNotification['params'], sessionId?: string) { + async sendLoggingMessage( + params: LoggingMessageNotification["params"], + sessionId?: string, + ) { return this.server.sendLoggingMessage(params, sessionId); } /** @@ -1219,7 +1547,7 @@ export type CompleteResourceTemplateCallback = ( value: string, context?: { arguments?: Record; - } + }, ) => string[] | Promise; /** @@ -1243,9 +1571,11 @@ export class ResourceTemplate { complete?: { [variable: string]: CompleteResourceTemplateCallback; }; - } + }, ) { - this._uriTemplate = typeof uriTemplate === 'string' ? new UriTemplate(uriTemplate) : uriTemplate; + this._uriTemplate = typeof uriTemplate === "string" + ? new UriTemplate(uriTemplate) + : uriTemplate; } /** @@ -1265,7 +1595,9 @@ export class ResourceTemplate { /** * Gets the callback for completing a specific URI template variable, if one was provided. */ - completeCallback(variable: string): CompleteResourceTemplateCallback | undefined { + completeCallback( + variable: string, + ): CompleteResourceTemplateCallback | undefined { return this._callbacks.complete?.[variable]; } } @@ -1273,12 +1605,16 @@ export class ResourceTemplate { export type BaseToolCallback< SendResultT extends Result, Extra extends RequestHandlerExtra, - Args extends undefined | ZodRawShapeCompat | AnySchema -> = Args extends ZodRawShapeCompat - ? (args: ShapeOutput, extra: Extra) => SendResultT | Promise - : Args extends AnySchema - ? (args: SchemaOutput, extra: Extra) => SendResultT | Promise - : (extra: Extra) => SendResultT | Promise; + Args extends undefined | ZodRawShapeCompat | AnySchema, +> = Args extends ZodRawShapeCompat ? ( + args: ShapeOutput, + extra: Extra, + ) => SendResultT | Promise + : Args extends AnySchema ? ( + args: SchemaOutput, + extra: Extra, + ) => SendResultT | Promise + : (extra: Extra) => SendResultT | Promise; /** * Callback for a tool handler registered with Server.tool(). @@ -1290,7 +1626,9 @@ export type BaseToolCallback< * - `content` if the tool does not have an outputSchema * - Both fields are optional but typically one should be provided */ -export type ToolCallback = BaseToolCallback< +export type ToolCallback< + Args extends undefined | ZodRawShapeCompat | AnySchema = undefined, +> = BaseToolCallback< CallToolResult, RequestHandlerExtra, Args @@ -1299,7 +1637,9 @@ export type ToolCallback = ToolCallback | ToolTaskHandler; +export type AnyToolHandler< + Args extends undefined | ZodRawShapeCompat | AnySchema = undefined, +> = ToolCallback | ToolTaskHandler; export type RegisteredTool = { title?: string; @@ -1313,7 +1653,10 @@ export type RegisteredTool = { enabled: boolean; enable(): void; disable(): void; - update(updates: { + update< + InputArgs extends ZodRawShapeCompat, + OutputArgs extends ZodRawShapeCompat, + >(updates: { name?: string | null; title?: string; description?: string; @@ -1328,8 +1671,8 @@ export type RegisteredTool = { }; const EMPTY_OBJECT_JSON_SCHEMA = { - type: 'object' as const, - properties: {} + type: "object" as const, + properties: {}, }; /** @@ -1338,11 +1681,11 @@ const EMPTY_OBJECT_JSON_SCHEMA = { function isZodTypeLike(value: unknown): value is AnySchema { return ( value !== null && - typeof value === 'object' && - 'parse' in value && - typeof value.parse === 'function' && - 'safeParse' in value && - typeof value.safeParse === 'function' + typeof value === "object" && + "parse" in value && + typeof value.parse === "function" && + "safeParse" in value && + typeof value.safeParse === "function" ); } @@ -1356,7 +1699,7 @@ function isZodTypeLike(value: unknown): value is AnySchema { * This includes transformed schemas like z.preprocess(), z.transform(), z.pipe(). */ function isZodSchemaInstance(obj: object): boolean { - return '_def' in obj || '_zod' in obj || isZodTypeLike(obj); + return "_def" in obj || "_zod" in obj || isZodTypeLike(obj); } /** @@ -1368,7 +1711,7 @@ function isZodSchemaInstance(obj: object): boolean { * which have internal properties that could be mistaken for schema values. */ function isZodRawShapeCompat(obj: unknown): obj is ZodRawShapeCompat { - if (typeof obj !== 'object' || obj === null) { + if (typeof obj !== "object" || obj === null) { return false; } @@ -1390,7 +1733,9 @@ function isZodRawShapeCompat(obj: unknown): obj is ZodRawShapeCompat { * Converts a provided Zod schema to a Zod object if it is a ZodRawShapeCompat, * otherwise returns the schema as is. */ -function getZodSchemaObject(schema: ZodRawShapeCompat | AnySchema | undefined): AnySchema | undefined { +function getZodSchemaObject( + schema: ZodRawShapeCompat | AnySchema | undefined, +): AnySchema | undefined { if (!schema) { return undefined; } @@ -1405,13 +1750,13 @@ function getZodSchemaObject(schema: ZodRawShapeCompat | AnySchema | undefined): /** * Additional, optional information for annotating a resource. */ -export type ResourceMetadata = Omit; +export type ResourceMetadata = Omit; /** * Callback to list all resources matching a given template. */ export type ListResourcesCallback = ( - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => ListResourcesResult | Promise; /** @@ -1419,7 +1764,7 @@ export type ListResourcesCallback = ( */ export type ReadResourceCallback = ( uri: URL, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => ReadResourceResult | Promise; export type RegisteredResource = { @@ -1447,7 +1792,7 @@ export type RegisteredResource = { export type ReadResourceTemplateCallback = ( uri: URL, variables: Variables, - extra: RequestHandlerExtra + extra: RequestHandlerExtra, ) => ReadResourceResult | Promise; export type RegisteredResourceTemplate = { @@ -1471,9 +1816,15 @@ export type RegisteredResourceTemplate = { type PromptArgsRawShape = ZodRawShapeCompat; -export type PromptCallback = Args extends PromptArgsRawShape - ? (args: ShapeOutput, extra: RequestHandlerExtra) => GetPromptResult | Promise - : (extra: RequestHandlerExtra) => GetPromptResult | Promise; +export type PromptCallback< + Args extends undefined | PromptArgsRawShape = undefined, +> = Args extends PromptArgsRawShape ? ( + args: ShapeOutput, + extra: RequestHandlerExtra, + ) => GetPromptResult | Promise + : ( + extra: RequestHandlerExtra, + ) => GetPromptResult | Promise; export type RegisteredPrompt = { title?: string; @@ -1505,7 +1856,7 @@ function promptArgumentsFromSchema(schema: AnyObjectSchema): PromptArgument[] { return { name, description, - required: !isOptional + required: !isOptional, }; }); } @@ -1514,16 +1865,16 @@ 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'); + 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') { + if (typeof value === "string") { return value; } - throw new Error('Schema method literal must be a string'); + throw new Error("Schema method literal must be a string"); } function createCompletionResult(suggestions: string[]): CompleteResult { @@ -1531,14 +1882,14 @@ function createCompletionResult(suggestions: string[]): CompleteResult { completion: { values: suggestions.slice(0, 100), total: suggestions.length, - hasMore: suggestions.length > 100 - } + hasMore: suggestions.length > 100, + }, }; } const EMPTY_COMPLETION_RESULT: CompleteResult = { completion: { values: [], - hasMore: false - } + hasMore: false, + }, }; From 6a3a64690968b79525b7bbcd35952a5b5da0d2a4 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 00:09:09 +0530 Subject: [PATCH 02/22] feat: add and types for intercepting requests --- packages/server/src/server/mcp.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index e200cd691..68e5b764f 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -66,6 +66,31 @@ import { getCompleter, isCompletable } from "./completable.js"; import type { ServerOptions } from "./server.js"; import { Server } from "./server.js"; +/** + * Context passed to MCP middleware functions. + */ +export interface McpMiddlewareContext { + /** + * The incoming JSON-RPC request. + */ + request: ServerRequest; + + /** + * Additional metadata passed from the transport or SDK. + */ + extra: RequestHandlerExtra; +} + +/** + * Middleware function for intercepting MCP requests. + * @param context The request context. + * @param next A function that calls the next middleware or the implementation handler. + */ +export type McpMiddleware = ( + context: McpMiddleware, + next: () => Promise, +) => Promise; + /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying From c3b0c875e16b344b05f43e1d551f228a5eabc2b5 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 10:52:57 +0530 Subject: [PATCH 03/22] feat: add private helper method for handling server requests --- packages/server/src/server/mcp.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 68e5b764f..1fc91550f 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1625,6 +1625,17 @@ export class ResourceTemplate { ): CompleteResourceTemplateCallback | undefined { return this._callbacks.complete?.[variable]; } + + private async _executeRequest( + handler: ( + request: RequestT, + extra: RequestHandlerExtra, + ) => Promise, + request: RequestT, + extra: RequestHandlerExtra, + ): Promise { + return handler(request, extra); + } } export type BaseToolCallback< From 26021ced3ba89b03e9c973052d8b9ce0177117d1 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 14:18:05 +0530 Subject: [PATCH 04/22] refactor: Wrap and handlers with and add new request-related types --- packages/server/src/server/mcp.ts | 493 ++++++++++++++++++------------ 1 file changed, 299 insertions(+), 194 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 1fc91550f..b57c6c624 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -7,16 +7,20 @@ import type { CompleteRequestPrompt, CompleteRequestResourceTemplate, CompleteResult, + CreateTaskRequestHandlerExtra, CreateTaskResult, GetPromptResult, Implementation, ListPromptsResult, + ListResourcesRequest, ListResourcesResult, + ListToolsRequest, ListToolsResult, LoggingMessageNotification, Prompt, PromptArgument, PromptReference, + ReadResourceRequest, ReadResourceResult, RequestHandlerExtra, Resource, @@ -168,140 +172,180 @@ export class McpServer { this.server.setRequestHandler( ListToolsRequestSchema, - (): ListToolsResult => ({ - tools: Object.entries(this._registeredTools) - .filter(([, tool]) => tool.enabled) - .map(([name, tool]): Tool => { - const toolDefinition: Tool = { - name, - title: tool.title, - description: tool.description, - inputSchema: (() => { - const obj = normalizeObjectSchema( - tool.inputSchema, - ); - return obj - ? (toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: "input", - }) as Tool["inputSchema"]) - : EMPTY_OBJECT_JSON_SCHEMA; - })(), - annotations: tool.annotations, - execution: tool.execution, - _meta: tool._meta, - }; - - if (tool.outputSchema) { - const obj = normalizeObjectSchema( - tool.outputSchema, - ); - if (obj) { - toolDefinition.outputSchema = - toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: "output", - }) as Tool["outputSchema"]; - } - } - - return toolDefinition; + ( + request: ListToolsRequest, + extra: RequestHandlerExtra< + ListToolsRequest, + ServerNotification + >, + ) => this._executeRequest< + ListToolsResult, + ListToolsRequest, + RequestHandlerExtra + >( + (): Promise => + Promise.resolve({ + tools: Object.entries(this._registeredTools) + .filter(([, tool]) => tool.enabled) + .map(([name, tool]): Tool => { + const toolDefinition: Tool = { + name, + title: tool.title, + description: tool.description, + inputSchema: (() => { + const obj = normalizeObjectSchema( + tool.inputSchema, + ); + return obj + ? (toJsonSchemaCompat(obj, { + strictUnions: true, + pipeStrategy: "input", + }) as Tool["inputSchema"]) + : EMPTY_OBJECT_JSON_SCHEMA; + })(), + annotations: tool.annotations, + execution: tool.execution, + _meta: tool._meta, + }; + + if (tool.outputSchema) { + const obj = normalizeObjectSchema( + tool.outputSchema, + ); + if (obj) { + toolDefinition.outputSchema = + toJsonSchemaCompat(obj, { + strictUnions: true, + pipeStrategy: "output", + }) as Tool["outputSchema"]; + } + } + + return toolDefinition; + }), }), - }), + request, + extra, + ), ); this.server.setRequestHandler( CallToolRequestSchema, - async ( - request, - extra, - ): Promise => { - try { - const tool = this._registeredTools[request.params.name]; - if (!tool) { - throw new McpError( - ErrorCode.InvalidParams, - `Tool ${request.params.name} not found`, - ); - } - if (!tool.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Tool ${request.params.name} disabled`, - ); - } + ( + request: CallToolRequest, + extra: RequestHandlerExtra< + CallToolRequest, + ServerNotification + >, + ) => this._executeRequest< + CallToolResult | CreateTaskResult, + CallToolRequest, + RequestHandlerExtra + >( + async ( + request: CallToolRequest, + extra: RequestHandlerExtra< + CallToolRequest, + ServerNotification + >, + ): Promise => { + try { + const tool = this._registeredTools[request.params.name]; + if (!tool) { + throw new McpError( + ErrorCode.InvalidParams, + `Tool ${request.params.name} not found`, + ); + } + if (!tool.enabled) { + throw new McpError( + ErrorCode.InvalidParams, + `Tool ${request.params.name} disabled`, + ); + } - const isTaskRequest = !!request.params.task; - const taskSupport = tool.execution?.taskSupport; - const isTaskHandler = "createTask" in - (tool.handler as AnyToolHandler); + const isTaskRequest = !!request.params.task; + const taskSupport = tool.execution?.taskSupport; + const isTaskHandler = "createTask" in + (tool.handler as AnyToolHandler< + ZodRawShapeCompat + >); + + // Validate task hint configuration + if ( + (taskSupport === "required" || + taskSupport === "optional") && + !isTaskHandler + ) { + throw new McpError( + ErrorCode.InternalError, + `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`, + ); + } - // Validate task hint configuration - if ( - (taskSupport === "required" || - taskSupport === "optional") && !isTaskHandler - ) { - throw new McpError( - ErrorCode.InternalError, - `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`, - ); - } + // Handle taskSupport 'required' without task augmentation + if (taskSupport === "required" && !isTaskRequest) { + throw new McpError( + ErrorCode.MethodNotFound, + `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`, + ); + } - // Handle taskSupport 'required' without task augmentation - if (taskSupport === "required" && !isTaskRequest) { - throw new McpError( - ErrorCode.MethodNotFound, - `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`, - ); - } + // Handle taskSupport 'optional' without task augmentation - automatic polling + if ( + taskSupport === "optional" && !isTaskRequest && + isTaskHandler + ) { + return await this.handleAutomaticTaskPolling( + tool, + request, + extra, + ); + } - // Handle taskSupport 'optional' without task augmentation - automatic polling - if ( - taskSupport === "optional" && !isTaskRequest && - isTaskHandler - ) { - return await this.handleAutomaticTaskPolling( + // Normal execution path + const args = await this.validateToolInput( tool, - request, + request.params.arguments, + request.params.name, + ); + const result = await this.executeToolHandler( + tool, + args, extra, ); - } - // Normal execution path - const args = await this.validateToolInput( - tool, - request.params.arguments, - request.params.name, - ); - const result = await this.executeToolHandler( - tool, - args, - extra, - ); + // Return CreateTaskResult immediately for task requests + if (isTaskRequest) { + return result; + } - // Return CreateTaskResult immediately for task requests - if (isTaskRequest) { + // Validate output schema for non-task requests + await this.validateToolOutput( + tool, + result, + request.params.name, + ); return result; - } - - // Validate output schema for non-task requests - await this.validateToolOutput( - tool, - result, - request.params.name, - ); - return result; - } catch (error) { - if (error instanceof McpError) { - if (error.code === ErrorCode.UrlElicitationRequired) { - throw error; // Return the error to the caller without wrapping in CallToolResult + } catch (error) { + if (error instanceof McpError) { + if ( + error.code === + ErrorCode.UrlElicitationRequired + ) { + throw error; + } } + return this.createToolError( + error instanceof Error + ? error.message + : String(error), + ); } - return this.createToolError( - error instanceof Error ? error.message : String(error), - ); - } - }, + }, + request, + extra, + ), ); this._toolHandlersInitialized = true; @@ -409,10 +453,12 @@ export class McpServer { /** * Executes a tool handler (either regular or task-based). */ - private async executeToolHandler( + private async executeToolHandler< + ExtraT extends RequestHandlerExtra, + >( tool: RegisteredTool, args: unknown, - extra: RequestHandlerExtra, + extra: ExtraT, ): Promise { const handler = tool.handler as AnyToolHandler< ZodRawShapeCompat | undefined @@ -431,13 +477,18 @@ export class McpServer { >; // eslint-disable-next-line @typescript-eslint/no-explicit-any return await Promise.resolve( - typedHandler.createTask(args as any, taskExtra), + typedHandler.createTask( + args as any, + taskExtra as unknown as CreateTaskRequestHandlerExtra, + ), ); } else { const typedHandler = handler as ToolTaskHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any return await Promise.resolve( - (typedHandler.createTask as any)(taskExtra), + (typedHandler.createTask as any)( + taskExtra as unknown as CreateTaskRequestHandlerExtra, + ), ); } } @@ -456,10 +507,13 @@ export class McpServer { /** * Handles automatic task polling for tools with taskSupport 'optional'. */ - private async handleAutomaticTaskPolling( + private async handleAutomaticTaskPolling< + RequestT extends CallToolRequest, + ExtraT extends RequestHandlerExtra, + >( tool: RegisteredTool, request: RequestT, - extra: RequestHandlerExtra, + extra: ExtraT, ): Promise { if (!extra.taskStore) { throw new Error("No task store provided for task-capable tool."); @@ -480,13 +534,13 @@ export class McpServer { ? await Promise.resolve( (handler as ToolTaskHandler).createTask( args, - taskExtra, + taskExtra as unknown as CreateTaskRequestHandlerExtra, ), ) // eslint-disable-next-line @typescript-eslint/no-explicit-any : await Promise.resolve( ((handler as ToolTaskHandler).createTask as any)( - taskExtra, + taskExtra as unknown as CreateTaskRequestHandlerExtra, ), ); @@ -658,39 +712,64 @@ export class McpServer { this.server.setRequestHandler( ListResourcesRequestSchema, - async (request, extra) => { - const resources = Object.entries(this._registeredResources) - .filter(([_, resource]) => resource.enabled) - .map(([uri, resource]) => ({ - uri, - name: resource.name, - ...resource.metadata, - })); - - const templateResources: Resource[] = []; - for ( - const template of Object.values( - this._registeredResourceTemplates, + ( + request: ListResourcesRequest, + extra: RequestHandlerExtra< + ListResourcesRequest, + ServerNotification + >, + ) => this._executeRequest< + ListResourcesResult, + ListResourcesRequest, + RequestHandlerExtra + >( + async ( + request: ListResourcesRequest, + extra: RequestHandlerExtra< + ListResourcesRequest, + ServerNotification + >, + ) => { + const resources = Object.entries( + this._registeredResources, ) - ) { - if (!template.resourceTemplate.listCallback) { - continue; - } + .filter(([_, resource]) => resource.enabled) + .map(([uri, resource]) => ({ + uri, + name: resource.name, + ...resource.metadata, + })); + + const templateResources: Resource[] = []; + for ( + const template of Object.values( + this._registeredResourceTemplates, + ) + ) { + if (!template.resourceTemplate.listCallback) { + continue; + } - const result = await template.resourceTemplate.listCallback( - extra, - ); - for (const resource of result.resources) { - templateResources.push({ - ...template.metadata, - // the defined resource metadata should override the template metadata if present - ...resource, - }); + const result = await template.resourceTemplate + .listCallback( + extra as any, + ); + for (const resource of result.resources) { + templateResources.push({ + ...template.metadata, + // the defined resource metadata should override the template metadata if present + ...resource, + }); + } } - } - return { resources: [...resources, ...templateResources] }; - }, + return { + resources: [...resources, ...templateResources], + }; + }, + request, + extra, + ), ); this.server.setRequestHandler( @@ -711,39 +790,65 @@ export class McpServer { this.server.setRequestHandler( ReadResourceRequestSchema, - async (request, extra) => { - const uri = new URL(request.params.uri); - - // First check for exact resource match - const resource = this._registeredResources[uri.toString()]; - if (resource) { - if (!resource.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Resource ${uri} disabled`, - ); + ( + request: ReadResourceRequest, + extra: RequestHandlerExtra< + ReadResourceRequest, + ServerNotification + >, + ) => this._executeRequest< + ReadResourceResult, + ReadResourceRequest, + RequestHandlerExtra + >( + async ( + request: ReadResourceRequest, + extra: RequestHandlerExtra< + ReadResourceRequest, + ServerNotification + >, + ) => { + const uri = new URL(request.params.uri); + + // First check for exact resource match + const resource = this._registeredResources[uri.toString()]; + if (resource) { + if (!resource.enabled) { + throw new McpError( + ErrorCode.InvalidParams, + `Resource ${uri} disabled`, + ); + } + return resource.readCallback(uri, extra as any); } - return resource.readCallback(uri, extra); - } - // Then check templates - for ( - const template of Object.values( - this._registeredResourceTemplates, - ) - ) { - const variables = template.resourceTemplate.uriTemplate - .match(uri.toString()); - if (variables) { - return template.readCallback(uri, variables, extra); + // Then check templates + for ( + const template of Object.values( + this._registeredResourceTemplates, + ) + ) { + const variables = template.resourceTemplate + .uriTemplate.match( + uri.toString(), + ); + if (variables) { + return template.readCallback( + uri, + variables, + extra as any, + ); + } } - } - throw new McpError( - ErrorCode.InvalidParams, - `Resource ${uri} not found`, - ); - }, + throw new McpError( + ErrorCode.InvalidParams, + `Resource ${uri} not found`, + ); + }, + request, + extra, + ), ); this._resourceHandlersInitialized = true; @@ -1563,6 +1668,17 @@ export class McpServer { this.server.sendPromptListChanged(); } } + + private async _executeRequest( + handler: ( + request: RequestT, + extra: ExtraT, + ) => Promise, + request: RequestT, + extra: ExtraT, + ): Promise { + return handler(request, extra); + } } /** @@ -1625,17 +1741,6 @@ export class ResourceTemplate { ): CompleteResourceTemplateCallback | undefined { return this._callbacks.complete?.[variable]; } - - private async _executeRequest( - handler: ( - request: RequestT, - extra: RequestHandlerExtra, - ) => Promise, - request: RequestT, - extra: RequestHandlerExtra, - ): Promise { - return handler(request, extra); - } } export type BaseToolCallback< From a58eced086ccebbdc25816c46fec84c4d7a17102 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 14:21:10 +0530 Subject: [PATCH 05/22] refactor: update context parameter type to --- packages/server/src/server/mcp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index b57c6c624..f1ec7dd96 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -91,7 +91,7 @@ export interface McpMiddlewareContext { * @param next A function that calls the next middleware or the implementation handler. */ export type McpMiddleware = ( - context: McpMiddleware, + context: McpMiddlewareContext, next: () => Promise, ) => Promise; From b02c3ef80c4eefb81ae33216fb1e193be91b7823 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 14:33:07 +0530 Subject: [PATCH 06/22] feat: Add middleware support to McpServer's call handler --- packages/server/src/server/mcp.ts | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index f1ec7dd96..d2be2c251 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -112,6 +112,7 @@ export class McpServer { } = {}; private _registeredTools: { [name: string]: RegisteredTool } = {}; private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; + private _middleware: McpMiddleware[] = []; private _experimental?: { tasks: ExperimentalMcpServerTasks }; constructor(serverInfo: Implementation, options?: ServerOptions) { @@ -1677,7 +1678,56 @@ export class McpServer { request: RequestT, extra: ExtraT, ): Promise { - return handler(request, extra); + const middleware = this._middleware; + + // Optimized path: If there are no middleware, just run the handler + if (middleware.length === 0) { + return handler(request, extra); + } + + let result: ResultT | undefined; + let handlerError: unknown; + + // Wrap the handler as the final middleware + const leafMiddleware: McpMiddleware = async (_context, _next) => { + try { + result = await handler(request, extra); + } catch (e) { + handlerError = e; + } + }; + + const chain = [...middleware, leafMiddleware]; + + // Execute the chain + const executeChain = async (i: number): Promise => { + if (i >= chain.length) { + return; + } + const fn = chain[i] as McpMiddleware; + + // Protect against creating a context with incorrect types by casting + const context: McpMiddlewareContext = { + request: request as unknown as ServerRequest, + extra: extra as unknown as RequestHandlerExtra< + ServerRequest, + ServerNotification + >, + }; + + await fn(context, async () => { + await executeChain(i + 1); + }); + }; + + await executeChain(0); + + if (handlerError) { + throw handlerError; + } + + // Return result, asserting it exists (handlers should generally return something) + return result as ResultT; } } From f13e8dc69a039d34bd76bfc6b9740f73f37a77c5 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 14:36:28 +0530 Subject: [PATCH 07/22] feat: add method to register middleware functions --- packages/server/src/server/mcp.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index d2be2c251..4a5e414fd 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -135,6 +135,14 @@ export class McpServer { return this._experimental; } + /** + * Registers a middleware function. + * @param middleware The middleware to register. + */ + public use(middleware: McpMiddleware) { + this._middleware.push(middleware); + } + /** * Attaches to the given transport, starts it, and starts listening for messages. * From d7550f06ebf07715802daa6d4da87cc702ff9f87 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 14:39:02 +0530 Subject: [PATCH 08/22] feat: Prevent middleware registration after server connection or initial request processing --- packages/server/src/server/mcp.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 4a5e414fd..7a045488a 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -113,6 +113,7 @@ export class McpServer { private _registeredTools: { [name: string]: RegisteredTool } = {}; private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; private _middleware: McpMiddleware[] = []; + private _middlewareFrozen = false; private _experimental?: { tasks: ExperimentalMcpServerTasks }; constructor(serverInfo: Implementation, options?: ServerOptions) { @@ -140,6 +141,11 @@ export class McpServer { * @param middleware The middleware to register. */ public use(middleware: McpMiddleware) { + if (this._middlewareFrozen) { + throw new Error( + "Cannot register middleware after the server has started or processed requests.", + ); + } this._middleware.push(middleware); } @@ -149,6 +155,7 @@ export class McpServer { * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. */ async connect(transport: Transport): Promise { + this._middlewareFrozen = true; return await this.server.connect(transport); } @@ -1686,6 +1693,7 @@ export class McpServer { request: RequestT, extra: ExtraT, ): Promise { + this._middlewareFrozen = true; const middleware = this._middleware; // Optimized path: If there are no middleware, just run the handler From 6b0a62841a0f2afd7e6171ba78ca847c61a495bf Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 14:58:36 +0530 Subject: [PATCH 09/22] test: Add middleware tests covering execution order, short-circuiting, and error handling --- packages/server/test/server/mcpServer.test.ts | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 packages/server/test/server/mcpServer.test.ts diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts new file mode 100644 index 000000000..0c5a8b8b7 --- /dev/null +++ b/packages/server/test/server/mcpServer.test.ts @@ -0,0 +1,191 @@ +import { McpServer } from "../../src/server/mcp.js"; +import { JSONRPCMessage } from "@modelcontextprotocol/core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("McpServer Middleware", () => { + let server: McpServer; + + beforeEach(() => { + server = new McpServer({ + name: "test-server", + version: "1.0.0", + }); + }); + + // Helper to simulate a tool call and capture the response + async function simulateCallTool(toolName: string): Promise { + let serverOnMessage: (message: any) => Promise; + let capturedResponse: JSONRPCMessage | undefined; + let resolveSend: () => void; + const sendPromise = new Promise((resolve) => { + resolveSend = resolve; + }); + + const transport = { + start: vi.fn(), + send: vi.fn().mockImplementation(async (msg) => { + capturedResponse = msg as JSONRPCMessage; + resolveSend(); + }), + close: vi.fn(), + set onmessage(handler: any) { + serverOnMessage = handler; + }, + }; + + await server.connect(transport); + + const request = { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: toolName, + arguments: {}, + }, + }; + + if (!serverOnMessage!) { + throw new Error("Server did not attach onMessage listener"); + } + + // Trigger request + serverOnMessage(request); + + // Wait for response + await sendPromise; + + return capturedResponse!; + } + + it("should execute middleware in registration order (Onion model)", async () => { + const sequence: string[] = []; + + server.use(async (context, next) => { + sequence.push("mw1 start"); + await next(); + sequence.push("mw1 end"); + }); + + server.use(async (context, next) => { + sequence.push("mw2 start"); + await next(); + sequence.push("mw2 end"); + }); + + server.tool("test-tool", {}, async () => { + sequence.push("handler"); + return { content: [{ type: "text", text: "result" }] }; + }); + + await simulateCallTool("test-tool"); + + expect(sequence).toEqual([ + "mw1 start", + "mw2 start", + "handler", + "mw2 end", + "mw1 end", + ]); + }); + + it("should short-circuit if next() is not called", async () => { + const sequence: string[] = []; + + server.use(async (context, next) => { + sequence.push("mw1 start"); + // next() NOT called + sequence.push("mw1 end"); + }); + + server.use(async (context, next) => { + sequence.push("mw2 start"); + await next(); + }); + + server.tool("test-tool", {}, async () => { + sequence.push("handler"); + return { content: [{ type: "text", text: "result" }] }; + }); + + await simulateCallTool("test-tool"); + + // mw2 and handler should NOT run + expect(sequence).toEqual(["mw1 start", "mw1 end"]); + }); + + it("should execute middleware for other methods (e.g. tools/list)", async () => { + // For this check, we need to simulate tools/list. + // We can adapt our helper or just copy-paste a simplified version here for variety. + const sequence: string[] = []; + server.use(async (context, next) => { + sequence.push("mw"); + await next(); + }); + + // Register a dummy tool to ensure tools/list handler is set up + server.tool("dummy", {}, async () => ({ content: [] })); + + let serverOnMessage: any; + let resolveSend: any; + const p = new Promise((r) => resolveSend = r); + const transport = { + start: vi.fn(), + send: vi.fn().mockImplementation(() => resolveSend()), + close: vi.fn(), + set onmessage(h: any) { + serverOnMessage = h; + }, + }; + await server.connect(transport); + + serverOnMessage({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {}, + }); + await p; + + expect(sequence).toEqual(["mw"]); + }); + + it("should allow middleware to catch errors from downstream", async () => { + server.use(async (context, next) => { + try { + await next(); + } catch (e) { + // Suppress error + } + }); + + server.tool("error-tool", {}, async () => { + throw new Error("Boom"); + }); + + const response = await simulateCallTool("error-tool"); + + // Since middleware swallowed the error, the handler returns undefined (or whatever executed). + // Actually, if handler throws and middleware catches, `result` in `_executeRequest` will be undefined. + // The server transport might expect a result. + // Typescript core SDK might throw if result is missing maybe? + // Or it sends a success response with "undefined"? + + // Let's check what response we got. If error was swallowed, it shouldn't be an error response. + expect((response as any).error).toBeUndefined(); + }); + + it("should propagate errors if middleware throws", async () => { + server.use(async (context, next) => { + throw new Error("Middleware Error"); + }); + + server.tool("test-tool", {}, async () => ({ content: [] })); + + const response = await simulateCallTool("test-tool"); + + // Standard JSON-RPC error response + expect((response as any).error).toBeDefined(); + expect((response as any).error.message).toContain("Middleware Error"); + }); +}); From 51c003ab0a65293bbfe95a9d3ce5ef03b11dcc80 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 15:51:21 +0530 Subject: [PATCH 10/22] feat: Prevent multiple calls to in middleware and add a corresponding test --- packages/server/src/server/mcp.ts | 7 +++++++ packages/server/test/server/mcpServer.test.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 7a045488a..42a0a9e42 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1731,7 +1731,14 @@ export class McpServer { >, }; + let nextCalled = false; await fn(context, async () => { + if (nextCalled) { + throw new Error( + "next() called multiple times in middleware", + ); + } + nextCalled = true; await executeChain(i + 1); }); }; diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index 0c5a8b8b7..e5d9f5172 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -188,4 +188,21 @@ describe("McpServer Middleware", () => { expect((response as any).error).toBeDefined(); expect((response as any).error.message).toContain("Middleware Error"); }); + + it("should throw an error if next() is called multiple times", async () => { + server.use(async (context, next) => { + await next(); + await next(); // Second call should throw + }); + + server.tool("test-tool", {}, async () => ({ content: [] })); + + const response = await simulateCallTool("test-tool"); + + // Expect an error response due to double-call + expect((response as any).error).toBeDefined(); + expect((response as any).error.message).toContain( + "next() called multiple times", + ); + }); }); From 2e6ea43067024942ea3486d0bb30d74ce9762c7f Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 15:53:10 +0530 Subject: [PATCH 11/22] test: ensure middleware cannot be registered after server connection --- packages/server/test/server/mcpServer.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index e5d9f5172..df6bdb71d 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -205,4 +205,22 @@ describe("McpServer Middleware", () => { "next() called multiple times", ); }); + + it("should throw an error if use() is called after connect()", async () => { + const transport = { + start: vi.fn(), + send: vi.fn(), + close: vi.fn(), + set onmessage(_handler: any) {}, + }; + + await server.connect(transport); + + // Trying to register middleware after connect should throw + expect(() => { + server.use(async (context, next) => { + await next(); + }); + }).toThrow("Cannot register middleware after the server has started"); + }); }); From e98cadb150d2232a551a74ed4da41700a1ef3c6d Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 16:04:45 +0530 Subject: [PATCH 12/22] test: add real-world use case tests for middleware covering logging, authentication, and activity aggregation --- packages/server/test/server/mcpServer.test.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index df6bdb71d..e96355dfa 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -223,4 +223,124 @@ describe("McpServer Middleware", () => { }); }).toThrow("Cannot register middleware after the server has started"); }); + + // ============================================================ + // Real World Use Case Integration Tests + // ============================================================ + + describe("Real World Use Cases", () => { + it("Logging: should observe request method and capture response timing", async () => { + const logs: { method: string; durationMs: number }[] = []; + + server.use(async (context, next) => { + const start = Date.now(); + const method = (context.request as any).method || "unknown"; + + await next(); + + const durationMs = Date.now() - start; + logs.push({ method, durationMs }); + }); + + server.tool("fast-tool", {}, async () => { + return { content: [{ type: "text", text: "done" }] }; + }); + + await simulateCallTool("fast-tool"); + + expect(logs).toHaveLength(1); + expect(logs[0]!.method).toBe("tools/call"); + expect(logs[0]!.durationMs).toBeGreaterThanOrEqual(0); + }); + + it("Auth: should short-circuit unauthorized requests", async () => { + const VALID_TOKEN = "secret-token"; + + server.use(async (context, next) => { + // Simulate checking for an auth token in extra/authInfo + const authInfo = (context.extra as any)?.authInfo; + + // In real usage, authInfo would come from the transport. + // For this test, we simulate by checking a header-like property. + // Since we can't inject authInfo easily, we'll check a custom property. + const token = (context.request as any).params?._authToken; + + if (token !== VALID_TOKEN) { + // Short-circuit: don't call next(), effectively blocking the request + // In a real scenario, you might throw an error or set a response + throw new Error("Unauthorized"); + } + + await next(); + }); + + server.tool("protected-tool", {}, async () => { + return { content: [{ type: "text", text: "secret data" }] }; + }); + + // Simulate unauthorized request (no token) + const response = await simulateCallTool("protected-tool"); + + expect((response as any).error).toBeDefined(); + expect((response as any).error.message).toContain("Unauthorized"); + }); + + it("Activity Aggregation: should intercept tools/list and count discoveries", async () => { + let toolListCount = 0; + let toolCallCount = 0; + + server.use(async (context, next) => { + const method = (context.request as any).method; + + if (method === "tools/list") { + toolListCount++; + } else if (method === "tools/call") { + toolCallCount++; + } + + await next(); + }); + + server.tool("my-tool", {}, async () => ({ content: [] })); + + // Simulate tools/list + let serverOnMessage: any; + let resolveSend: any; + const p = new Promise((r) => (resolveSend = r)); + const transport = { + start: vi.fn(), + send: vi.fn().mockImplementation(() => resolveSend()), + close: vi.fn(), + set onmessage(h: any) { + serverOnMessage = h; + }, + }; + await server.connect(transport); + + // First: tools/list + serverOnMessage({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {}, + }); + await p; + + // Second: tools/call (need new promise) + let resolveSend2: any; + const p2 = new Promise((r) => (resolveSend2 = r)); + transport.send.mockImplementation(() => resolveSend2()); + + serverOnMessage({ + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name: "my-tool", arguments: {} }, + }); + await p2; + + expect(toolListCount).toBe(1); + expect(toolCallCount).toBe(1); + }); + }); }); From 6f70de5c1c9f9f5c9d53a9b68818eb578d4d5d44 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sat, 27 Dec 2025 16:10:17 +0530 Subject: [PATCH 13/22] test: Add failure mode verification tests for mcpServer middleware and tool handler error propagation --- packages/server/test/server/mcpServer.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index e96355dfa..bded6b282 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -343,4 +343,115 @@ describe("McpServer Middleware", () => { expect(toolCallCount).toBe(1); }); }); + + // ============================================================ + // Failure Mode Verification Tests + // ============================================================ + + describe("Failure Mode Verification", () => { + it("Pre-next: error thrown before next() maps to JSON-RPC error", async () => { + server.use(async (context, next) => { + // Error thrown BEFORE calling next() + throw new Error("Pre-next failure"); + }); + + server.tool("test-tool", {}, async () => ({ content: [] })); + + const response = await simulateCallTool("test-tool"); + + // Should be a proper JSON-RPC error response + expect((response as any).jsonrpc).toBe("2.0"); + expect((response as any).id).toBe(1); + expect((response as any).error).toBeDefined(); + expect((response as any).error.message).toContain( + "Pre-next failure", + ); + // Server should not crash - we got a response + }); + + it("Post-next: error thrown after next() maps to JSON-RPC error", async () => { + server.use(async (context, next) => { + await next(); + // Error thrown AFTER calling next() + throw new Error("Post-next failure"); + }); + + server.tool("test-tool", {}, async () => ({ content: [] })); + + const response = await simulateCallTool("test-tool"); + + // Should be a proper JSON-RPC error response + expect((response as any).jsonrpc).toBe("2.0"); + expect((response as any).id).toBe(1); + expect((response as any).error).toBeDefined(); + expect((response as any).error.message).toContain( + "Post-next failure", + ); + }); + + it("Handler: error thrown in tool handler returns error result (SDK behavior)", async () => { + // No middleware - test pure handler error + server.tool("failing-tool", {}, async () => { + throw new Error("Handler failure"); + }); + + const response = await simulateCallTool("failing-tool"); + + // MCP SDK converts handler errors to result with isError: true + // (not JSON-RPC error - this is intentional SDK behavior) + expect((response as any).jsonrpc).toBe("2.0"); + expect((response as any).id).toBe(1); + expect((response as any).result).toBeDefined(); + expect((response as any).result.isError).toBe(true); + expect((response as any).result.content[0]!.text).toContain( + "Handler failure", + ); + }); + + it("Multiple middleware: error in second middleware propagates correctly", async () => { + const sequence: string[] = []; + + server.use(async (context, next) => { + sequence.push("mw1 start"); + try { + await next(); + } catch (e) { + sequence.push("mw1 caught"); + throw e; // Re-throw to propagate + } + sequence.push("mw1 end"); + }); + + server.use(async (context, next) => { + sequence.push("mw2 start"); + throw new Error("mw2 failure"); + }); + + server.tool("test-tool", {}, async () => ({ content: [] })); + + const response = await simulateCallTool("test-tool"); + + expect((response as any).error).toBeDefined(); + expect((response as any).error.message).toContain("mw2 failure"); + // Verify mw1 caught the error + expect(sequence).toContain("mw1 caught"); + // mw1 end should NOT be in sequence since error was re-thrown + expect(sequence).not.toContain("mw1 end"); + }); + + it("Error contains proper JSON-RPC error code", async () => { + server.use(async (context, next) => { + throw new Error("Generic middleware error"); + }); + + server.tool("test-tool", {}, async () => ({ content: [] })); + + const response = await simulateCallTool("test-tool"); + + expect((response as any).error).toBeDefined(); + // JSON-RPC internal error code is -32603 + expect((response as any).error.code).toBeDefined(); + expect(typeof (response as any).error.code).toBe("number"); + }); + }); }); From 014342295a2ab3a3dfbb84b03f9852e28bded0cf Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 06:02:28 +0530 Subject: [PATCH 14/22] feat: Add property to request handler context for cross-middleware communication --- packages/server/src/server/mcp.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 42a0a9e42..b6058c9fc 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -83,6 +83,11 @@ export interface McpMiddlewareContext { * Additional metadata passed from the transport or SDK. */ extra: RequestHandlerExtra; + + /** + * A generic key-value store for cross-middleware communication (e.g., attaching a user object after auth). + */ + state: Record; } /** @@ -1729,6 +1734,7 @@ export class McpServer { ServerRequest, ServerNotification >, + state: {}, }; let nextCalled = false; From 5a6a1724febd3c02ae5cb26cb318975672430920 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 06:10:31 +0530 Subject: [PATCH 15/22] feat: enable middleware state sharing via by initializing the context once and adding a verification test --- packages/server/src/server/mcp.ts | 20 +++---- packages/server/test/server/mcpServer.test.ts | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index b6058c9fc..fa0ec50a1 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1721,22 +1721,22 @@ export class McpServer { const chain = [...middleware, leafMiddleware]; // Execute the chain + // Protect against creating a context with incorrect types by casting + const context: McpMiddlewareContext = { + request: request as unknown as ServerRequest, + extra: extra as unknown as RequestHandlerExtra< + ServerRequest, + ServerNotification + >, + state: {}, + }; + const executeChain = async (i: number): Promise => { if (i >= chain.length) { return; } const fn = chain[i] as McpMiddleware; - // Protect against creating a context with incorrect types by casting - const context: McpMiddlewareContext = { - request: request as unknown as ServerRequest, - extra: extra as unknown as RequestHandlerExtra< - ServerRequest, - ServerNotification - >, - state: {}, - }; - let nextCalled = false; await fn(context, async () => { if (nextCalled) { diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index bded6b282..2769a4892 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -114,6 +114,58 @@ describe("McpServer Middleware", () => { expect(sequence).toEqual(["mw1 start", "mw1 end"]); }); + it("should allow middleware to communicate via ctx.state", async () => { + const server = new McpServer({ name: "test", version: "1.0" }); + server.use(async (ctx, next) => { + ctx.state.value = 1; + await next(); + }); + server.use(async (ctx, next) => { + ctx.state.value = (ctx.state.value as number) + 1; + await next(); + }); + + // Use a tool list request to trigger the chain + server.tool( + "test-tool", + {}, + async () => ({ content: [{ type: "text", text: "ok" }] }), + ); + + let capturedState: any; + server.use(async (ctx, next) => { + capturedState = ctx.state; + await next(); + }); + + let resolveSend: () => void; + const sendPromise = new Promise((resolve) => { + resolveSend = resolve; + }); + + const transport = { + start: vi.fn(), + send: vi.fn().mockImplementation(async () => { + resolveSend(); + }), + close: vi.fn(), + }; + await server.connect(transport as any); + // @ts-ignore + const onMsg = (server.server.transport as any).onmessage; + onMsg({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "test-tool", arguments: {} }, + }); + + await sendPromise; + + expect(capturedState).toBeDefined(); + expect(capturedState.value).toBe(2); + }); + it("should execute middleware for other methods (e.g. tools/list)", async () => { // For this check, we need to simulate tools/list. // We can adapt our helper or just copy-paste a simplified version here for variety. From 259103d80934531b8bed7810cad1f324e228a591 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 06:12:48 +0530 Subject: [PATCH 16/22] docs: clarify request mutability and mutation guidelines in McpMiddlewareContext --- packages/server/src/server/mcp.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index fa0ec50a1..1dd2fad1c 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -76,6 +76,8 @@ import { Server } from "./server.js"; export interface McpMiddlewareContext { /** * The incoming JSON-RPC request. + * While technically mutable, middleware should generally treat this as read-only. + * Mutation is permitted only for specific cases like schema normalization or request enrichment. */ request: ServerRequest; From 2a276cae70ad1fe58fac2640c83d7c45de411797 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 06:39:20 +0530 Subject: [PATCH 17/22] refactor: Wrap prompt handlers with and add a type constraint to its generic --- packages/server/src/server/mcp.ts | 150 +++++++++++++++++++----------- 1 file changed, 96 insertions(+), 54 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 1dd2fad1c..028637bf6 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -9,8 +9,10 @@ import type { CompleteResult, CreateTaskRequestHandlerExtra, CreateTaskResult, + GetPromptRequest, GetPromptResult, Implementation, + ListPromptsRequest, ListPromptsResult, ListResourcesRequest, ListResourcesResult, @@ -899,70 +901,106 @@ export class McpServer { this.server.setRequestHandler( ListPromptsRequestSchema, - (): ListPromptsResult => ({ - prompts: Object.entries(this._registeredPrompts) - .filter(([, prompt]) => prompt.enabled) - .map(([name, prompt]): Prompt => { - return { - name, - title: prompt.title, - description: prompt.description, - arguments: prompt.argsSchema - ? promptArgumentsFromSchema(prompt.argsSchema) - : undefined, - }; + ( + request: ListPromptsRequest, + extra: RequestHandlerExtra< + ListPromptsRequest, + ServerNotification + >, + ) => this._executeRequest< + ListPromptsResult, + ListPromptsRequest, + RequestHandlerExtra + >( + (): Promise => + Promise.resolve({ + prompts: Object.entries(this._registeredPrompts) + .filter(([, prompt]) => prompt.enabled) + .map(([name, prompt]): Prompt => { + return { + name, + title: prompt.title, + description: prompt.description, + arguments: prompt.argsSchema + ? promptArgumentsFromSchema( + prompt.argsSchema, + ) + : undefined, + }; + }), }), - }), + request, + extra, + ), ); this.server.setRequestHandler( GetPromptRequestSchema, - async (request, extra): Promise => { - const prompt = this._registeredPrompts[request.params.name]; - if (!prompt) { - throw new McpError( - ErrorCode.InvalidParams, - `Prompt ${request.params.name} not found`, - ); - } - - if (!prompt.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Prompt ${request.params.name} disabled`, - ); - } + ( + request: GetPromptRequest, + extra: RequestHandlerExtra< + GetPromptRequest, + ServerNotification + >, + ) => this._executeRequest< + GetPromptResult, + GetPromptRequest, + RequestHandlerExtra + >( + async ( + request: GetPromptRequest, + extra: RequestHandlerExtra< + GetPromptRequest, + ServerNotification + >, + ): Promise => { + const prompt = this._registeredPrompts[request.params.name]; + if (!prompt) { + throw new McpError( + ErrorCode.InvalidParams, + `Prompt ${request.params.name} not found`, + ); + } - if (prompt.argsSchema) { - const argsObj = normalizeObjectSchema( - prompt.argsSchema, - ) as AnyObjectSchema; - const parseResult = await safeParseAsync( - argsObj, - request.params.arguments, - ); - if (!parseResult.success) { - const error = "error" in parseResult - ? parseResult.error - : "Unknown error"; - const errorMessage = getParseErrorMessage(error); + if (!prompt.enabled) { throw new McpError( ErrorCode.InvalidParams, - `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`, + `Prompt ${request.params.name} disabled`, ); } - const args = parseResult.data; - const cb = prompt.callback as PromptCallback< - PromptArgsRawShape - >; - return await Promise.resolve(cb(args, extra)); - } else { - const cb = prompt.callback as PromptCallback; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve((cb as any)(extra)); - } - }, + if (prompt.argsSchema) { + const argsObj = normalizeObjectSchema( + prompt.argsSchema, + ) as AnyObjectSchema; + const parseResult = await safeParseAsync( + argsObj, + request.params.arguments, + ); + if (!parseResult.success) { + const error = "error" in parseResult + ? parseResult.error + : "Unknown error"; + const errorMessage = getParseErrorMessage(error); + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`, + ); + } + + const args = parseResult.data; + const cb = prompt.callback as PromptCallback< + PromptArgsRawShape + >; + return await Promise.resolve(cb(args, extra as any)); + } else { + const cb = prompt.callback as PromptCallback; + return await Promise.resolve((cb as any)(extra)); + } + }, + request, + extra, + ), ); this._promptHandlersInitialized = true; @@ -1692,7 +1730,11 @@ export class McpServer { } } - private async _executeRequest( + private async _executeRequest< + ResultT, + RequestT, + ExtraT extends RequestHandlerExtra, + >( handler: ( request: RequestT, extra: ExtraT, From d5ceadaef077dc26060bba22c18726a966a826a0 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 06:45:17 +0530 Subject: [PATCH 18/22] fix: explicitly type argument when invoking prompt callbacks --- packages/server/src/server/mcp.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 028637bf6..88345d43f 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -992,10 +992,25 @@ export class McpServer { const cb = prompt.callback as PromptCallback< PromptArgsRawShape >; - return await Promise.resolve(cb(args, extra as any)); + return await Promise.resolve( + cb( + args, + extra as unknown as RequestHandlerExtra< + ServerRequest, + ServerNotification + >, + ), + ); } else { const cb = prompt.callback as PromptCallback; - return await Promise.resolve((cb as any)(extra)); + return await Promise.resolve( + cb( + extra as unknown as RequestHandlerExtra< + ServerRequest, + ServerNotification + >, + ), + ); } }, request, From 326754844de79c4c28d3cee012d36862aafe2aa9 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 06:54:34 +0530 Subject: [PATCH 19/22] feat: enable chainable middleware registration and add test for async middleware timing --- packages/server/src/server/mcp.ts | 1 + packages/server/test/server/mcpServer.test.ts | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 88345d43f..b2026050b 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -156,6 +156,7 @@ export class McpServer { ); } this._middleware.push(middleware); + return this; } /** diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index 2769a4892..36ed7f141 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -258,6 +258,40 @@ describe("McpServer Middleware", () => { ); }); + it("should respect async timing (middleware can await)", async () => { + const sequence: string[] = []; + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + server.use(async (context, next) => { + sequence.push("mw1 start"); + await delay(10); // Wait 10ms + sequence.push("mw1 after delay"); + await next(); + sequence.push("mw1 end"); + }); + + server.use(async (context, next) => { + sequence.push("mw2 start"); + await next(); + }); + + server.tool("test-tool", {}, async () => { + sequence.push("handler"); + return { content: [] }; + }); + + await simulateCallTool("test-tool"); + + expect(sequence).toEqual([ + "mw1 start", + "mw1 after delay", + "mw2 start", + "handler", + "mw1 end", + ]); + }); + it("should throw an error if use() is called after connect()", async () => { const transport = { start: vi.fn(), From 7b88bd12ac3fc11fb77e314af3de3916a02b9570 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 19:28:54 +0530 Subject: [PATCH 20/22] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cc0a52b82..58cff53ff 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,4 @@ dist/ # IDE .idea/ -ahammednibras8 \ No newline at end of file +# ahammednibras8 \ No newline at end of file From ed43f1fac85345d53c244b5fd919fd9bc77df2b9 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 20:05:38 +0530 Subject: [PATCH 21/22] feat(server): finalize middleware types and remove any usage --- packages/server/src/server/mcp.ts | 1338 ++++++----------- packages/server/test/server/mcpServer.test.ts | 301 ++-- 2 files changed, 615 insertions(+), 1024 deletions(-) diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index b2026050b..19ff4ceae 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -37,8 +37,8 @@ import type { ToolExecution, Transport, Variables, - ZodRawShapeCompat, -} from "@modelcontextprotocol/core"; + ZodRawShapeCompat +} from '@modelcontextprotocol/core'; import { assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, @@ -62,15 +62,15 @@ import { safeParseAsync, toJsonSchemaCompat, UriTemplate, - validateAndWarnToolName, -} from "@modelcontextprotocol/core"; -import { ZodOptional } from "zod"; + validateAndWarnToolName +} from '@modelcontextprotocol/core'; +import { ZodOptional } from 'zod'; -import type { ToolTaskHandler } from "../experimental/tasks/interfaces.js"; -import { ExperimentalMcpServerTasks } from "../experimental/tasks/mcp-server.js"; -import { getCompleter, isCompletable } from "./completable.js"; -import type { ServerOptions } from "./server.js"; -import { Server } from "./server.js"; +import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; +import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.js'; +import { getCompleter, isCompletable } from './completable.js'; +import type { ServerOptions } from './server.js'; +import { Server } from './server.js'; /** * Context passed to MCP middleware functions. @@ -99,10 +99,7 @@ export interface McpMiddlewareContext { * @param context The request context. * @param next A function that calls the next middleware or the implementation handler. */ -export type McpMiddleware = ( - context: McpMiddlewareContext, - next: () => Promise, -) => Promise; +export type McpMiddleware = (context: McpMiddlewareContext, next: () => Promise) => Promise; /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. @@ -139,7 +136,7 @@ export class McpServer { get experimental(): { tasks: ExperimentalMcpServerTasks } { if (!this._experimental) { this._experimental = { - tasks: new ExperimentalMcpServerTasks(this), + tasks: new ExperimentalMcpServerTasks(this) }; } return this._experimental; @@ -151,9 +148,7 @@ export class McpServer { */ public use(middleware: McpMiddleware) { if (this._middlewareFrozen) { - throw new Error( - "Cannot register middleware after the server has started or processed requests.", - ); + throw new Error('Cannot register middleware after the server has started or processed requests.'); } this._middleware.push(middleware); return this; @@ -183,195 +178,130 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler( - getMethodValue(ListToolsRequestSchema), - ); - this.server.assertCanSetRequestHandler( - getMethodValue(CallToolRequestSchema), - ); + this.server.assertCanSetRequestHandler(getMethodValue(ListToolsRequestSchema)); + this.server.assertCanSetRequestHandler(getMethodValue(CallToolRequestSchema)); this.server.registerCapabilities({ tools: { - listChanged: true, - }, + listChanged: true + } }); this.server.setRequestHandler( ListToolsRequestSchema, - ( - request: ListToolsRequest, - extra: RequestHandlerExtra< - ListToolsRequest, - ServerNotification - >, - ) => this._executeRequest< - ListToolsResult, - ListToolsRequest, - RequestHandlerExtra - >( - (): Promise => - Promise.resolve({ - tools: Object.entries(this._registeredTools) - .filter(([, tool]) => tool.enabled) - .map(([name, tool]): Tool => { - const toolDefinition: Tool = { - name, - title: tool.title, - description: tool.description, - inputSchema: (() => { - const obj = normalizeObjectSchema( - tool.inputSchema, - ); - return obj - ? (toJsonSchemaCompat(obj, { - strictUnions: true, - pipeStrategy: "input", - }) as Tool["inputSchema"]) - : EMPTY_OBJECT_JSON_SCHEMA; - })(), - annotations: tool.annotations, - execution: tool.execution, - _meta: tool._meta, - }; - - if (tool.outputSchema) { - const obj = normalizeObjectSchema( - tool.outputSchema, - ); - if (obj) { - toolDefinition.outputSchema = - toJsonSchemaCompat(obj, { + (request: ListToolsRequest, extra: RequestHandlerExtra) => + this._executeRequest>( + (): Promise => + Promise.resolve({ + tools: Object.entries(this._registeredTools) + .filter(([, tool]) => tool.enabled) + .map(([name, tool]): Tool => { + const toolDefinition: Tool = { + name, + title: tool.title, + description: tool.description, + inputSchema: (() => { + const obj = normalizeObjectSchema(tool.inputSchema); + return obj + ? (toJsonSchemaCompat(obj, { + strictUnions: true, + pipeStrategy: 'input' + }) as Tool['inputSchema']) + : EMPTY_OBJECT_JSON_SCHEMA; + })(), + annotations: tool.annotations, + execution: tool.execution, + _meta: tool._meta + }; + + if (tool.outputSchema) { + const obj = normalizeObjectSchema(tool.outputSchema); + if (obj) { + toolDefinition.outputSchema = toJsonSchemaCompat(obj, { strictUnions: true, - pipeStrategy: "output", - }) as Tool["outputSchema"]; + pipeStrategy: 'output' + }) as Tool['outputSchema']; + } } - } - return toolDefinition; - }), - }), - request, - extra, - ), + return toolDefinition; + }) + }), + request, + extra + ) ); this.server.setRequestHandler( CallToolRequestSchema, - ( - request: CallToolRequest, - extra: RequestHandlerExtra< + (request: CallToolRequest, extra: RequestHandlerExtra) => + this._executeRequest< + CallToolResult | CreateTaskResult, CallToolRequest, - ServerNotification - >, - ) => this._executeRequest< - CallToolResult | CreateTaskResult, - CallToolRequest, - RequestHandlerExtra - >( - async ( - request: CallToolRequest, - extra: RequestHandlerExtra< - CallToolRequest, - ServerNotification - >, - ): Promise => { - try { - const tool = this._registeredTools[request.params.name]; - if (!tool) { - throw new McpError( - ErrorCode.InvalidParams, - `Tool ${request.params.name} not found`, - ); - } - if (!tool.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Tool ${request.params.name} disabled`, - ); - } + RequestHandlerExtra + >( + async ( + request: CallToolRequest, + extra: RequestHandlerExtra + ): Promise => { + try { + const tool = this._registeredTools[request.params.name]; + if (!tool) { + throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`); + } + if (!tool.enabled) { + throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`); + } - const isTaskRequest = !!request.params.task; - const taskSupport = tool.execution?.taskSupport; - const isTaskHandler = "createTask" in - (tool.handler as AnyToolHandler< - ZodRawShapeCompat - >); - - // Validate task hint configuration - if ( - (taskSupport === "required" || - taskSupport === "optional") && - !isTaskHandler - ) { - throw new McpError( - ErrorCode.InternalError, - `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`, - ); - } + const isTaskRequest = !!request.params.task; + const taskSupport = tool.execution?.taskSupport; + const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler); - // Handle taskSupport 'required' without task augmentation - if (taskSupport === "required" && !isTaskRequest) { - throw new McpError( - ErrorCode.MethodNotFound, - `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`, - ); - } + // Validate task hint configuration + if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) { + throw new McpError( + ErrorCode.InternalError, + `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask` + ); + } - // Handle taskSupport 'optional' without task augmentation - automatic polling - if ( - taskSupport === "optional" && !isTaskRequest && - isTaskHandler - ) { - return await this.handleAutomaticTaskPolling( - tool, - request, - extra, - ); - } + // Handle taskSupport 'required' without task augmentation + if (taskSupport === 'required' && !isTaskRequest) { + throw new McpError( + ErrorCode.MethodNotFound, + `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')` + ); + } - // Normal execution path - const args = await this.validateToolInput( - tool, - request.params.arguments, - request.params.name, - ); - const result = await this.executeToolHandler( - tool, - args, - extra, - ); - - // Return CreateTaskResult immediately for task requests - if (isTaskRequest) { - return result; - } + // Handle taskSupport 'optional' without task augmentation - automatic polling + if (taskSupport === 'optional' && !isTaskRequest && isTaskHandler) { + return await this.handleAutomaticTaskPolling(tool, request, extra); + } + + // Normal execution path + const args = await this.validateToolInput(tool, request.params.arguments, request.params.name); + const result = await this.executeToolHandler(tool, args, extra); + + // Return CreateTaskResult immediately for task requests + if (isTaskRequest) { + return result; + } - // Validate output schema for non-task requests - await this.validateToolOutput( - tool, - result, - request.params.name, - ); - return result; - } catch (error) { - if (error instanceof McpError) { - if ( - error.code === - ErrorCode.UrlElicitationRequired - ) { - throw error; + // Validate output schema for non-task requests + await this.validateToolOutput(tool, result, request.params.name); + return result; + } catch (error) { + if (error instanceof McpError) { + if (error.code === ErrorCode.UrlElicitationRequired) { + throw error; + } } + return this.createToolError(error instanceof Error ? error.message : String(error)); } - return this.createToolError( - error instanceof Error - ? error.message - : String(error), - ); - } - }, - request, - extra, - ), + }, + request, + extra + ) ); this._toolHandlersInitialized = true; @@ -387,11 +317,11 @@ export class McpServer { return { content: [ { - type: "text", - text: errorMessage, - }, + type: 'text', + text: errorMessage + } ], - isError: true, + isError: true }; } @@ -400,10 +330,11 @@ export class McpServer { */ private async validateToolInput< Tool extends RegisteredTool, - Args extends Tool["inputSchema"] extends infer InputSchema - ? InputSchema extends AnySchema ? SchemaOutput + Args extends Tool['inputSchema'] extends infer InputSchema + ? InputSchema extends AnySchema + ? SchemaOutput + : undefined : undefined - : undefined, >(tool: Tool, args: Args, toolName: string): Promise { if (!tool.inputSchema) { return undefined as Args; @@ -415,14 +346,9 @@ export class McpServer { const schemaToParse = inputObj ?? (tool.inputSchema as AnySchema); const parseResult = await safeParseAsync(schemaToParse, args); if (!parseResult.success) { - const error = "error" in parseResult - ? parseResult.error - : "Unknown error"; + const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; const errorMessage = getParseErrorMessage(error); - throw new McpError( - ErrorCode.InvalidParams, - `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`, - ); + throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`); } return parseResult.data as unknown as Args; @@ -431,17 +357,13 @@ export class McpServer { /** * Validates tool output against the tool's output schema. */ - private async validateToolOutput( - tool: RegisteredTool, - result: CallToolResult | CreateTaskResult, - toolName: string, - ): Promise { + private async validateToolOutput(tool: RegisteredTool, result: CallToolResult | CreateTaskResult, toolName: string): Promise { if (!tool.outputSchema) { return; } // Only validate CallToolResult, not CreateTaskResult - if (!("content" in result)) { + if (!('content' in result)) { return; } @@ -452,26 +374,19 @@ export class McpServer { if (!result.structuredContent) { throw new McpError( ErrorCode.InvalidParams, - `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`, + `Output validation error: Tool ${toolName} has an output schema but no structured content was provided` ); } // if the tool has an output schema, validate structured content - const outputObj = normalizeObjectSchema( - tool.outputSchema, - ) as AnyObjectSchema; - const parseResult = await safeParseAsync( - outputObj, - result.structuredContent, - ); + const outputObj = normalizeObjectSchema(tool.outputSchema) as AnyObjectSchema; + const parseResult = await safeParseAsync(outputObj, result.structuredContent); if (!parseResult.success) { - const error = "error" in parseResult - ? parseResult.error - : "Unknown error"; + const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; const errorMessage = getParseErrorMessage(error); throw new McpError( ErrorCode.InvalidParams, - `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`, + `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}` ); } } @@ -479,54 +394,46 @@ export class McpServer { /** * Executes a tool handler (either regular or task-based). */ - private async executeToolHandler< - ExtraT extends RequestHandlerExtra, - >( + private async executeToolHandler( tool: RegisteredTool, args: unknown, - extra: ExtraT, + extra: ExtraT ): Promise { - const handler = tool.handler as AnyToolHandler< - ZodRawShapeCompat | undefined - >; - const isTaskHandler = "createTask" in handler; + const handler = tool.handler as AnyToolHandler; + const isTaskHandler = 'createTask' in handler; if (isTaskHandler) { - if (!extra.taskStore) { - throw new Error("No task store provided."); + const hasTaskStore = 'taskStore' in (extra as object) && (extra as { taskStore?: unknown }).taskStore; + if (!hasTaskStore) { + throw new Error('No task store provided.'); } - const taskExtra = { ...extra, taskStore: extra.taskStore }; + const taskExtra = { + ...extra, + taskStore: (extra as { taskStore: unknown }).taskStore + }; if (tool.inputSchema) { - const typedHandler = handler as ToolTaskHandler< - ZodRawShapeCompat - >; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedHandler = handler as ToolTaskHandler; return await Promise.resolve( - typedHandler.createTask( - args as any, - taskExtra as unknown as CreateTaskRequestHandlerExtra, - ), + typedHandler.createTask(args as ShapeOutput, taskExtra as unknown as CreateTaskRequestHandlerExtra) ); } else { const typedHandler = handler as ToolTaskHandler; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve( - (typedHandler.createTask as any)( - taskExtra as unknown as CreateTaskRequestHandlerExtra, - ), - ); + return await Promise.resolve(typedHandler.createTask(taskExtra as unknown as CreateTaskRequestHandlerExtra)); } } if (tool.inputSchema) { const typedHandler = handler as ToolCallback; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve(typedHandler(args as any, extra)); + return await Promise.resolve( + typedHandler( + args as ShapeOutput, + extra as unknown as RequestHandlerExtra + ) + ); } else { const typedHandler = handler as ToolCallback; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return await Promise.resolve((typedHandler as any)(extra)); + return await Promise.resolve(typedHandler(extra as unknown as RequestHandlerExtra)); } } @@ -535,57 +442,38 @@ export class McpServer { */ private async handleAutomaticTaskPolling< RequestT extends CallToolRequest, - ExtraT extends RequestHandlerExtra, - >( - tool: RegisteredTool, - request: RequestT, - extra: ExtraT, - ): Promise { + ExtraT extends RequestHandlerExtra + >(tool: RegisteredTool, request: RequestT, extra: ExtraT): Promise { if (!extra.taskStore) { - throw new Error("No task store provided for task-capable tool."); + throw new Error('No task store provided for task-capable tool.'); } // Validate input and create task - const args = await this.validateToolInput( - tool, - request.params.arguments, - request.params.name, - ); - const handler = tool.handler as ToolTaskHandler< - ZodRawShapeCompat | undefined - >; + const args = await this.validateToolInput(tool, request.params.arguments, request.params.name); + const handler = tool.handler as ToolTaskHandler; const taskExtra = { ...extra, taskStore: extra.taskStore }; const createTaskResult: CreateTaskResult = args // undefined only if tool.inputSchema is undefined ? await Promise.resolve( - (handler as ToolTaskHandler).createTask( - args, - taskExtra as unknown as CreateTaskRequestHandlerExtra, - ), - ) - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (handler as ToolTaskHandler).createTask( + args as ShapeOutput, + taskExtra as unknown as CreateTaskRequestHandlerExtra + ) + ) : await Promise.resolve( - ((handler as ToolTaskHandler).createTask as any)( - taskExtra as unknown as CreateTaskRequestHandlerExtra, - ), - ); + (handler as ToolTaskHandler).createTask(taskExtra as unknown as CreateTaskRequestHandlerExtra) + ); // Poll until completion const taskId = createTaskResult.task.taskId; let task = createTaskResult.task; const pollInterval = task.pollInterval ?? 5000; - while ( - task.status !== "completed" && task.status !== "failed" && - task.status !== "cancelled" - ) { - await new Promise((resolve) => setTimeout(resolve, pollInterval)); + while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') { + await new Promise(resolve => setTimeout(resolve, pollInterval)); const updatedTask = await extra.taskStore.getTask(taskId); if (!updatedTask) { - throw new McpError( - ErrorCode.InternalError, - `Task ${taskId} not found during polling`, - ); + throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`); } task = updatedTask; } @@ -601,61 +489,38 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler( - getMethodValue(CompleteRequestSchema), - ); + this.server.assertCanSetRequestHandler(getMethodValue(CompleteRequestSchema)); this.server.registerCapabilities({ - completions: {}, + completions: {} }); - this.server.setRequestHandler( - CompleteRequestSchema, - async (request): Promise => { - switch (request.params.ref.type) { - case "ref/prompt": - assertCompleteRequestPrompt(request); - return this.handlePromptCompletion( - request, - request.params.ref, - ); - - case "ref/resource": - assertCompleteRequestResourceTemplate(request); - return this.handleResourceCompletion( - request, - request.params.ref, - ); - - default: - throw new McpError( - ErrorCode.InvalidParams, - `Invalid completion reference: ${request.params.ref}`, - ); - } - }, - ); + this.server.setRequestHandler(CompleteRequestSchema, async (request): Promise => { + switch (request.params.ref.type) { + case 'ref/prompt': + assertCompleteRequestPrompt(request); + return this.handlePromptCompletion(request, request.params.ref); + + case 'ref/resource': + assertCompleteRequestResourceTemplate(request); + return this.handleResourceCompletion(request, request.params.ref); + + default: + throw new McpError(ErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`); + } + }); this._completionHandlerInitialized = true; } - private async handlePromptCompletion( - request: CompleteRequestPrompt, - ref: PromptReference, - ): Promise { + private async handlePromptCompletion(request: CompleteRequestPrompt, ref: PromptReference): Promise { const prompt = this._registeredPrompts[ref.name]; if (!prompt) { - throw new McpError( - ErrorCode.InvalidParams, - `Prompt ${ref.name} not found`, - ); + throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} not found`); } if (!prompt.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Prompt ${ref.name} disabled`, - ); + throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} disabled`); } if (!prompt.argsSchema) { @@ -672,20 +537,15 @@ export class McpServer { if (!completer) { return EMPTY_COMPLETION_RESULT; } - const suggestions = await completer( - request.params.argument.value, - request.params.context, - ); + const suggestions = await completer(request.params.argument.value, request.params.context); return createCompletionResult(suggestions); } private async handleResourceCompletion( request: CompleteRequestResourceTemplate, - ref: ResourceTemplateReference, + ref: ResourceTemplateReference ): Promise { - const template = Object.values(this._registeredResourceTemplates).find( - (t) => t.resourceTemplate.uriTemplate.toString() === ref.uri, - ); + const template = Object.values(this._registeredResourceTemplates).find(t => t.resourceTemplate.uriTemplate.toString() === ref.uri); if (!template) { if (this._registeredResources[ref.uri]) { @@ -693,23 +553,15 @@ export class McpServer { return EMPTY_COMPLETION_RESULT; } - throw new McpError( - ErrorCode.InvalidParams, - `Resource template ${request.params.ref.uri} not found`, - ); + throw new McpError(ErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`); } - const completer = template.resourceTemplate.completeCallback( - request.params.argument.name, - ); + const completer = template.resourceTemplate.completeCallback(request.params.argument.name); if (!completer) { return EMPTY_COMPLETION_RESULT; } - const suggestions = await completer( - request.params.argument.value, - request.params.context, - ); + const suggestions = await completer(request.params.argument.value, request.params.context); return createCompletionResult(suggestions); } @@ -720,161 +572,103 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler( - getMethodValue(ListResourcesRequestSchema), - ); - this.server.assertCanSetRequestHandler( - getMethodValue(ListResourceTemplatesRequestSchema), - ); - this.server.assertCanSetRequestHandler( - getMethodValue(ReadResourceRequestSchema), - ); + this.server.assertCanSetRequestHandler(getMethodValue(ListResourcesRequestSchema)); + this.server.assertCanSetRequestHandler(getMethodValue(ListResourceTemplatesRequestSchema)); + this.server.assertCanSetRequestHandler(getMethodValue(ReadResourceRequestSchema)); this.server.registerCapabilities({ resources: { - listChanged: true, - }, + listChanged: true + } }); this.server.setRequestHandler( ListResourcesRequestSchema, - ( - request: ListResourcesRequest, - extra: RequestHandlerExtra< + (request: ListResourcesRequest, extra: RequestHandlerExtra) => + this._executeRequest< + ListResourcesResult, ListResourcesRequest, - ServerNotification - >, - ) => this._executeRequest< - ListResourcesResult, - ListResourcesRequest, - RequestHandlerExtra - >( - async ( - request: ListResourcesRequest, - extra: RequestHandlerExtra< - ListResourcesRequest, - ServerNotification - >, - ) => { - const resources = Object.entries( - this._registeredResources, - ) - .filter(([_, resource]) => resource.enabled) - .map(([uri, resource]) => ({ - uri, - name: resource.name, - ...resource.metadata, - })); - - const templateResources: Resource[] = []; - for ( - const template of Object.values( - this._registeredResourceTemplates, - ) - ) { - if (!template.resourceTemplate.listCallback) { - continue; - } + RequestHandlerExtra + >( + async (request: ListResourcesRequest, extra: RequestHandlerExtra) => { + const resources = Object.entries(this._registeredResources) + .filter(([_, resource]) => resource.enabled) + .map(([uri, resource]) => ({ + uri, + name: resource.name, + ...resource.metadata + })); + + const templateResources: Resource[] = []; + for (const template of Object.values(this._registeredResourceTemplates)) { + if (!template.resourceTemplate.listCallback) { + continue; + } - const result = await template.resourceTemplate - .listCallback( - extra as any, + const result = await template.resourceTemplate.listCallback( + extra as unknown as RequestHandlerExtra ); - for (const resource of result.resources) { - templateResources.push({ - ...template.metadata, - // the defined resource metadata should override the template metadata if present - ...resource, - }); + for (const resource of result.resources) { + templateResources.push({ + ...template.metadata, + // the defined resource metadata should override the template metadata if present + ...resource + }); + } } - } - return { - resources: [...resources, ...templateResources], - }; - }, - request, - extra, - ), + return { + resources: [...resources, ...templateResources] + }; + }, + request, + extra + ) ); - this.server.setRequestHandler( - ListResourceTemplatesRequestSchema, - async () => { - const resourceTemplates = Object.entries( - this._registeredResourceTemplates, - ).map(([name, template]) => ({ - name, - uriTemplate: template.resourceTemplate.uriTemplate - .toString(), - ...template.metadata, - })); - - return { resourceTemplates }; - }, - ); + this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { + const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({ + name, + uriTemplate: template.resourceTemplate.uriTemplate.toString(), + ...template.metadata + })); + + return { resourceTemplates }; + }); this.server.setRequestHandler( ReadResourceRequestSchema, - ( - request: ReadResourceRequest, - extra: RequestHandlerExtra< - ReadResourceRequest, - ServerNotification - >, - ) => this._executeRequest< - ReadResourceResult, - ReadResourceRequest, - RequestHandlerExtra - >( - async ( - request: ReadResourceRequest, - extra: RequestHandlerExtra< - ReadResourceRequest, - ServerNotification - >, - ) => { - const uri = new URL(request.params.uri); - - // First check for exact resource match - const resource = this._registeredResources[uri.toString()]; - if (resource) { - if (!resource.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Resource ${uri} disabled`, - ); + (request: ReadResourceRequest, extra: RequestHandlerExtra) => + this._executeRequest>( + async (request: ReadResourceRequest, extra: RequestHandlerExtra) => { + const uri = new URL(request.params.uri); + + // First check for exact resource match + const resource = this._registeredResources[uri.toString()]; + if (resource) { + if (!resource.enabled) { + throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} disabled`); + } + return resource.readCallback(uri, extra as unknown as RequestHandlerExtra); } - return resource.readCallback(uri, extra as any); - } - // Then check templates - for ( - const template of Object.values( - this._registeredResourceTemplates, - ) - ) { - const variables = template.resourceTemplate - .uriTemplate.match( - uri.toString(), - ); - if (variables) { - return template.readCallback( - uri, - variables, - extra as any, - ); + // Then check templates + for (const template of Object.values(this._registeredResourceTemplates)) { + const variables = template.resourceTemplate.uriTemplate.match(uri.toString()); + if (variables) { + return template.readCallback( + uri, + variables, + extra as unknown as RequestHandlerExtra + ); + } } - } - throw new McpError( - ErrorCode.InvalidParams, - `Resource ${uri} not found`, - ); - }, - request, - extra, - ), + throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); + }, + request, + extra + ) ); this._resourceHandlersInitialized = true; @@ -887,136 +681,79 @@ export class McpServer { return; } - this.server.assertCanSetRequestHandler( - getMethodValue(ListPromptsRequestSchema), - ); - this.server.assertCanSetRequestHandler( - getMethodValue(GetPromptRequestSchema), - ); + this.server.assertCanSetRequestHandler(getMethodValue(ListPromptsRequestSchema)); + this.server.assertCanSetRequestHandler(getMethodValue(GetPromptRequestSchema)); this.server.registerCapabilities({ prompts: { - listChanged: true, - }, + listChanged: true + } }); this.server.setRequestHandler( ListPromptsRequestSchema, - ( - request: ListPromptsRequest, - extra: RequestHandlerExtra< - ListPromptsRequest, - ServerNotification - >, - ) => this._executeRequest< - ListPromptsResult, - ListPromptsRequest, - RequestHandlerExtra - >( - (): Promise => - Promise.resolve({ - prompts: Object.entries(this._registeredPrompts) - .filter(([, prompt]) => prompt.enabled) - .map(([name, prompt]): Prompt => { - return { - name, - title: prompt.title, - description: prompt.description, - arguments: prompt.argsSchema - ? promptArgumentsFromSchema( - prompt.argsSchema, - ) - : undefined, - }; - }), - }), - request, - extra, - ), + (request: ListPromptsRequest, extra: RequestHandlerExtra) => + this._executeRequest>( + (): Promise => + Promise.resolve({ + prompts: Object.entries(this._registeredPrompts) + .filter(([, prompt]) => prompt.enabled) + .map(([name, prompt]): Prompt => { + return { + name, + title: prompt.title, + description: prompt.description, + arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : undefined + }; + }) + }), + request, + extra + ) ); this.server.setRequestHandler( GetPromptRequestSchema, - ( - request: GetPromptRequest, - extra: RequestHandlerExtra< - GetPromptRequest, - ServerNotification - >, - ) => this._executeRequest< - GetPromptResult, - GetPromptRequest, - RequestHandlerExtra - >( - async ( - request: GetPromptRequest, - extra: RequestHandlerExtra< - GetPromptRequest, - ServerNotification - >, - ): Promise => { - const prompt = this._registeredPrompts[request.params.name]; - if (!prompt) { - throw new McpError( - ErrorCode.InvalidParams, - `Prompt ${request.params.name} not found`, - ); - } + (request: GetPromptRequest, extra: RequestHandlerExtra) => + this._executeRequest>( + async ( + request: GetPromptRequest, + extra: RequestHandlerExtra + ): Promise => { + const prompt = this._registeredPrompts[request.params.name]; + if (!prompt) { + throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} not found`); + } - if (!prompt.enabled) { - throw new McpError( - ErrorCode.InvalidParams, - `Prompt ${request.params.name} disabled`, - ); - } + if (!prompt.enabled) { + throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`); + } - if (prompt.argsSchema) { - const argsObj = normalizeObjectSchema( - prompt.argsSchema, - ) as AnyObjectSchema; - const parseResult = await safeParseAsync( - argsObj, - request.params.arguments, - ); - if (!parseResult.success) { - const error = "error" in parseResult - ? parseResult.error - : "Unknown error"; - const errorMessage = getParseErrorMessage(error); - throw new McpError( - ErrorCode.InvalidParams, - `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`, + if (prompt.argsSchema) { + const argsObj = normalizeObjectSchema(prompt.argsSchema) as AnyObjectSchema; + const parseResult = await safeParseAsync(argsObj, request.params.arguments); + if (!parseResult.success) { + const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; + const errorMessage = getParseErrorMessage(error); + throw new McpError( + ErrorCode.InvalidParams, + `Invalid arguments for prompt ${request.params.name}: ${errorMessage}` + ); + } + + const args = parseResult.data; + const cb = prompt.callback as PromptCallback; + return await Promise.resolve( + cb(args, extra as unknown as RequestHandlerExtra) ); + } else { + const cb = prompt.callback as PromptCallback; + return await Promise.resolve(cb(extra as unknown as RequestHandlerExtra)); } - - const args = parseResult.data; - const cb = prompt.callback as PromptCallback< - PromptArgsRawShape - >; - return await Promise.resolve( - cb( - args, - extra as unknown as RequestHandlerExtra< - ServerRequest, - ServerNotification - >, - ), - ); - } else { - const cb = prompt.callback as PromptCallback; - return await Promise.resolve( - cb( - extra as unknown as RequestHandlerExtra< - ServerRequest, - ServerNotification - >, - ), - ); - } - }, - request, - extra, - ), + }, + request, + extra + ) ); this._promptHandlersInitialized = true; @@ -1026,32 +763,19 @@ export class McpServer { * Registers a resource `name` at a fixed URI, which will use the given callback to respond to read requests. * @deprecated Use `registerResource` instead. */ - resource( - name: string, - uri: string, - readCallback: ReadResourceCallback, - ): RegisteredResource; + resource(name: string, uri: string, readCallback: ReadResourceCallback): RegisteredResource; /** * Registers a resource `name` at a fixed URI with metadata, which will use the given callback to respond to read requests. * @deprecated Use `registerResource` instead. */ - resource( - name: string, - uri: string, - metadata: ResourceMetadata, - readCallback: ReadResourceCallback, - ): RegisteredResource; + resource(name: string, uri: string, metadata: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; /** * Registers a resource `name` with a template pattern, which will use the given callback to respond to read requests. * @deprecated Use `registerResource` instead. */ - resource( - name: string, - template: ResourceTemplate, - readCallback: ReadResourceTemplateCallback, - ): RegisteredResourceTemplate; + resource(name: string, template: ResourceTemplate, readCallback: ReadResourceTemplateCallback): RegisteredResourceTemplate; /** * Registers a resource `name` with a template pattern and metadata, which will use the given callback to respond to read requests. @@ -1061,28 +785,20 @@ export class McpServer { name: string, template: ResourceTemplate, metadata: ResourceMetadata, - readCallback: ReadResourceTemplateCallback, + readCallback: ReadResourceTemplateCallback ): RegisteredResourceTemplate; - resource( - name: string, - uriOrTemplate: string | ResourceTemplate, - ...rest: unknown[] - ): RegisteredResource | RegisteredResourceTemplate { + resource(name: string, uriOrTemplate: string | ResourceTemplate, ...rest: unknown[]): RegisteredResource | RegisteredResourceTemplate { let metadata: ResourceMetadata | undefined; - if (typeof rest[0] === "object") { + if (typeof rest[0] === 'object') { metadata = rest.shift() as ResourceMetadata; } - const readCallback = rest[0] as - | ReadResourceCallback - | ReadResourceTemplateCallback; + const readCallback = rest[0] as ReadResourceCallback | ReadResourceTemplateCallback; - if (typeof uriOrTemplate === "string") { + if (typeof uriOrTemplate === 'string') { if (this._registeredResources[uriOrTemplate]) { - throw new Error( - `Resource ${uriOrTemplate} is already registered`, - ); + throw new Error(`Resource ${uriOrTemplate} is already registered`); } const registeredResource = this._createRegisteredResource( @@ -1090,7 +806,7 @@ export class McpServer { undefined, uriOrTemplate, metadata, - readCallback as ReadResourceCallback, + readCallback as ReadResourceCallback ); this.setResourceRequestHandlers(); @@ -1098,19 +814,16 @@ export class McpServer { return registeredResource; } else { if (this._registeredResourceTemplates[name]) { - throw new Error( - `Resource template ${name} is already registered`, - ); + throw new Error(`Resource template ${name} is already registered`); } - const registeredResourceTemplate = this - ._createRegisteredResourceTemplate( - name, - undefined, - uriOrTemplate, - metadata, - readCallback as ReadResourceTemplateCallback, - ); + const registeredResourceTemplate = this._createRegisteredResourceTemplate( + name, + undefined, + uriOrTemplate, + metadata, + readCallback as ReadResourceTemplateCallback + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -1122,29 +835,22 @@ export class McpServer { * Registers a resource with a config object and callback. * For static resources, use a URI string. For dynamic resources, use a ResourceTemplate. */ - registerResource( - name: string, - uriOrTemplate: string, - config: ResourceMetadata, - readCallback: ReadResourceCallback, - ): RegisteredResource; + registerResource(name: string, uriOrTemplate: string, config: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; registerResource( name: string, uriOrTemplate: ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceTemplateCallback, + readCallback: ReadResourceTemplateCallback ): RegisteredResourceTemplate; registerResource( name: string, uriOrTemplate: string | ResourceTemplate, config: ResourceMetadata, - readCallback: ReadResourceCallback | ReadResourceTemplateCallback, + readCallback: ReadResourceCallback | ReadResourceTemplateCallback ): RegisteredResource | RegisteredResourceTemplate { - if (typeof uriOrTemplate === "string") { + if (typeof uriOrTemplate === 'string') { if (this._registeredResources[uriOrTemplate]) { - throw new Error( - `Resource ${uriOrTemplate} is already registered`, - ); + throw new Error(`Resource ${uriOrTemplate} is already registered`); } const registeredResource = this._createRegisteredResource( @@ -1152,7 +858,7 @@ export class McpServer { (config as BaseMetadata).title, uriOrTemplate, config, - readCallback as ReadResourceCallback, + readCallback as ReadResourceCallback ); this.setResourceRequestHandlers(); @@ -1160,19 +866,16 @@ export class McpServer { return registeredResource; } else { if (this._registeredResourceTemplates[name]) { - throw new Error( - `Resource template ${name} is already registered`, - ); + throw new Error(`Resource template ${name} is already registered`); } - const registeredResourceTemplate = this - ._createRegisteredResourceTemplate( - name, - (config as BaseMetadata).title, - uriOrTemplate, - config, - readCallback as ReadResourceTemplateCallback, - ); + const registeredResourceTemplate = this._createRegisteredResourceTemplate( + name, + (config as BaseMetadata).title, + uriOrTemplate, + config, + readCallback as ReadResourceTemplateCallback + ); this.setResourceRequestHandlers(); this.sendResourceListChanged(); @@ -1185,7 +888,7 @@ export class McpServer { title: string | undefined, uri: string, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceCallback, + readCallback: ReadResourceCallback ): RegisteredResource { const registeredResource: RegisteredResource = { name, @@ -1196,31 +899,30 @@ export class McpServer { disable: () => registeredResource.update({ enabled: false }), enable: () => registeredResource.update({ enabled: true }), remove: () => registeredResource.update({ uri: null }), - update: (updates) => { - if (typeof updates.uri !== "undefined" && updates.uri !== uri) { + update: updates => { + if (typeof updates.uri !== 'undefined' && updates.uri !== uri) { delete this._registeredResources[uri]; if (updates.uri) { - this._registeredResources[updates.uri] = - registeredResource; + this._registeredResources[updates.uri] = registeredResource; } } - if (typeof updates.name !== "undefined") { + if (typeof updates.name !== 'undefined') { registeredResource.name = updates.name; } - if (typeof updates.title !== "undefined") { + if (typeof updates.title !== 'undefined') { registeredResource.title = updates.title; } - if (typeof updates.metadata !== "undefined") { + if (typeof updates.metadata !== 'undefined') { registeredResource.metadata = updates.metadata; } - if (typeof updates.callback !== "undefined") { + if (typeof updates.callback !== 'undefined') { registeredResource.readCallback = updates.callback; } - if (typeof updates.enabled !== "undefined") { + if (typeof updates.enabled !== 'undefined') { registeredResource.enabled = updates.enabled; } this.sendResourceListChanged(); - }, + } }; this._registeredResources[uri] = registeredResource; return registeredResource; @@ -1231,7 +933,7 @@ export class McpServer { title: string | undefined, template: ResourceTemplate, metadata: ResourceMetadata | undefined, - readCallback: ReadResourceTemplateCallback, + readCallback: ReadResourceTemplateCallback ): RegisteredResourceTemplate { const registeredResourceTemplate: RegisteredResourceTemplate = { resourceTemplate: template, @@ -1239,45 +941,39 @@ export class McpServer { metadata, readCallback, enabled: true, - disable: () => - registeredResourceTemplate.update({ enabled: false }), + disable: () => registeredResourceTemplate.update({ enabled: false }), enable: () => registeredResourceTemplate.update({ enabled: true }), remove: () => registeredResourceTemplate.update({ name: null }), - update: (updates) => { - if ( - typeof updates.name !== "undefined" && updates.name !== name - ) { + update: updates => { + if (typeof updates.name !== 'undefined' && updates.name !== name) { delete this._registeredResourceTemplates[name]; if (updates.name) { - this._registeredResourceTemplates[updates.name] = - registeredResourceTemplate; + this._registeredResourceTemplates[updates.name] = registeredResourceTemplate; } } - if (typeof updates.title !== "undefined") { + if (typeof updates.title !== 'undefined') { registeredResourceTemplate.title = updates.title; } - if (typeof updates.template !== "undefined") { - registeredResourceTemplate.resourceTemplate = - updates.template; + if (typeof updates.template !== 'undefined') { + registeredResourceTemplate.resourceTemplate = updates.template; } - if (typeof updates.metadata !== "undefined") { + if (typeof updates.metadata !== 'undefined') { registeredResourceTemplate.metadata = updates.metadata; } - if (typeof updates.callback !== "undefined") { + if (typeof updates.callback !== 'undefined') { registeredResourceTemplate.readCallback = updates.callback; } - if (typeof updates.enabled !== "undefined") { + if (typeof updates.enabled !== 'undefined') { registeredResourceTemplate.enabled = updates.enabled; } this.sendResourceListChanged(); - }, + } }; this._registeredResourceTemplates[name] = registeredResourceTemplate; // If the resource template has any completion callbacks, enable completions capability const variableNames = template.uriTemplate.variableNames; - const hasCompleter = Array.isArray(variableNames) && - variableNames.some((v) => !!template.completeCallback(v)); + const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v)); if (hasCompleter) { this.setCompletionRequestHandler(); } @@ -1290,57 +986,48 @@ export class McpServer { title: string | undefined, description: string | undefined, argsSchema: PromptArgsRawShape | undefined, - callback: PromptCallback, + callback: PromptCallback ): RegisteredPrompt { const registeredPrompt: RegisteredPrompt = { title, description, - argsSchema: argsSchema === undefined - ? undefined - : objectFromShape(argsSchema), + argsSchema: argsSchema === undefined ? undefined : objectFromShape(argsSchema), callback, enabled: true, disable: () => registeredPrompt.update({ enabled: false }), enable: () => registeredPrompt.update({ enabled: true }), remove: () => registeredPrompt.update({ name: null }), - update: (updates) => { - if ( - typeof updates.name !== "undefined" && updates.name !== name - ) { + update: updates => { + if (typeof updates.name !== 'undefined' && updates.name !== name) { delete this._registeredPrompts[name]; if (updates.name) { - this._registeredPrompts[updates.name] = - registeredPrompt; + this._registeredPrompts[updates.name] = registeredPrompt; } } - if (typeof updates.title !== "undefined") { + if (typeof updates.title !== 'undefined') { registeredPrompt.title = updates.title; } - if (typeof updates.description !== "undefined") { + if (typeof updates.description !== 'undefined') { registeredPrompt.description = updates.description; } - if (typeof updates.argsSchema !== "undefined") { - registeredPrompt.argsSchema = objectFromShape( - updates.argsSchema, - ); + if (typeof updates.argsSchema !== 'undefined') { + registeredPrompt.argsSchema = objectFromShape(updates.argsSchema); } - if (typeof updates.callback !== "undefined") { + if (typeof updates.callback !== 'undefined') { registeredPrompt.callback = updates.callback; } - if (typeof updates.enabled !== "undefined") { + if (typeof updates.enabled !== 'undefined') { registeredPrompt.enabled = updates.enabled; } this.sendPromptListChanged(); - }, + } }; this._registeredPrompts[name] = registeredPrompt; // If any argument uses a Completable schema, enable completions capability if (argsSchema) { - const hasCompletable = Object.values(argsSchema).some((field) => { - const inner: unknown = field instanceof ZodOptional - ? field._def?.innerType - : field; + const hasCompletable = Object.values(argsSchema).some(field => { + const inner: unknown = field instanceof ZodOptional ? field._def?.innerType : field; return isCompletable(inner); }); if (hasCompletable) { @@ -1360,7 +1047,7 @@ export class McpServer { annotations: ToolAnnotations | undefined, execution: ToolExecution | undefined, _meta: Record | undefined, - handler: AnyToolHandler, + handler: AnyToolHandler ): RegisteredTool { // Validate tool name according to SEP specification validateAndWarnToolName(name); @@ -1378,11 +1065,9 @@ export class McpServer { disable: () => registeredTool.update({ enabled: false }), enable: () => registeredTool.update({ enabled: true }), remove: () => registeredTool.update({ name: null }), - update: (updates) => { - if ( - typeof updates.name !== "undefined" && updates.name !== name - ) { - if (typeof updates.name === "string") { + update: updates => { + if (typeof updates.name !== 'undefined' && updates.name !== name) { + if (typeof updates.name === 'string') { validateAndWarnToolName(updates.name); } delete this._registeredTools[name]; @@ -1390,36 +1075,32 @@ export class McpServer { this._registeredTools[updates.name] = registeredTool; } } - if (typeof updates.title !== "undefined") { + if (typeof updates.title !== 'undefined') { registeredTool.title = updates.title; } - if (typeof updates.description !== "undefined") { + if (typeof updates.description !== 'undefined') { registeredTool.description = updates.description; } - if (typeof updates.paramsSchema !== "undefined") { - registeredTool.inputSchema = objectFromShape( - updates.paramsSchema, - ); + if (typeof updates.paramsSchema !== 'undefined') { + registeredTool.inputSchema = objectFromShape(updates.paramsSchema); } - if (typeof updates.outputSchema !== "undefined") { - registeredTool.outputSchema = objectFromShape( - updates.outputSchema, - ); + if (typeof updates.outputSchema !== 'undefined') { + registeredTool.outputSchema = objectFromShape(updates.outputSchema); } - if (typeof updates.callback !== "undefined") { + if (typeof updates.callback !== 'undefined') { registeredTool.handler = updates.callback; } - if (typeof updates.annotations !== "undefined") { + if (typeof updates.annotations !== 'undefined') { registeredTool.annotations = updates.annotations; } - if (typeof updates._meta !== "undefined") { + if (typeof updates._meta !== 'undefined') { registeredTool._meta = updates._meta; } - if (typeof updates.enabled !== "undefined") { + if (typeof updates.enabled !== 'undefined') { registeredTool.enabled = updates.enabled; } this.sendToolListChanged(); - }, + } }; this._registeredTools[name] = registeredTool; @@ -1452,7 +1133,7 @@ export class McpServer { tool( name: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** @@ -1468,7 +1149,7 @@ export class McpServer { name: string, description: string, paramsSchemaOrAnnotations: Args | ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** @@ -1479,7 +1160,7 @@ export class McpServer { name: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** @@ -1491,7 +1172,7 @@ export class McpServer { description: string, paramsSchema: Args, annotations: ToolAnnotations, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool; /** @@ -1511,7 +1192,7 @@ export class McpServer { // Support for this style is frozen as of protocol version 2025-03-26. Future additions // to tool definition should *NOT* be added. - if (typeof rest[0] === "string") { + if (typeof rest[0] === 'string') { description = rest.shift() as string; } @@ -1525,15 +1206,12 @@ export class McpServer { inputSchema = rest.shift() as ZodRawShapeCompat; // Check if the next arg is potentially annotations - if ( - rest.length > 1 && typeof rest[0] === "object" && - rest[0] !== null && !isZodRawShapeCompat(rest[0]) - ) { + if (rest.length > 1 && typeof rest[0] === 'object' && rest[0] !== null && !isZodRawShapeCompat(rest[0])) { // Case: tool(name, paramsSchema, annotations, cb) // Or: tool(name, description, paramsSchema, annotations, cb) annotations = rest.shift() as ToolAnnotations; } - } else if (typeof firstArg === "object" && firstArg !== null) { + } else if (typeof firstArg === 'object' && firstArg !== null) { // Not a ZodRawShapeCompat, so must be annotations in this position // Case: tool(name, annotations, cb) // Or: tool(name, description, annotations, cb) @@ -1549,19 +1227,16 @@ export class McpServer { inputSchema, outputSchema, annotations, - { taskSupport: "forbidden" }, + { taskSupport: 'forbidden' }, undefined, - callback, + callback ); } /** * Registers a tool with a config object and callback. */ - registerTool< - OutputArgs extends ZodRawShapeCompat | AnySchema, - InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, - >( + registerTool( name: string, config: { title?: string; @@ -1571,20 +1246,13 @@ export class McpServer { annotations?: ToolAnnotations; _meta?: Record; }, - cb: ToolCallback, + cb: ToolCallback ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } - const { - title, - description, - inputSchema, - outputSchema, - annotations, - _meta, - } = config; + const { title, description, inputSchema, outputSchema, annotations, _meta } = config; return this._createRegisteredTool( name, @@ -1593,9 +1261,9 @@ export class McpServer { inputSchema, outputSchema, annotations, - { taskSupport: "forbidden" }, + { taskSupport: 'forbidden' }, _meta, - cb as ToolCallback, + cb as ToolCallback ); } @@ -1609,21 +1277,13 @@ export class McpServer { * Registers a zero-argument prompt `name` (with a description) which will run the given function when the client calls it. * @deprecated Use `registerPrompt` instead. */ - prompt( - name: string, - description: string, - cb: PromptCallback, - ): RegisteredPrompt; + prompt(name: string, description: string, cb: PromptCallback): RegisteredPrompt; /** * Registers a prompt `name` accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. * @deprecated Use `registerPrompt` instead. */ - prompt( - name: string, - argsSchema: Args, - cb: PromptCallback, - ): RegisteredPrompt; + prompt(name: string, argsSchema: Args, cb: PromptCallback): RegisteredPrompt; /** * Registers a prompt `name` (with a description) accepting the given arguments, which must be an object containing named properties associated with Zod schemas. When the client calls it, the function will be run with the parsed and validated arguments. @@ -1633,7 +1293,7 @@ export class McpServer { name: string, description: string, argsSchema: Args, - cb: PromptCallback, + cb: PromptCallback ): RegisteredPrompt; prompt(name: string, ...rest: unknown[]): RegisteredPrompt { @@ -1642,7 +1302,7 @@ export class McpServer { } let description: string | undefined; - if (typeof rest[0] === "string") { + if (typeof rest[0] === 'string') { description = rest.shift() as string; } @@ -1652,13 +1312,7 @@ export class McpServer { } const cb = rest[0] as PromptCallback; - const registeredPrompt = this._createRegisteredPrompt( - name, - undefined, - description, - argsSchema, - cb, - ); + const registeredPrompt = this._createRegisteredPrompt(name, undefined, description, argsSchema, cb); this.setPromptRequestHandlers(); this.sendPromptListChanged(); @@ -1676,7 +1330,7 @@ export class McpServer { description?: string; argsSchema?: Args; }, - cb: PromptCallback, + cb: PromptCallback ): RegisteredPrompt { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); @@ -1689,7 +1343,7 @@ export class McpServer { title, description, argsSchema, - cb as PromptCallback, + cb as PromptCallback ); this.setPromptRequestHandlers(); @@ -1713,10 +1367,7 @@ export class McpServer { * @param params * @param sessionId optional for stateless and backward compatibility */ - async sendLoggingMessage( - params: LoggingMessageNotification["params"], - sessionId?: string, - ) { + async sendLoggingMessage(params: LoggingMessageNotification['params'], sessionId?: string) { return this.server.sendLoggingMessage(params, sessionId); } /** @@ -1746,17 +1397,10 @@ export class McpServer { } } - private async _executeRequest< - ResultT, - RequestT, - ExtraT extends RequestHandlerExtra, - >( - handler: ( - request: RequestT, - extra: ExtraT, - ) => Promise, + private async _executeRequest( + handler: (request: RequestT, extra: ExtraT) => Promise, request: RequestT, - extra: ExtraT, + extra: ExtraT ): Promise { this._middlewareFrozen = true; const middleware = this._middleware; @@ -1784,11 +1428,8 @@ export class McpServer { // Protect against creating a context with incorrect types by casting const context: McpMiddlewareContext = { request: request as unknown as ServerRequest, - extra: extra as unknown as RequestHandlerExtra< - ServerRequest, - ServerNotification - >, - state: {}, + extra: extra as unknown as RequestHandlerExtra, + state: {} }; const executeChain = async (i: number): Promise => { @@ -1800,9 +1441,7 @@ export class McpServer { let nextCalled = false; await fn(context, async () => { if (nextCalled) { - throw new Error( - "next() called multiple times in middleware", - ); + throw new Error('next() called multiple times in middleware'); } nextCalled = true; await executeChain(i + 1); @@ -1827,7 +1466,7 @@ export type CompleteResourceTemplateCallback = ( value: string, context?: { arguments?: Record; - }, + } ) => string[] | Promise; /** @@ -1851,11 +1490,9 @@ export class ResourceTemplate { complete?: { [variable: string]: CompleteResourceTemplateCallback; }; - }, + } ) { - this._uriTemplate = typeof uriTemplate === "string" - ? new UriTemplate(uriTemplate) - : uriTemplate; + this._uriTemplate = typeof uriTemplate === 'string' ? new UriTemplate(uriTemplate) : uriTemplate; } /** @@ -1875,9 +1512,7 @@ export class ResourceTemplate { /** * Gets the callback for completing a specific URI template variable, if one was provided. */ - completeCallback( - variable: string, - ): CompleteResourceTemplateCallback | undefined { + completeCallback(variable: string): CompleteResourceTemplateCallback | undefined { return this._callbacks.complete?.[variable]; } } @@ -1885,16 +1520,12 @@ export class ResourceTemplate { export type BaseToolCallback< SendResultT extends Result, Extra extends RequestHandlerExtra, - Args extends undefined | ZodRawShapeCompat | AnySchema, -> = Args extends ZodRawShapeCompat ? ( - args: ShapeOutput, - extra: Extra, - ) => SendResultT | Promise - : Args extends AnySchema ? ( - args: SchemaOutput, - extra: Extra, - ) => SendResultT | Promise - : (extra: Extra) => SendResultT | Promise; + Args extends undefined | ZodRawShapeCompat | AnySchema +> = Args extends ZodRawShapeCompat + ? (args: ShapeOutput, extra: Extra) => SendResultT | Promise + : Args extends AnySchema + ? (args: SchemaOutput, extra: Extra) => SendResultT | Promise + : (extra: Extra) => SendResultT | Promise; /** * Callback for a tool handler registered with Server.tool(). @@ -1906,9 +1537,7 @@ export type BaseToolCallback< * - `content` if the tool does not have an outputSchema * - Both fields are optional but typically one should be provided */ -export type ToolCallback< - Args extends undefined | ZodRawShapeCompat | AnySchema = undefined, -> = BaseToolCallback< +export type ToolCallback = BaseToolCallback< CallToolResult, RequestHandlerExtra, Args @@ -1917,9 +1546,7 @@ export type ToolCallback< /** * Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object). */ -export type AnyToolHandler< - Args extends undefined | ZodRawShapeCompat | AnySchema = undefined, -> = ToolCallback | ToolTaskHandler; +export type AnyToolHandler = ToolCallback | ToolTaskHandler; export type RegisteredTool = { title?: string; @@ -1933,10 +1560,7 @@ export type RegisteredTool = { enabled: boolean; enable(): void; disable(): void; - update< - InputArgs extends ZodRawShapeCompat, - OutputArgs extends ZodRawShapeCompat, - >(updates: { + update(updates: { name?: string | null; title?: string; description?: string; @@ -1951,8 +1575,8 @@ export type RegisteredTool = { }; const EMPTY_OBJECT_JSON_SCHEMA = { - type: "object" as const, - properties: {}, + type: 'object' as const, + properties: {} }; /** @@ -1961,11 +1585,11 @@ const EMPTY_OBJECT_JSON_SCHEMA = { function isZodTypeLike(value: unknown): value is AnySchema { return ( value !== null && - typeof value === "object" && - "parse" in value && - typeof value.parse === "function" && - "safeParse" in value && - typeof value.safeParse === "function" + typeof value === 'object' && + 'parse' in value && + typeof value.parse === 'function' && + 'safeParse' in value && + typeof value.safeParse === 'function' ); } @@ -1979,7 +1603,7 @@ function isZodTypeLike(value: unknown): value is AnySchema { * This includes transformed schemas like z.preprocess(), z.transform(), z.pipe(). */ function isZodSchemaInstance(obj: object): boolean { - return "_def" in obj || "_zod" in obj || isZodTypeLike(obj); + return '_def' in obj || '_zod' in obj || isZodTypeLike(obj); } /** @@ -1991,7 +1615,7 @@ function isZodSchemaInstance(obj: object): boolean { * which have internal properties that could be mistaken for schema values. */ function isZodRawShapeCompat(obj: unknown): obj is ZodRawShapeCompat { - if (typeof obj !== "object" || obj === null) { + if (typeof obj !== 'object' || obj === null) { return false; } @@ -2013,9 +1637,7 @@ function isZodRawShapeCompat(obj: unknown): obj is ZodRawShapeCompat { * Converts a provided Zod schema to a Zod object if it is a ZodRawShapeCompat, * otherwise returns the schema as is. */ -function getZodSchemaObject( - schema: ZodRawShapeCompat | AnySchema | undefined, -): AnySchema | undefined { +function getZodSchemaObject(schema: ZodRawShapeCompat | AnySchema | undefined): AnySchema | undefined { if (!schema) { return undefined; } @@ -2030,13 +1652,13 @@ function getZodSchemaObject( /** * Additional, optional information for annotating a resource. */ -export type ResourceMetadata = Omit; +export type ResourceMetadata = Omit; /** * Callback to list all resources matching a given template. */ export type ListResourcesCallback = ( - extra: RequestHandlerExtra, + extra: RequestHandlerExtra ) => ListResourcesResult | Promise; /** @@ -2044,7 +1666,7 @@ export type ListResourcesCallback = ( */ export type ReadResourceCallback = ( uri: URL, - extra: RequestHandlerExtra, + extra: RequestHandlerExtra ) => ReadResourceResult | Promise; export type RegisteredResource = { @@ -2072,7 +1694,7 @@ export type RegisteredResource = { export type ReadResourceTemplateCallback = ( uri: URL, variables: Variables, - extra: RequestHandlerExtra, + extra: RequestHandlerExtra ) => ReadResourceResult | Promise; export type RegisteredResourceTemplate = { @@ -2096,15 +1718,9 @@ export type RegisteredResourceTemplate = { type PromptArgsRawShape = ZodRawShapeCompat; -export type PromptCallback< - Args extends undefined | PromptArgsRawShape = undefined, -> = Args extends PromptArgsRawShape ? ( - args: ShapeOutput, - extra: RequestHandlerExtra, - ) => GetPromptResult | Promise - : ( - extra: RequestHandlerExtra, - ) => GetPromptResult | Promise; +export type PromptCallback = Args extends PromptArgsRawShape + ? (args: ShapeOutput, extra: RequestHandlerExtra) => GetPromptResult | Promise + : (extra: RequestHandlerExtra) => GetPromptResult | Promise; export type RegisteredPrompt = { title?: string; @@ -2136,7 +1752,7 @@ function promptArgumentsFromSchema(schema: AnyObjectSchema): PromptArgument[] { return { name, description, - required: !isOptional, + required: !isOptional }; }); } @@ -2145,16 +1761,16 @@ 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"); + 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") { + if (typeof value === 'string') { return value; } - throw new Error("Schema method literal must be a string"); + throw new Error('Schema method literal must be a string'); } function createCompletionResult(suggestions: string[]): CompleteResult { @@ -2162,14 +1778,14 @@ function createCompletionResult(suggestions: string[]): CompleteResult { completion: { values: suggestions.slice(0, 100), total: suggestions.length, - hasMore: suggestions.length > 100, - }, + hasMore: suggestions.length > 100 + } }; } const EMPTY_COMPLETION_RESULT: CompleteResult = { completion: { values: [], - hasMore: false, - }, + hasMore: false + } }; diff --git a/packages/server/test/server/mcpServer.test.ts b/packages/server/test/server/mcpServer.test.ts index 36ed7f141..b4649a29b 100644 --- a/packages/server/test/server/mcpServer.test.ts +++ b/packages/server/test/server/mcpServer.test.ts @@ -1,14 +1,14 @@ -import { McpServer } from "../../src/server/mcp.js"; -import { JSONRPCMessage } from "@modelcontextprotocol/core"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { McpServer } from '../../src/server/mcp.js'; +import { JSONRPCMessage } from '@modelcontextprotocol/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -describe("McpServer Middleware", () => { +describe('McpServer Middleware', () => { let server: McpServer; beforeEach(() => { server = new McpServer({ - name: "test-server", - version: "1.0.0", + name: 'test-server', + version: '1.0.0' }); }); @@ -17,36 +17,36 @@ describe("McpServer Middleware", () => { let serverOnMessage: (message: any) => Promise; let capturedResponse: JSONRPCMessage | undefined; let resolveSend: () => void; - const sendPromise = new Promise((resolve) => { + const sendPromise = new Promise(resolve => { resolveSend = resolve; }); const transport = { start: vi.fn(), - send: vi.fn().mockImplementation(async (msg) => { + send: vi.fn().mockImplementation(async msg => { capturedResponse = msg as JSONRPCMessage; resolveSend(); }), close: vi.fn(), set onmessage(handler: any) { serverOnMessage = handler; - }, + } }; await server.connect(transport); const request = { - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - method: "tools/call", + method: 'tools/call', params: { name: toolName, - arguments: {}, - }, + arguments: {} + } }; if (!serverOnMessage!) { - throw new Error("Server did not attach onMessage listener"); + throw new Error('Server did not attach onMessage listener'); } // Trigger request @@ -58,64 +58,58 @@ describe("McpServer Middleware", () => { return capturedResponse!; } - it("should execute middleware in registration order (Onion model)", async () => { + it('should execute middleware in registration order (Onion model)', async () => { const sequence: string[] = []; server.use(async (context, next) => { - sequence.push("mw1 start"); + sequence.push('mw1 start'); await next(); - sequence.push("mw1 end"); + sequence.push('mw1 end'); }); server.use(async (context, next) => { - sequence.push("mw2 start"); + sequence.push('mw2 start'); await next(); - sequence.push("mw2 end"); + sequence.push('mw2 end'); }); - server.tool("test-tool", {}, async () => { - sequence.push("handler"); - return { content: [{ type: "text", text: "result" }] }; + server.tool('test-tool', {}, async () => { + sequence.push('handler'); + return { content: [{ type: 'text', text: 'result' }] }; }); - await simulateCallTool("test-tool"); + await simulateCallTool('test-tool'); - expect(sequence).toEqual([ - "mw1 start", - "mw2 start", - "handler", - "mw2 end", - "mw1 end", - ]); + expect(sequence).toEqual(['mw1 start', 'mw2 start', 'handler', 'mw2 end', 'mw1 end']); }); - it("should short-circuit if next() is not called", async () => { + it('should short-circuit if next() is not called', async () => { const sequence: string[] = []; server.use(async (context, next) => { - sequence.push("mw1 start"); + sequence.push('mw1 start'); // next() NOT called - sequence.push("mw1 end"); + sequence.push('mw1 end'); }); server.use(async (context, next) => { - sequence.push("mw2 start"); + sequence.push('mw2 start'); await next(); }); - server.tool("test-tool", {}, async () => { - sequence.push("handler"); - return { content: [{ type: "text", text: "result" }] }; + server.tool('test-tool', {}, async () => { + sequence.push('handler'); + return { content: [{ type: 'text', text: 'result' }] }; }); - await simulateCallTool("test-tool"); + await simulateCallTool('test-tool'); // mw2 and handler should NOT run - expect(sequence).toEqual(["mw1 start", "mw1 end"]); + expect(sequence).toEqual(['mw1 start', 'mw1 end']); }); - it("should allow middleware to communicate via ctx.state", async () => { - const server = new McpServer({ name: "test", version: "1.0" }); + it('should allow middleware to communicate via ctx.state', async () => { + const server = new McpServer({ name: 'test', version: '1.0' }); server.use(async (ctx, next) => { ctx.state.value = 1; await next(); @@ -126,11 +120,7 @@ describe("McpServer Middleware", () => { }); // Use a tool list request to trigger the chain - server.tool( - "test-tool", - {}, - async () => ({ content: [{ type: "text", text: "ok" }] }), - ); + server.tool('test-tool', {}, async () => ({ content: [{ type: 'text', text: 'ok' }] })); let capturedState: any; server.use(async (ctx, next) => { @@ -139,7 +129,7 @@ describe("McpServer Middleware", () => { }); let resolveSend: () => void; - const sendPromise = new Promise((resolve) => { + const sendPromise = new Promise(resolve => { resolveSend = resolve; }); @@ -148,16 +138,16 @@ describe("McpServer Middleware", () => { send: vi.fn().mockImplementation(async () => { resolveSend(); }), - close: vi.fn(), + close: vi.fn() }; await server.connect(transport as any); // @ts-ignore const onMsg = (server.server.transport as any).onmessage; onMsg({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - method: "tools/call", - params: { name: "test-tool", arguments: {} }, + method: 'tools/call', + params: { name: 'test-tool', arguments: {} } }); await sendPromise; @@ -166,43 +156,43 @@ describe("McpServer Middleware", () => { expect(capturedState.value).toBe(2); }); - it("should execute middleware for other methods (e.g. tools/list)", async () => { + it('should execute middleware for other methods (e.g. tools/list)', async () => { // For this check, we need to simulate tools/list. // We can adapt our helper or just copy-paste a simplified version here for variety. const sequence: string[] = []; server.use(async (context, next) => { - sequence.push("mw"); + sequence.push('mw'); await next(); }); // Register a dummy tool to ensure tools/list handler is set up - server.tool("dummy", {}, async () => ({ content: [] })); + server.tool('dummy', {}, async () => ({ content: [] })); let serverOnMessage: any; let resolveSend: any; - const p = new Promise((r) => resolveSend = r); + const p = new Promise(r => (resolveSend = r)); const transport = { start: vi.fn(), send: vi.fn().mockImplementation(() => resolveSend()), close: vi.fn(), set onmessage(h: any) { serverOnMessage = h; - }, + } }; await server.connect(transport); serverOnMessage({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - method: "tools/list", - params: {}, + method: 'tools/list', + params: {} }); await p; - expect(sequence).toEqual(["mw"]); + expect(sequence).toEqual(['mw']); }); - it("should allow middleware to catch errors from downstream", async () => { + it('should allow middleware to catch errors from downstream', async () => { server.use(async (context, next) => { try { await next(); @@ -211,11 +201,11 @@ describe("McpServer Middleware", () => { } }); - server.tool("error-tool", {}, async () => { - throw new Error("Boom"); + server.tool('error-tool', {}, async () => { + throw new Error('Boom'); }); - const response = await simulateCallTool("error-tool"); + const response = await simulateCallTool('error-tool'); // Since middleware swallowed the error, the handler returns undefined (or whatever executed). // Actually, if handler throws and middleware catches, `result` in `_executeRequest` will be undefined. @@ -227,77 +217,68 @@ describe("McpServer Middleware", () => { expect((response as any).error).toBeUndefined(); }); - it("should propagate errors if middleware throws", async () => { + it('should propagate errors if middleware throws', async () => { server.use(async (context, next) => { - throw new Error("Middleware Error"); + throw new Error('Middleware Error'); }); - server.tool("test-tool", {}, async () => ({ content: [] })); + server.tool('test-tool', {}, async () => ({ content: [] })); - const response = await simulateCallTool("test-tool"); + const response = await simulateCallTool('test-tool'); // Standard JSON-RPC error response expect((response as any).error).toBeDefined(); - expect((response as any).error.message).toContain("Middleware Error"); + expect((response as any).error.message).toContain('Middleware Error'); }); - it("should throw an error if next() is called multiple times", async () => { + it('should throw an error if next() is called multiple times', async () => { server.use(async (context, next) => { await next(); await next(); // Second call should throw }); - server.tool("test-tool", {}, async () => ({ content: [] })); + server.tool('test-tool', {}, async () => ({ content: [] })); - const response = await simulateCallTool("test-tool"); + const response = await simulateCallTool('test-tool'); // Expect an error response due to double-call expect((response as any).error).toBeDefined(); - expect((response as any).error.message).toContain( - "next() called multiple times", - ); + expect((response as any).error.message).toContain('next() called multiple times'); }); - it("should respect async timing (middleware can await)", async () => { + it('should respect async timing (middleware can await)', async () => { const sequence: string[] = []; - const delay = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); server.use(async (context, next) => { - sequence.push("mw1 start"); + sequence.push('mw1 start'); await delay(10); // Wait 10ms - sequence.push("mw1 after delay"); + sequence.push('mw1 after delay'); await next(); - sequence.push("mw1 end"); + sequence.push('mw1 end'); }); server.use(async (context, next) => { - sequence.push("mw2 start"); + sequence.push('mw2 start'); await next(); }); - server.tool("test-tool", {}, async () => { - sequence.push("handler"); + server.tool('test-tool', {}, async () => { + sequence.push('handler'); return { content: [] }; }); - await simulateCallTool("test-tool"); + await simulateCallTool('test-tool'); - expect(sequence).toEqual([ - "mw1 start", - "mw1 after delay", - "mw2 start", - "handler", - "mw1 end", - ]); + expect(sequence).toEqual(['mw1 start', 'mw1 after delay', 'mw2 start', 'handler', 'mw1 end']); }); - it("should throw an error if use() is called after connect()", async () => { + it('should throw an error if use() is called after connect()', async () => { const transport = { start: vi.fn(), send: vi.fn(), close: vi.fn(), - set onmessage(_handler: any) {}, + set onmessage(_handler: any) {} }; await server.connect(transport); @@ -307,20 +288,20 @@ describe("McpServer Middleware", () => { server.use(async (context, next) => { await next(); }); - }).toThrow("Cannot register middleware after the server has started"); + }).toThrow('Cannot register middleware after the server has started'); }); // ============================================================ // Real World Use Case Integration Tests // ============================================================ - describe("Real World Use Cases", () => { - it("Logging: should observe request method and capture response timing", async () => { + describe('Real World Use Cases', () => { + it('Logging: should observe request method and capture response timing', async () => { const logs: { method: string; durationMs: number }[] = []; server.use(async (context, next) => { const start = Date.now(); - const method = (context.request as any).method || "unknown"; + const method = (context.request as any).method || 'unknown'; await next(); @@ -328,19 +309,19 @@ describe("McpServer Middleware", () => { logs.push({ method, durationMs }); }); - server.tool("fast-tool", {}, async () => { - return { content: [{ type: "text", text: "done" }] }; + server.tool('fast-tool', {}, async () => { + return { content: [{ type: 'text', text: 'done' }] }; }); - await simulateCallTool("fast-tool"); + await simulateCallTool('fast-tool'); expect(logs).toHaveLength(1); - expect(logs[0]!.method).toBe("tools/call"); + expect(logs[0]!.method).toBe('tools/call'); expect(logs[0]!.durationMs).toBeGreaterThanOrEqual(0); }); - it("Auth: should short-circuit unauthorized requests", async () => { - const VALID_TOKEN = "secret-token"; + it('Auth: should short-circuit unauthorized requests', async () => { + const VALID_TOKEN = 'secret-token'; server.use(async (context, next) => { // Simulate checking for an auth token in extra/authInfo @@ -354,74 +335,74 @@ describe("McpServer Middleware", () => { if (token !== VALID_TOKEN) { // Short-circuit: don't call next(), effectively blocking the request // In a real scenario, you might throw an error or set a response - throw new Error("Unauthorized"); + throw new Error('Unauthorized'); } await next(); }); - server.tool("protected-tool", {}, async () => { - return { content: [{ type: "text", text: "secret data" }] }; + server.tool('protected-tool', {}, async () => { + return { content: [{ type: 'text', text: 'secret data' }] }; }); // Simulate unauthorized request (no token) - const response = await simulateCallTool("protected-tool"); + const response = await simulateCallTool('protected-tool'); expect((response as any).error).toBeDefined(); - expect((response as any).error.message).toContain("Unauthorized"); + expect((response as any).error.message).toContain('Unauthorized'); }); - it("Activity Aggregation: should intercept tools/list and count discoveries", async () => { + it('Activity Aggregation: should intercept tools/list and count discoveries', async () => { let toolListCount = 0; let toolCallCount = 0; server.use(async (context, next) => { const method = (context.request as any).method; - if (method === "tools/list") { + if (method === 'tools/list') { toolListCount++; - } else if (method === "tools/call") { + } else if (method === 'tools/call') { toolCallCount++; } await next(); }); - server.tool("my-tool", {}, async () => ({ content: [] })); + server.tool('my-tool', {}, async () => ({ content: [] })); // Simulate tools/list let serverOnMessage: any; let resolveSend: any; - const p = new Promise((r) => (resolveSend = r)); + const p = new Promise(r => (resolveSend = r)); const transport = { start: vi.fn(), send: vi.fn().mockImplementation(() => resolveSend()), close: vi.fn(), set onmessage(h: any) { serverOnMessage = h; - }, + } }; await server.connect(transport); // First: tools/list serverOnMessage({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 1, - method: "tools/list", - params: {}, + method: 'tools/list', + params: {} }); await p; // Second: tools/call (need new promise) let resolveSend2: any; - const p2 = new Promise((r) => (resolveSend2 = r)); + const p2 = new Promise(r => (resolveSend2 = r)); transport.send.mockImplementation(() => resolveSend2()); serverOnMessage({ - jsonrpc: "2.0", + jsonrpc: '2.0', id: 2, - method: "tools/call", - params: { name: "my-tool", arguments: {} }, + method: 'tools/call', + params: { name: 'my-tool', arguments: {} } }); await p2; @@ -434,110 +415,104 @@ describe("McpServer Middleware", () => { // Failure Mode Verification Tests // ============================================================ - describe("Failure Mode Verification", () => { - it("Pre-next: error thrown before next() maps to JSON-RPC error", async () => { + describe('Failure Mode Verification', () => { + it('Pre-next: error thrown before next() maps to JSON-RPC error', async () => { server.use(async (context, next) => { // Error thrown BEFORE calling next() - throw new Error("Pre-next failure"); + throw new Error('Pre-next failure'); }); - server.tool("test-tool", {}, async () => ({ content: [] })); + server.tool('test-tool', {}, async () => ({ content: [] })); - const response = await simulateCallTool("test-tool"); + const response = await simulateCallTool('test-tool'); // Should be a proper JSON-RPC error response - expect((response as any).jsonrpc).toBe("2.0"); + expect((response as any).jsonrpc).toBe('2.0'); expect((response as any).id).toBe(1); expect((response as any).error).toBeDefined(); - expect((response as any).error.message).toContain( - "Pre-next failure", - ); + expect((response as any).error.message).toContain('Pre-next failure'); // Server should not crash - we got a response }); - it("Post-next: error thrown after next() maps to JSON-RPC error", async () => { + it('Post-next: error thrown after next() maps to JSON-RPC error', async () => { server.use(async (context, next) => { await next(); // Error thrown AFTER calling next() - throw new Error("Post-next failure"); + throw new Error('Post-next failure'); }); - server.tool("test-tool", {}, async () => ({ content: [] })); + server.tool('test-tool', {}, async () => ({ content: [] })); - const response = await simulateCallTool("test-tool"); + const response = await simulateCallTool('test-tool'); // Should be a proper JSON-RPC error response - expect((response as any).jsonrpc).toBe("2.0"); + expect((response as any).jsonrpc).toBe('2.0'); expect((response as any).id).toBe(1); expect((response as any).error).toBeDefined(); - expect((response as any).error.message).toContain( - "Post-next failure", - ); + expect((response as any).error.message).toContain('Post-next failure'); }); - it("Handler: error thrown in tool handler returns error result (SDK behavior)", async () => { + it('Handler: error thrown in tool handler returns error result (SDK behavior)', async () => { // No middleware - test pure handler error - server.tool("failing-tool", {}, async () => { - throw new Error("Handler failure"); + server.tool('failing-tool', {}, async () => { + throw new Error('Handler failure'); }); - const response = await simulateCallTool("failing-tool"); + const response = await simulateCallTool('failing-tool'); // MCP SDK converts handler errors to result with isError: true // (not JSON-RPC error - this is intentional SDK behavior) - expect((response as any).jsonrpc).toBe("2.0"); + expect((response as any).jsonrpc).toBe('2.0'); expect((response as any).id).toBe(1); expect((response as any).result).toBeDefined(); expect((response as any).result.isError).toBe(true); - expect((response as any).result.content[0]!.text).toContain( - "Handler failure", - ); + expect((response as any).result.content[0]!.text).toContain('Handler failure'); }); - it("Multiple middleware: error in second middleware propagates correctly", async () => { + it('Multiple middleware: error in second middleware propagates correctly', async () => { const sequence: string[] = []; server.use(async (context, next) => { - sequence.push("mw1 start"); + sequence.push('mw1 start'); try { await next(); } catch (e) { - sequence.push("mw1 caught"); + sequence.push('mw1 caught'); throw e; // Re-throw to propagate } - sequence.push("mw1 end"); + sequence.push('mw1 end'); }); server.use(async (context, next) => { - sequence.push("mw2 start"); - throw new Error("mw2 failure"); + sequence.push('mw2 start'); + throw new Error('mw2 failure'); }); - server.tool("test-tool", {}, async () => ({ content: [] })); + server.tool('test-tool', {}, async () => ({ content: [] })); - const response = await simulateCallTool("test-tool"); + const response = await simulateCallTool('test-tool'); expect((response as any).error).toBeDefined(); - expect((response as any).error.message).toContain("mw2 failure"); + expect((response as any).error.message).toContain('mw2 failure'); // Verify mw1 caught the error - expect(sequence).toContain("mw1 caught"); + expect(sequence).toContain('mw1 caught'); // mw1 end should NOT be in sequence since error was re-thrown - expect(sequence).not.toContain("mw1 end"); + expect(sequence).not.toContain('mw1 end'); }); - it("Error contains proper JSON-RPC error code", async () => { + it('Error contains proper JSON-RPC error code', async () => { server.use(async (context, next) => { - throw new Error("Generic middleware error"); + throw new Error('Generic middleware error'); }); - server.tool("test-tool", {}, async () => ({ content: [] })); + server.tool('test-tool', {}, async () => ({ content: [] })); - const response = await simulateCallTool("test-tool"); + const response = await simulateCallTool('test-tool'); expect((response as any).error).toBeDefined(); // JSON-RPC internal error code is -32603 expect((response as any).error.code).toBeDefined(); - expect(typeof (response as any).error.code).toBe("number"); + expect(typeof (response as any).error.code).toBe('number'); }); }); }); From 38905168d44631b3c376ae61d54d3aef38df54b5 Mon Sep 17 00:00:00 2001 From: ahammednibras8 Date: Sun, 28 Dec 2025 20:13:04 +0530 Subject: [PATCH 22/22] docs: add middleware section to server documentation --- docs/server.md | 205 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 140 insertions(+), 65 deletions(-) diff --git a/docs/server.md b/docs/server.md index 4d5138e84..080b36d4e 100644 --- a/docs/server.md +++ b/docs/server.md @@ -1,6 +1,8 @@ ## Server overview -This SDK lets you build MCP servers in TypeScript and connect them to different transports. For most use cases you will use `McpServer` from `@modelcontextprotocol/server` and choose one of: +This SDK lets you build MCP servers in TypeScript and connect them to different +transports. For most use cases you will use `McpServer` from +`@modelcontextprotocol/server` and choose one of: - **Streamable HTTP** (recommended for remote servers) - **HTTP + SSE** (deprecated, for backwards compatibility only) @@ -8,11 +10,16 @@ This SDK lets you build MCP servers in TypeScript and connect them to different For a complete, runnable example server, see: -- [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) – feature‑rich Streamable HTTP server -- [`jsonResponseStreamableHttp.ts`](../examples/server/src/jsonResponseStreamableHttp.ts) – Streamable HTTP with JSON response mode -- [`simpleStatelessStreamableHttp.ts`](../examples/server/src/simpleStatelessStreamableHttp.ts) – stateless Streamable HTTP server -- [`simpleSseServer.ts`](../examples/server/src/simpleSseServer.ts) – deprecated HTTP+SSE transport -- [`sseAndStreamableHttpCompatibleServer.ts`](../examples/server/src/sseAndStreamableHttpCompatibleServer.ts) – backwards‑compatible server for old and new clients +- [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) – + feature‑rich Streamable HTTP server +- [`jsonResponseStreamableHttp.ts`](../examples/server/src/jsonResponseStreamableHttp.ts) + – Streamable HTTP with JSON response mode +- [`simpleStatelessStreamableHttp.ts`](../examples/server/src/simpleStatelessStreamableHttp.ts) + – stateless Streamable HTTP server +- [`simpleSseServer.ts`](../examples/server/src/simpleSseServer.ts) – deprecated + HTTP+SSE transport +- [`sseAndStreamableHttpCompatibleServer.ts`](../examples/server/src/sseAndStreamableHttpCompatibleServer.ts) + – backwards‑compatible server for old and new clients ## Transports @@ -27,69 +34,122 @@ Streamable HTTP is the modern, fully featured transport. It supports: Key examples: -- [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) – sessions, logging, tasks, elicitation, auth hooks -- [`jsonResponseStreamableHttp.ts`](../examples/server/src/jsonResponseStreamableHttp.ts) – `enableJsonResponse: true`, no SSE -- [`standaloneSseWithGetStreamableHttp.ts`](../examples/server/src/standaloneSseWithGetStreamableHttp.ts) – notifications with Streamable HTTP GET + SSE +- [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) – + sessions, logging, tasks, elicitation, auth hooks +- [`jsonResponseStreamableHttp.ts`](../examples/server/src/jsonResponseStreamableHttp.ts) + – `enableJsonResponse: true`, no SSE +- [`standaloneSseWithGetStreamableHttp.ts`](../examples/server/src/standaloneSseWithGetStreamableHttp.ts) + – notifications with Streamable HTTP GET + SSE -See the MCP spec for full transport details: `https://modelcontextprotocol.io/specification/2025-11-25/basic/transports` +See the MCP spec for full transport details: +`https://modelcontextprotocol.io/specification/2025-11-25/basic/transports` ### Stateless vs stateful sessions Streamable HTTP can run: - **Stateless** – no session tracking, ideal for simple API‑style servers. -- **Stateful** – sessions have IDs, and you can enable resumability and advanced features. +- **Stateful** – sessions have IDs, and you can enable resumability and advanced + features. Examples: -- Stateless Streamable HTTP: [`simpleStatelessStreamableHttp.ts`](../examples/server/src/simpleStatelessStreamableHttp.ts) -- Stateful with resumability: [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) +- Stateless Streamable HTTP: + [`simpleStatelessStreamableHttp.ts`](../examples/server/src/simpleStatelessStreamableHttp.ts) +- Stateful with resumability: + [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) ### Deprecated HTTP + SSE -The older HTTP+SSE transport (protocol version 2024‑11‑05) is supported only for backwards compatibility. New implementations should prefer Streamable HTTP. +The older HTTP+SSE transport (protocol version 2024‑11‑05) is supported only for +backwards compatibility. New implementations should prefer Streamable HTTP. Examples: -- Legacy SSE server: [`simpleSseServer.ts`](../examples/server/src/simpleSseServer.ts) -- Backwards‑compatible server (Streamable HTTP + SSE): +- Legacy SSE server: + [`simpleSseServer.ts`](../examples/server/src/simpleSseServer.ts) +- Backwards‑compatible server (Streamable HTTP + SSE):\ [`sseAndStreamableHttpCompatibleServer.ts`](../examples/server/src/sseAndStreamableHttpCompatibleServer.ts) ## Running your server For a minimal “getting started” experience: -1. Start from [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts). +1. Start from + [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts). 2. Remove features you do not need (tasks, advanced logging, OAuth, etc.). 3. Register your own tools, resources and prompts. -For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS rebind protection), see the examples above and the MCP spec sections on transports. +For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS +rebind protection), see the examples above and the MCP spec sections on +transports. + +## Middleware + +The `McpServer` supports a middleware system similar to Express or Koa, allowing +you to intercept and modify requests, log activity, or enforce authentication +across all tools, prompts, and resources. + +Register middleware using `server.use()`: + +```typescript +const server = new McpServer( + { name: "my-server", version: "1.0.0" }, + { capabilities: { logging: {} } }, +); + +// Logging middleware +server.use(async (context, next) => { + const start = Date.now(); + try { + await next(); + } finally { + const duration = Date.now() - start; + console.error(`[${context.request.method}] took ${duration}ms`); + } +}); + +// Authentication middleware example +server.use(async (context, next) => { + if (context.request.method === "tools/call") { + // Perform auth checks here + // throw new McpError(ErrorCode.InvalidRequest, "Unauthorized"); + } + await next(); +}); +``` + +Middleware executes in the order registered. Calling `next()` passes control to +the next middleware or the actual handler. Note that middleware must be +registered before connecting the server transport. ## DNS rebinding protection -MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default: +MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use +`createMcpExpressApp()` to create an Express app with DNS rebinding protection +enabled by default: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from "@modelcontextprotocol/server"; // Protection auto-enabled (default host is 127.0.0.1) const app = createMcpExpressApp(); // Protection auto-enabled for localhost -const app = createMcpExpressApp({ host: 'localhost' }); +const app = createMcpExpressApp({ host: "localhost" }); // No auto protection when binding to all interfaces, unless you provide allowedHosts -const app = createMcpExpressApp({ host: '0.0.0.0' }); +const app = createMcpExpressApp({ host: "0.0.0.0" }); ``` When binding to `0.0.0.0` / `::`, provide an allow-list of hosts: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/server'; +import { createMcpExpressApp } from "@modelcontextprotocol/server"; const app = createMcpExpressApp({ - host: '0.0.0.0', - allowedHosts: ['localhost', '127.0.0.1', 'myhost.local'] + host: "0.0.0.0", + allowedHosts: ["localhost", "127.0.0.1", "myhost.local"], }); ``` @@ -97,29 +157,30 @@ const app = createMcpExpressApp({ ### Tools -Tools let MCP clients ask your server to take actions. They are usually the main way that LLMs call into your application. +Tools let MCP clients ask your server to take actions. They are usually the main +way that LLMs call into your application. A typical registration with `registerTool` looks like this: ```typescript server.registerTool( - 'calculate-bmi', + "calculate-bmi", { - title: 'BMI Calculator', - description: 'Calculate Body Mass Index', + title: "BMI Calculator", + description: "Calculate Body Mass Index", inputSchema: { weightKg: z.number(), - heightM: z.number() + heightM: z.number(), }, - outputSchema: { bmi: z.number() } + outputSchema: { bmi: z.number() }, }, async ({ weightKg, heightM }) => { const output = { bmi: weightKg / (heightM * heightM) }; return { - content: [{ type: 'text', text: JSON.stringify(output) }], - structuredContent: output + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output, }; - } + }, ); ``` @@ -130,60 +191,66 @@ This snippet is illustrative only; for runnable servers that expose tools, see: #### ResourceLink outputs -Tools can return `resource_link` content items to reference large resources without embedding them directly, allowing clients to fetch only what they need. +Tools can return `resource_link` content items to reference large resources +without embedding them directly, allowing clients to fetch only what they need. -The README’s `list-files` example shows the pattern conceptually; for concrete usage, see the Streamable HTTP examples in `examples/server/src`. +The README’s `list-files` example shows the pattern conceptually; for concrete +usage, see the Streamable HTTP examples in `examples/server/src`. ### Resources -Resources expose data to clients, but should not perform heavy computation or side‑effects. They are ideal for configuration, documents, or other reference data. +Resources expose data to clients, but should not perform heavy computation or +side‑effects. They are ideal for configuration, documents, or other reference +data. Conceptually, you might register resources like: ```typescript server.registerResource( - 'config', - 'config://app', + "config", + "config://app", { - title: 'Application Config', - description: 'Application configuration data', - mimeType: 'text/plain' + title: "Application Config", + description: "Application configuration data", + mimeType: "text/plain", }, - async uri => ({ - contents: [{ uri: uri.href, text: 'App configuration here' }] - }) + async (uri) => ({ + contents: [{ uri: uri.href, text: "App configuration here" }], + }), ); ``` -Dynamic resources use `ResourceTemplate` and can support completions on path parameters. For full runnable examples of resources: +Dynamic resources use `ResourceTemplate` and can support completions on path +parameters. For full runnable examples of resources: - [`simpleStreamableHttp.ts`](../examples/server/src/simpleStreamableHttp.ts) ### Prompts -Prompts are reusable templates that help humans (or client UIs) talk to models in a consistent way. They are declared on the server and listed through MCP. +Prompts are reusable templates that help humans (or client UIs) talk to models +in a consistent way. They are declared on the server and listed through MCP. A minimal prompt: ```typescript server.registerPrompt( - 'review-code', + "review-code", { - title: 'Code Review', - description: 'Review code for best practices and potential issues', - argsSchema: { code: z.string() } + title: "Code Review", + description: "Review code for best practices and potential issues", + argsSchema: { code: z.string() }, }, ({ code }) => ({ messages: [ { - role: 'user', + role: "user", content: { - type: 'text', - text: `Please review this code:\n\n${code}` - } - } - ] - }) + type: "text", + text: `Please review this code:\n\n${code}`, + }, + }, + ], + }), ); ``` @@ -193,19 +260,26 @@ For prompts integrated into a full server, see: ### Completions -Both prompts and resources can support argument completions. On the client side, you use `client.complete()` with a reference to the prompt or resource and the partially‑typed argument. +Both prompts and resources can support argument completions. On the client side, +you use `client.complete()` with a reference to the prompt or resource and the +partially‑typed argument. -See the MCP spec sections on prompts and resources for complete details, and [`simpleStreamableHttp.ts`](../examples/client/src/simpleStreamableHttp.ts) for client‑side usage patterns. +See the MCP spec sections on prompts and resources for complete details, and +[`simpleStreamableHttp.ts`](../examples/client/src/simpleStreamableHttp.ts) for +client‑side usage patterns. ### Display names and metadata -Tools, resources and prompts support a `title` field for human‑readable names. Older APIs can also attach `annotations.title`. To compute the correct display name on the client, use: +Tools, resources and prompts support a `title` field for human‑readable names. +Older APIs can also attach `annotations.title`. To compute the correct display +name on the client, use: - `getDisplayName` from `@modelcontextprotocol/client` ## Multi‑node deployment patterns -The SDK supports multi‑node deployments using Streamable HTTP. The high‑level patterns and diagrams live with the runnable server examples: +The SDK supports multi‑node deployments using Streamable HTTP. The high‑level +patterns and diagrams live with the runnable server examples: - [`examples/server/README.md`](../examples/server/README.md#multi-node-deployment-patterns) @@ -214,8 +288,9 @@ The SDK supports multi‑node deployments using Streamable HTTP. The high‑leve To handle both modern and legacy clients: - Run a backwards‑compatible server: - - [`sseAndStreamableHttpCompatibleServer.ts`](../examples/server/src/sseAndStreamableHttpCompatibleServer.ts) + - [`sseAndStreamableHttpCompatibleServer.ts`](../examples/server/src/sseAndStreamableHttpCompatibleServer.ts) - Use a client that falls back from Streamable HTTP to SSE: - - [`streamableHttpWithSseFallbackClient.ts`](../examples/client/src/streamableHttpWithSseFallbackClient.ts) + - [`streamableHttpWithSseFallbackClient.ts`](../examples/client/src/streamableHttpWithSseFallbackClient.ts) -For the detailed protocol rules, see the “Backwards compatibility” section of the MCP spec. +For the detailed protocol rules, see the “Backwards compatibility” section of +the MCP spec.