Skip to content
Open
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
50 changes: 50 additions & 0 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4383,4 +4383,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");
});
});
13 changes: 10 additions & 3 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,14 +774,19 @@ export class McpServer {
inputSchema: ZodRawShape | undefined,
outputSchema: ZodRawShape | undefined,
annotations: ToolAnnotations | undefined,
strict: boolean | undefined,
_meta: Record<string, unknown> | undefined,
callback: ToolCallback<ZodRawShape | undefined>
): 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,
Expand Down Expand Up @@ -919,7 +924,7 @@ export class McpServer {
}
const callback = rest[0] as ToolCallback<ZodRawShape | undefined>;

return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, undefined, callback)
return this._createRegisteredTool(name, undefined, description, inputSchema, outputSchema, annotations, false, undefined, callback)
}

/**
Expand All @@ -933,6 +938,7 @@ export class McpServer {
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
strict?: boolean;
_meta?: Record<string, unknown>;
},
cb: ToolCallback<InputArgs>
Expand All @@ -941,7 +947,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, strict, _meta } = config;

return this._createRegisteredTool(
name,
Expand All @@ -950,6 +956,7 @@ export class McpServer {
inputSchema,
outputSchema,
annotations,
strict,
_meta,
cb as ToolCallback<ZodRawShape | undefined>
);
Expand Down