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 4bb42d7fc..f4aa6a94c 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4085,4 +4085,84 @@ 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)]); + + await expect( + client.request( + { + method: 'tools/call', + params: { + name: 'test-strict', + arguments: { username: 'test', itemcount: 42 } + } + }, + CallToolResultSchema + ) + ).rejects.toThrow('Invalid arguments'); + }); }); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index cef1722d6..77eb76131 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -647,13 +647,19 @@ 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, @@ -780,7 +786,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); } /** @@ -794,6 +800,7 @@ export class McpServer { inputSchema?: InputArgs; outputSchema?: OutputArgs; annotations?: ToolAnnotations; + strictInputSchemaValidation?: boolean; _meta?: Record; }, cb: ToolCallback @@ -802,7 +809,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, @@ -811,6 +818,7 @@ export class McpServer { inputSchema, outputSchema, annotations, + strictInputSchemaValidation, _meta, cb as ToolCallback );