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: diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 10e550df4..01e87c1ce 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4290,4 +4290,54 @@ describe("elicitInput()", () => { text: "No booking made. Original date not available." }]); }); + + /** + * 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 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", + }); + + // 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}` }], + }) + ); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + // 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 }, // Unknown parameters should cause error + }, + }, + CallToolResultSchema, + )).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 ); }