Skip to content

Commit 7a1412c

Browse files
committed
Return protocol errors for invalid tool args
1 parent 86276ed commit 7a1412c

3 files changed

Lines changed: 49 additions & 91 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@modelcontextprotocol/server": patch
3+
---
4+
5+
Return protocol errors for invalid tool input arguments instead of wrapping them as tool execution errors.

packages/server/src/server/mcp.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,12 @@ export class McpServer {
208208
await this.validateToolOutput(tool, result, request.params.name);
209209
return result;
210210
} catch (error) {
211-
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) {
212-
throw error; // Return the error to the caller without wrapping in CallToolResult
211+
if (
212+
error instanceof ProtocolError &&
213+
(error.code === ProtocolErrorCode.UrlElicitationRequired ||
214+
(error.code === ProtocolErrorCode.InvalidParams && error.message.startsWith('Input validation error:')))
215+
) {
216+
throw error; // Return protocol errors to the caller without wrapping in CallToolResult
213217
}
214218
return this.createToolError(error instanceof Error ? error.message : String(error));
215219
}

test/integration/test/standardSchema.test.ts

Lines changed: 38 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { Client } from '@modelcontextprotocol/client';
77
import type { TextContent } from '@modelcontextprotocol/core';
8-
import { InMemoryTransport } from '@modelcontextprotocol/core';
8+
import { InMemoryTransport, ProtocolErrorCode } from '@modelcontextprotocol/core';
99
import { completable, fromJsonSchema as serverFromJsonSchema, McpServer } from '@modelcontextprotocol/server';
1010
import { toStandardJsonSchema } from '@valibot/to-json-schema';
1111
import { type } from 'arktype';
@@ -33,6 +33,18 @@ describe('Standard Schema Support', () => {
3333
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
3434
}
3535

36+
async function expectToolInputValidationError(params: { name: string; arguments: Record<string, unknown> }) {
37+
try {
38+
await client.request({ method: 'tools/call', params });
39+
} catch (error) {
40+
expect(error).toMatchObject({ code: ProtocolErrorCode.InvalidParams });
41+
const message = error instanceof Error ? error.message : String(error);
42+
expect(message).toContain('Input validation error');
43+
return message;
44+
}
45+
throw new Error('Expected tool input validation to reject with InvalidParams');
46+
}
47+
3648
describe('ArkType schemas', () => {
3749
describe('tool registration', () => {
3850
test('should register tool with ArkType input schema', async () => {
@@ -130,14 +142,7 @@ describe('Standard Schema Support', () => {
130142

131143
await connectClientAndServer();
132144

133-
const result = await client.request({
134-
method: 'tools/call',
135-
params: { name: 'double', arguments: { value: 'not a number' } }
136-
});
137-
138-
expect(result.isError).toBe(true);
139-
const errorText = (result.content[0] as TextContent).text;
140-
expect(errorText).toContain('Input validation error');
145+
const errorText = await expectToolInputValidationError({ name: 'double', arguments: { value: 'not a number' } });
141146
expect(errorText).toContain('value');
142147
expect(errorText).toContain('number');
143148
});
@@ -153,14 +158,7 @@ describe('Standard Schema Support', () => {
153158

154159
await connectClientAndServer();
155160

156-
const result = await client.request({
157-
method: 'tools/call',
158-
params: { name: 'calculate', arguments: { operation: 'divide' } }
159-
});
160-
161-
expect(result.isError).toBe(true);
162-
const errorText = (result.content[0] as TextContent).text;
163-
expect(errorText).toContain('Input validation error');
161+
const errorText = await expectToolInputValidationError({ name: 'calculate', arguments: { operation: 'divide' } });
164162
expect(errorText).toMatch(/add|subtract|multiply/);
165163
});
166164

@@ -173,14 +171,7 @@ describe('Standard Schema Support', () => {
173171

174172
await connectClientAndServer();
175173

176-
const result = await client.request({
177-
method: 'tools/call',
178-
params: { name: 'greet', arguments: { name: 'Alice' } }
179-
});
180-
181-
expect(result.isError).toBe(true);
182-
const errorText = (result.content[0] as TextContent).text;
183-
expect(errorText).toContain('Input validation error');
174+
const errorText = await expectToolInputValidationError({ name: 'greet', arguments: { name: 'Alice' } });
184175
expect(errorText).toContain('age');
185176
});
186177
});
@@ -273,14 +264,7 @@ describe('Standard Schema Support', () => {
273264

274265
await connectClientAndServer();
275266

276-
const result = await client.request({
277-
method: 'tools/call',
278-
params: { name: 'double', arguments: { value: 'not a number' } }
279-
});
280-
281-
expect(result.isError).toBe(true);
282-
const errorText = (result.content[0] as TextContent).text;
283-
expect(errorText).toContain('Input validation error');
267+
const errorText = await expectToolInputValidationError({ name: 'double', arguments: { value: 'not a number' } });
284268
expect(errorText).toContain('number');
285269
});
286270

@@ -297,14 +281,7 @@ describe('Standard Schema Support', () => {
297281

298282
await connectClientAndServer();
299283

300-
const result = await client.request({
301-
method: 'tools/call',
302-
params: { name: 'calculate', arguments: { operation: 'divide' } }
303-
});
304-
305-
expect(result.isError).toBe(true);
306-
const errorText = (result.content[0] as TextContent).text;
307-
expect(errorText).toContain('Input validation error');
284+
await expectToolInputValidationError({ name: 'calculate', arguments: { operation: 'divide' } });
308285
});
309286

310287
test('should validate min/max constraints', async () => {
@@ -328,13 +305,7 @@ describe('Standard Schema Support', () => {
328305
expect(validResult.isError).toBeFalsy();
329306

330307
// Invalid value (too high)
331-
const invalidResult = await client.request({
332-
method: 'tools/call',
333-
params: { name: 'setPercentage', arguments: { percentage: 150 } }
334-
});
335-
expect(invalidResult.isError).toBe(true);
336-
const errorText = (invalidResult.content[0] as TextContent).text;
337-
expect(errorText).toContain('Input validation error');
308+
await expectToolInputValidationError({ name: 'setPercentage', arguments: { percentage: 150 } });
338309
});
339310
});
340311
});
@@ -420,11 +391,7 @@ describe('Standard Schema Support', () => {
420391

421392
await connectClientAndServer();
422393

423-
const result = await client.request({ method: 'tools/call', params: { name: 'double', arguments: { count: 'not a number' } } });
424-
425-
expect(result.isError).toBe(true);
426-
const errorText = (result.content[0] as TextContent).text;
427-
expect(errorText).toContain('Input validation error');
394+
await expectToolInputValidationError({ name: 'double', arguments: { count: 'not a number' } });
428395
});
429396
});
430397

@@ -557,21 +524,15 @@ describe('Standard Schema Support', () => {
557524

558525
await connectClientAndServer();
559526

560-
const result = await client.request({
561-
method: 'tools/call',
562-
params: {
563-
name: 'test',
564-
arguments: {
565-
email: 123,
566-
age: 'not a number',
567-
status: 'unknown'
568-
}
527+
const errorText = await expectToolInputValidationError({
528+
name: 'test',
529+
arguments: {
530+
email: 123,
531+
age: 'not a number',
532+
status: 'unknown'
569533
}
570534
});
571535

572-
expect(result.isError).toBe(true);
573-
const errorText = (result.content[0] as TextContent).text;
574-
575536
// Check that error mentions the specific issues
576537
expect(errorText).toContain('Input validation error');
577538
// ArkType should mention type mismatches
@@ -593,21 +554,15 @@ describe('Standard Schema Support', () => {
593554

594555
await connectClientAndServer();
595556

596-
const result = await client.request({
597-
method: 'tools/call',
598-
params: {
599-
name: 'test',
600-
arguments: {
601-
email: 123,
602-
age: 'not a number',
603-
status: 'unknown'
604-
}
557+
const errorText = await expectToolInputValidationError({
558+
name: 'test',
559+
arguments: {
560+
email: 123,
561+
age: 'not a number',
562+
status: 'unknown'
605563
}
606564
});
607565

608-
expect(result.isError).toBe(true);
609-
const errorText = (result.content[0] as TextContent).text;
610-
611566
// Check that error mentions the specific issues
612567
expect(errorText).toContain('Input validation error');
613568
// Valibot should provide "Invalid type" messages
@@ -627,21 +582,15 @@ describe('Standard Schema Support', () => {
627582

628583
await connectClientAndServer();
629584

630-
const result = await client.request({
631-
method: 'tools/call',
632-
params: {
633-
name: 'test',
634-
arguments: {
635-
email: 123,
636-
age: 'not a number',
637-
status: 'unknown'
638-
}
585+
const errorText = await expectToolInputValidationError({
586+
name: 'test',
587+
arguments: {
588+
email: 123,
589+
age: 'not a number',
590+
status: 'unknown'
639591
}
640592
});
641593

642-
expect(result.isError).toBe(true);
643-
const errorText = (result.content[0] as TextContent).text;
644-
645594
// Check that error mentions the specific issues
646595
expect(errorText).toContain('Input validation error');
647596
});

0 commit comments

Comments
 (0)