Skip to content

Commit 2a4e1d5

Browse files
committed
Preserve tool error semantics for non-input failures
1 parent 2198620 commit 2a4e1d5

3 files changed

Lines changed: 163 additions & 151 deletions

File tree

packages/server/src/server/mcp.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ export class McpServer {
208208
await this.validateToolOutput(tool, result, request.params.name);
209209
return result;
210210
} catch (error) {
211-
if (error instanceof ProtocolError) {
212-
throw error; // Return protocol-level errors to the caller without wrapping in CallToolResult
211+
if (error instanceof ProtocolError && this.shouldReturnProtocolError(error)) {
212+
throw error; // Invalid client-supplied arguments and elicitation requests are protocol errors.
213213
}
214214
return this.createToolError(error instanceof Error ? error.message : String(error));
215215
}
@@ -236,6 +236,13 @@ export class McpServer {
236236
};
237237
}
238238

239+
private shouldReturnProtocolError(error: ProtocolError): boolean {
240+
return (
241+
error.code === ProtocolErrorCode.UrlElicitationRequired ||
242+
(error.code === ProtocolErrorCode.InvalidParams && error.message.startsWith('Input validation error:'))
243+
);
244+
}
245+
239246
/**
240247
* Validates tool input arguments against the tool's input schema.
241248
*/

test/integration/test/server/mcp.test.ts

Lines changed: 48 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Client } from '@modelcontextprotocol/client';
2-
import type { CallToolResult, Notification, TextContent } from '@modelcontextprotocol/core';
2+
import type { CallToolResult, Notification, ProtocolError, TextContent } from '@modelcontextprotocol/core';
33
import {
44
getDisplayName,
55
InMemoryTaskStore,
@@ -28,6 +28,18 @@ function createLatch() {
2828
};
2929
}
3030

31+
async function expectInvalidToolArguments(call: Promise<unknown>): Promise<ProtocolError> {
32+
try {
33+
await call;
34+
throw new Error('Expected invalid tool arguments to reject');
35+
} catch (error) {
36+
const protocolError = error as ProtocolError;
37+
expect(protocolError.code).toBe(ProtocolErrorCode.InvalidParams);
38+
expect(protocolError.message).toContain('Input validation error');
39+
return protocolError;
40+
}
41+
}
42+
3143
describe('Zod v4', () => {
3244
describe('McpServer', () => {
3345
/***
@@ -1212,26 +1224,20 @@ describe('Zod v4', () => {
12121224

12131225
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
12141226

1215-
const result = await client.request({
1216-
method: 'tools/call',
1217-
params: {
1218-
name: 'test',
1219-
arguments: {
1227+
const error = await expectInvalidToolArguments(
1228+
client.request({
1229+
method: 'tools/call',
1230+
params: {
12201231
name: 'test',
1221-
value: 'not a number'
1222-
}
1223-
}
1224-
});
1225-
1226-
expect(result.isError).toBe(true);
1227-
expect(result.content).toEqual(
1228-
expect.arrayContaining([
1229-
{
1230-
type: 'text',
1231-
text: expect.stringContaining('Input validation error: Invalid arguments for tool test')
1232+
arguments: {
1233+
name: 'test',
1234+
value: 'not a number'
1235+
}
12321236
}
1233-
])
1237+
})
12341238
);
1239+
1240+
expect(error.message).toContain('Input validation error: Invalid arguments for tool test');
12351241
});
12361242

12371243
/***
@@ -5149,22 +5155,14 @@ describe('Zod v4', () => {
51495155
await server.connect(serverTransport);
51505156
await client.connect(clientTransport);
51515157

5152-
const invalidTypeResult = await client.callTool({
5153-
name: 'union-test',
5154-
arguments: {
5155-
type: 'a',
5156-
value: 123
5157-
}
5158-
});
5159-
5160-
expect(invalidTypeResult.isError).toBe(true);
5161-
expect(invalidTypeResult.content).toEqual(
5162-
expect.arrayContaining([
5163-
expect.objectContaining({
5164-
type: 'text',
5165-
text: expect.stringContaining('Input validation error')
5166-
})
5167-
])
5158+
await expectInvalidToolArguments(
5159+
client.callTool({
5160+
name: 'union-test',
5161+
arguments: {
5162+
type: 'a',
5163+
value: 123
5164+
}
5165+
})
51685166
);
51695167
});
51705168
});
@@ -6407,40 +6405,24 @@ describe('Zod v4', () => {
64076405
await server.connect(serverTransport);
64086406
await client.connect(clientTransport);
64096407

6410-
const invalidTypeResult = await client.callTool({
6411-
name: 'union-test',
6412-
arguments: {
6413-
type: 'a',
6414-
value: 123
6415-
}
6416-
});
6417-
6418-
expect(invalidTypeResult.isError).toBe(true);
6419-
expect(invalidTypeResult.content).toEqual(
6420-
expect.arrayContaining([
6421-
expect.objectContaining({
6422-
type: 'text',
6423-
text: expect.stringContaining('Input validation error')
6424-
})
6425-
])
6408+
await expectInvalidToolArguments(
6409+
client.callTool({
6410+
name: 'union-test',
6411+
arguments: {
6412+
type: 'a',
6413+
value: 123
6414+
}
6415+
})
64266416
);
64276417

6428-
const invalidDiscriminatorResult = await client.callTool({
6429-
name: 'union-test',
6430-
arguments: {
6431-
type: 'c',
6432-
value: 'test'
6433-
}
6434-
});
6435-
6436-
expect(invalidDiscriminatorResult.isError).toBe(true);
6437-
expect(invalidDiscriminatorResult.content).toEqual(
6438-
expect.arrayContaining([
6439-
expect.objectContaining({
6440-
type: 'text',
6441-
text: expect.stringContaining('Input validation error')
6442-
})
6443-
])
6418+
await expectInvalidToolArguments(
6419+
client.callTool({
6420+
name: 'union-test',
6421+
arguments: {
6422+
type: 'c',
6423+
value: 'test'
6424+
}
6425+
})
64446426
);
64456427
});
64466428
});

0 commit comments

Comments
 (0)