Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,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:
Expand Down
80 changes: 80 additions & 0 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
14 changes: 11 additions & 3 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,13 +647,19 @@ export class McpServer {
inputSchema: ZodRawShape | undefined,
outputSchema: ZodRawShape | undefined,
annotations: ToolAnnotations | undefined,
strictInputSchemaValidation: 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:
inputSchema === undefined
? undefined
: strictInputSchemaValidation === true
? z.object(inputSchema).strict()
: z.object(inputSchema),
outputSchema: outputSchema === undefined ? undefined : z.object(outputSchema),
annotations,
_meta,
Expand Down Expand Up @@ -780,7 +786,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 @@ -794,6 +800,7 @@ export class McpServer {
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
strictInputSchemaValidation?: boolean;
_meta?: Record<string, unknown>;
},
cb: ToolCallback<InputArgs>
Expand All @@ -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,
Expand All @@ -811,6 +818,7 @@ export class McpServer {
inputSchema,
outputSchema,
annotations,
strictInputSchemaValidation,
_meta,
cb as ToolCallback<ZodRawShape | undefined>
);
Expand Down