From d5df2579be1ff0171423b6861ff8533ae450ec37 Mon Sep 17 00:00:00 2001 From: Adam Bloomston Date: Tue, 5 Aug 2025 11:00:58 -0600 Subject: [PATCH 1/3] test: add failing test showing unknown parameters are silently ignored This test demonstrates the current issue where tools accept unknown parameters (e.g. incorrect capitalization) without validation errors. The test uses 'username' and 'itemcount' instead of the expected 'userName' and 'itemCount' parameters, which should be rejected but currently aren't. This test is expected to fail until strict validation is implemented. --- src/server/mcp.test.ts | 54 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 10e550df4..73087b1b4 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4290,4 +4290,58 @@ describe("elicitInput()", () => { text: "No booking made. Original date not available." }]); }); + + /** + * Test: Tool parameter validation with incorrect capitalization + * This test demonstrates that tools currently silently ignore unknown parameters, + * including those with incorrect capitalization. This should fail to show the issue exists. + */ + test("should fail: tool with incorrect parameter capitalization is not rejected", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + + const client = new Client({ + name: "test client", + version: "1.0", + }); + + // Register a tool that expects optional 'userName' and 'itemCount' + mcpServer.registerTool( + "test-strict", + { + 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), + ]); + + // Call the tool with unknown parameters (incorrect capitalization) + // These should be rejected but currently aren't - they're silently ignored + const result = await client.request( + { + method: "tools/call", + params: { + name: "test-strict", + arguments: { username: "test", itemcount: 42 }, // Should be rejected as unknown parameters + }, + }, + CallToolResultSchema, + ); + + // This expectation should fail because the tool currently accepts the call + // and silently ignores unknown parameters, returning success instead of error + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("Invalid arguments"); + }); }); From 80166848bf1d0e4a63a7573fbfae11bcf7aeb9f8 Mon Sep 17 00:00:00 2001 From: Adam Bloomston Date: Tue, 5 Aug 2025 11:20:40 -0600 Subject: [PATCH 2/3] feat: add optional strict parameter to registerTool for Zod validation Add strict parameter to registerTool config that defaults to false for backward compatibility. When strict=true, applies .strict() to Zod schema creation for tool input validation to throw errors on unknown parameters instead of silently ignoring them. - Add strict?: boolean to registerTool config interface - Modify _createRegisteredTool to accept and use strict parameter - Apply z.object(inputSchema).strict() when strict=true - Update legacy tool() method to pass strict=false (backward compatibility) - Update test to verify strict validation rejects unknown parameters - All existing tests continue to pass (no breaking changes) This fixes the issue where parameter name typos are silently dropped, leading to confusing behavior where tools execute with missing data. --- src/server/mcp.test.ts | 26 +++++++++++--------------- src/server/mcp.ts | 13 ++++++++++--- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 73087b1b4..01e87c1ce 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4292,11 +4292,11 @@ describe("elicitInput()", () => { }); /** - * Test: Tool parameter validation with incorrect capitalization - * This test demonstrates that tools currently silently ignore unknown parameters, - * including those with incorrect capitalization. This should fail to show the issue exists. + * Test: Tool parameter validation with strict mode + * This test verifies that tools with strict: true reject unknown parameters, + * including those with incorrect capitalization. */ - test("should fail: tool with incorrect parameter capitalization is not rejected", async () => { + test("should reject unknown parameters when strict validation is enabled", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", @@ -4307,11 +4307,12 @@ describe("elicitInput()", () => { version: "1.0", }); - // Register a tool that expects optional 'userName' and 'itemCount' + // Register a tool with strict validation enabled mcpServer.registerTool( "test-strict", { inputSchema: { userName: z.string().optional(), itemCount: z.number().optional() }, + strict: true, }, async ({ userName, itemCount }) => ({ content: [{ type: "text", text: `${userName || 'none'}: ${itemCount || 0}` }], @@ -4326,22 +4327,17 @@ describe("elicitInput()", () => { mcpServer.server.connect(serverTransport), ]); - // Call the tool with unknown parameters (incorrect capitalization) - // These should be rejected but currently aren't - they're silently ignored - const result = await client.request( + // Call the tool with unknown parameters (incorrect capitalization) + // With strict: true, these should now be rejected + await expect(client.request( { method: "tools/call", params: { name: "test-strict", - arguments: { username: "test", itemcount: 42 }, // Should be rejected as unknown parameters + arguments: { username: "test", itemcount: 42 }, // Unknown parameters should cause error }, }, CallToolResultSchema, - ); - - // This expectation should fail because the tool currently accepts the call - // and silently ignores unknown parameters, returning success instead of error - expect(result.isError).toBe(true); - expect(result.content[0].text).toContain("Invalid arguments"); + )).rejects.toThrow("Invalid arguments"); }); }); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 791facef1..a54d72578 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -772,13 +772,18 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, + strict: boolean | undefined, callback: ToolCallback ): RegisteredTool { const registeredTool: RegisteredTool = { title, description, inputSchema: - inputSchema === undefined ? undefined : z.object(inputSchema), + inputSchema === undefined + ? undefined + : strict === true + ? z.object(inputSchema).strict() + : z.object(inputSchema), outputSchema: outputSchema === undefined ? undefined : z.object(outputSchema), annotations, @@ -914,7 +919,7 @@ export class McpServer { } const callback = rest[0] as ToolCallback; - return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, callback) + return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, false, callback) } /** @@ -928,6 +933,7 @@ export class McpServer { inputSchema?: InputArgs; outputSchema?: OutputArgs; annotations?: ToolAnnotations; + strict?: boolean; }, cb: ToolCallback ): RegisteredTool { @@ -935,7 +941,7 @@ export class McpServer { throw new Error(`Tool ${name} is already registered`); } - const { title, description, inputSchema, outputSchema, annotations } = config; + const { title, description, inputSchema, outputSchema, annotations, strict } = config; return this._createRegisteredTool( name, @@ -944,6 +950,7 @@ export class McpServer { inputSchema, outputSchema, annotations, + strict, cb as ToolCallback ); } From ef9937875b3feedcf1bad9fd2503ba203e24e006 Mon Sep 17 00:00:00 2001 From: Adam Bloomston Date: Tue, 5 Aug 2025 11:39:44 -0600 Subject: [PATCH 3/3] docs: add parameter validation documentation for strict mode Add Advanced Usage section explaining the strict parameter for registerTool: - Show examples of strict vs lenient validation - Explain when to use each mode (development vs production) - Document that strict parameter is only available in registerTool() - Note that legacy tool() method uses lenient validation for compatibility This helps developers understand when and how to use strict validation to catch parameter name typos and unexpected data. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index f1839845c..dd2a8451c 100644 --- a/README.md +++ b/README.md @@ -949,6 +949,31 @@ 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 for development - catches typos immediately +const devTool = server.registerTool("dev-tool", { + inputSchema: { userName: z.string(), itemCount: z.number() }, + strict: true // Reject { username: "test", itemcount: 42 } +}, handler); + +// Lenient validation for production - handles client variations gracefully +const prodTool = server.registerTool("prod-tool", { + inputSchema: { userName: z.string().optional(), itemCount: z.number().optional() }, + strict: false // Accept extra parameters (default behavior) +}, 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 `strict` 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: