diff --git a/README.md b/README.md index 92f56786f..93632fa91 100644 --- a/README.md +++ b/README.md @@ -1112,6 +1112,40 @@ server.registerTool("tool3", ...).disable(); // Only one 'notifications/tools/list_changed' is sent. ``` +### Parameter Validation and Error Handling + +Control how tools handle parameter validation errors and unexpected inputs: + +```typescript +// Strict validation - catches typos and unexpected parameters immediately +const devTool = server.registerTool( + 'dev-tool', + { + inputSchema: { userName: z.string(), itemCount: z.number() }, + strictInputSchemaValidation: true // Reject { username: "test", itemcount: 42 } + }, + handler +); + +// Lenient validation (default) - maintains backwards compatibility with existing clients +const lenientTool = server.registerTool( + 'lenient-tool', + { + inputSchema: { userName: z.string().optional(), itemCount: z.number().optional() }, + strictInputSchemaValidation: false // Accept extra parameters for backwards compatibility with clients that may send additional fields + }, + handler +); +``` + +**When to use strict validation:** + +- Development and testing: Catch parameter name typos early +- Production APIs: Ensure clients send only expected parameters +- Security-sensitive tools: Prevent injection of unexpected data + +**Note:** The `strictInputSchemaValidation` parameter is only available in `registerTool()`. The legacy `tool()` method uses lenient validation for backward compatibility. + ### Low-Level Server For more control, you can use the low-level Server class directly: diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index f3669fa64..21f54fdbf 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4161,4 +4161,91 @@ describe('elicitInput()', () => { } ]); }); + + test('should accept unknown parameters when strict validation is disabled (default)', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerTool( + 'test-lenient', + { + inputSchema: { userName: z.string().optional(), itemCount: z.number().optional() } + }, + async ({ userName, itemCount }) => ({ + content: [{ type: 'text', text: `${userName || 'none'}: ${itemCount || 0}` }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request( + { + method: 'tools/call', + params: { + name: 'test-lenient', + arguments: { username: 'test', itemcount: 42 } + } + }, + CallToolResultSchema + ); + + expect(result.isError).toBeUndefined(); + expect(result.content[0].text).toBe('none: 0'); + }); + + test('should reject unknown parameters when strict validation is enabled', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.registerTool( + 'test-strict', + { + inputSchema: { userName: z.string().optional(), itemCount: z.number().optional() }, + strictInputSchemaValidation: true + }, + async ({ userName, itemCount }) => ({ + content: [{ type: 'text', text: `${userName || 'none'}: ${itemCount || 0}` }] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request( + { + method: 'tools/call', + params: { + name: 'test-strict', + arguments: { username: 'test', itemcount: 42 } + } + }, + CallToolResultSchema + ); + + expect(result.isError).toBe(true); + expect(result.content).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + text: expect.stringContaining('Input validation error') + }) + ]) + ); + }); }); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index fb93bd326..fc1b1a644 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -649,16 +649,23 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, + strictInputSchemaValidation: boolean | undefined, _meta: Record | undefined, callback: ToolCallback ): RegisteredTool { const registeredTool: RegisteredTool = { title, description, - inputSchema: inputSchema === undefined ? undefined : z.object(inputSchema), + inputSchema: + inputSchema === undefined + ? undefined + : strictInputSchemaValidation === true + ? z.object(inputSchema).strict() + : z.object(inputSchema), outputSchema: outputSchema === undefined ? undefined : z.object(outputSchema), annotations, _meta, + strictInputSchemaValidation, callback, enabled: true, disable: () => registeredTool.update({ enabled: false }), @@ -782,7 +789,7 @@ export class McpServer { } const callback = rest[0] as ToolCallback; - return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, undefined, callback); + return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, false, undefined, callback); } /** @@ -796,6 +803,7 @@ export class McpServer { inputSchema?: InputArgs; outputSchema?: OutputArgs; annotations?: ToolAnnotations; + strictInputSchemaValidation?: boolean; _meta?: Record; }, cb: ToolCallback @@ -804,7 +812,7 @@ export class McpServer { throw new Error(`Tool ${name} is already registered`); } - const { title, description, inputSchema, outputSchema, annotations, _meta } = config; + const { title, description, inputSchema, outputSchema, annotations, strictInputSchemaValidation, _meta } = config; return this._createRegisteredTool( name, @@ -813,6 +821,7 @@ export class McpServer { inputSchema, outputSchema, annotations, + strictInputSchemaValidation, _meta, cb as ToolCallback ); @@ -1027,6 +1036,7 @@ export type RegisteredTool = { outputSchema?: AnyZodObject; annotations?: ToolAnnotations; _meta?: Record; + strictInputSchemaValidation?: boolean; callback: ToolCallback; enabled: boolean; enable(): void;