Skip to content

Commit 7387c44

Browse files
Fix: Non-existent tool, disabled tool and inputSchema validation return MCP protocol level instead of CallToolResult with isError: true (#1044)
1 parent 874aa27 commit 7387c44

File tree

3 files changed

+150
-116
lines changed

3 files changed

+150
-116
lines changed

package-lock.json

Lines changed: 1 addition & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/server/mcp.test.ts

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -928,37 +928,53 @@ describe('tool()', () => {
928928

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

931-
await expect(
932-
client.request(
933-
{
934-
method: 'tools/call',
935-
params: {
931+
const result = await client.request(
932+
{
933+
method: 'tools/call',
934+
params: {
935+
name: 'test',
936+
arguments: {
936937
name: 'test',
937-
arguments: {
938-
name: 'test',
939-
value: 'not a number'
940-
}
938+
value: 'not a number'
941939
}
942-
},
943-
CallToolResultSchema
944-
)
945-
).rejects.toThrow(/Invalid arguments/);
940+
}
941+
},
942+
CallToolResultSchema
943+
);
946944

947-
await expect(
948-
client.request(
945+
expect(result.isError).toBe(true);
946+
expect(result.content).toEqual(
947+
expect.arrayContaining([
949948
{
950-
method: 'tools/call',
951-
params: {
952-
name: 'test (new api)',
953-
arguments: {
954-
name: 'test',
955-
value: 'not a number'
956-
}
949+
type: 'text',
950+
text: expect.stringContaining('Input validation error: Invalid arguments for tool test')
951+
}
952+
])
953+
);
954+
955+
const result2 = await client.request(
956+
{
957+
method: 'tools/call',
958+
params: {
959+
name: 'test (new api)',
960+
arguments: {
961+
name: 'test',
962+
value: 'not a number'
957963
}
958-
},
959-
CallToolResultSchema
960-
)
961-
).rejects.toThrow(/Invalid arguments/);
964+
}
965+
},
966+
CallToolResultSchema
967+
);
968+
969+
expect(result2.isError).toBe(true);
970+
expect(result2.content).toEqual(
971+
expect.arrayContaining([
972+
{
973+
type: 'text',
974+
text: expect.stringContaining('Input validation error: Invalid arguments for tool test (new api)')
975+
}
976+
])
977+
);
962978
});
963979

964980
/***
@@ -1152,14 +1168,24 @@ describe('tool()', () => {
11521168
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
11531169

11541170
// Call the tool and expect it to throw an error
1155-
await expect(
1156-
client.callTool({
1157-
name: 'test',
1158-
arguments: {
1159-
input: 'hello'
1171+
const result = await client.callTool({
1172+
name: 'test',
1173+
arguments: {
1174+
input: 'hello'
1175+
}
1176+
});
1177+
1178+
expect(result.isError).toBe(true);
1179+
expect(result.content).toEqual(
1180+
expect.arrayContaining([
1181+
{
1182+
type: 'text',
1183+
text: expect.stringContaining(
1184+
'Output validation error: Tool test has an output schema but no structured content was provided'
1185+
)
11601186
}
1161-
})
1162-
).rejects.toThrow(/Tool test has an output schema but no structured content was provided/);
1187+
])
1188+
);
11631189
});
11641190
/***
11651191
* Test: Tool with Output Schema Must Provide Structured Content
@@ -1274,14 +1300,22 @@ describe('tool()', () => {
12741300
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
12751301

12761302
// Call the tool and expect it to throw a server-side validation error
1277-
await expect(
1278-
client.callTool({
1279-
name: 'test',
1280-
arguments: {
1281-
input: 'hello'
1303+
const result = await client.callTool({
1304+
name: 'test',
1305+
arguments: {
1306+
input: 'hello'
1307+
}
1308+
});
1309+
1310+
expect(result.isError).toBe(true);
1311+
expect(result.content).toEqual(
1312+
expect.arrayContaining([
1313+
{
1314+
type: 'text',
1315+
text: expect.stringContaining('Output validation error: Invalid structured content for tool test')
12821316
}
1283-
})
1284-
).rejects.toThrow(/Invalid structured content for tool test/);
1317+
])
1318+
);
12851319
});
12861320

12871321
/***
@@ -1552,17 +1586,25 @@ describe('tool()', () => {
15521586

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

1555-
await expect(
1556-
client.request(
1589+
const result = await client.request(
1590+
{
1591+
method: 'tools/call',
1592+
params: {
1593+
name: 'nonexistent-tool'
1594+
}
1595+
},
1596+
CallToolResultSchema
1597+
);
1598+
1599+
expect(result.isError).toBe(true);
1600+
expect(result.content).toEqual(
1601+
expect.arrayContaining([
15571602
{
1558-
method: 'tools/call',
1559-
params: {
1560-
name: 'nonexistent-tool'
1561-
}
1562-
},
1563-
CallToolResultSchema
1564-
)
1565-
).rejects.toThrow(/Tool nonexistent-tool not found/);
1603+
type: 'text',
1604+
text: expect.stringContaining('Tool nonexistent-tool not found')
1605+
}
1606+
])
1607+
);
15661608
});
15671609

15681610
/***

src/server/mcp.ts

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -126,73 +126,55 @@ export class McpServer {
126126

127127
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise<CallToolResult> => {
128128
const tool = this._registeredTools[request.params.name];
129-
if (!tool) {
130-
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
131-
}
132-
133-
if (!tool.enabled) {
134-
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
135-
}
136129

137130
let result: CallToolResult;
138131

139-
if (tool.inputSchema) {
140-
const parseResult = await tool.inputSchema.safeParseAsync(request.params.arguments);
141-
if (!parseResult.success) {
142-
throw new McpError(
143-
ErrorCode.InvalidParams,
144-
`Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`
145-
);
132+
try {
133+
if (!tool) {
134+
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
146135
}
147136

148-
const args = parseResult.data;
149-
const cb = tool.callback as ToolCallback<ZodRawShape>;
150-
try {
151-
result = await Promise.resolve(cb(args, extra));
152-
} catch (error) {
153-
result = {
154-
content: [
155-
{
156-
type: 'text',
157-
text: error instanceof Error ? error.message : String(error)
158-
}
159-
],
160-
isError: true
161-
};
162-
}
163-
} else {
164-
const cb = tool.callback as ToolCallback<undefined>;
165-
try {
166-
result = await Promise.resolve(cb(extra));
167-
} catch (error) {
168-
result = {
169-
content: [
170-
{
171-
type: 'text',
172-
text: error instanceof Error ? error.message : String(error)
173-
}
174-
],
175-
isError: true
176-
};
137+
if (!tool.enabled) {
138+
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
177139
}
178-
}
179140

180-
if (tool.outputSchema && !result.isError) {
181-
if (!result.structuredContent) {
182-
throw new McpError(
183-
ErrorCode.InvalidParams,
184-
`Tool ${request.params.name} has an output schema but no structured content was provided`
185-
);
141+
if (tool.inputSchema) {
142+
const cb = tool.callback as ToolCallback<ZodRawShape>;
143+
const parseResult = await tool.inputSchema.safeParseAsync(request.params.arguments);
144+
if (!parseResult.success) {
145+
throw new McpError(
146+
ErrorCode.InvalidParams,
147+
`Input validation error: Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`
148+
);
149+
}
150+
151+
const args = parseResult.data;
152+
153+
result = await Promise.resolve(cb(args, extra));
154+
} else {
155+
const cb = tool.callback as ToolCallback<undefined>;
156+
result = await Promise.resolve(cb(extra));
186157
}
187158

188-
// if the tool has an output schema, validate structured content
189-
const parseResult = await tool.outputSchema.safeParseAsync(result.structuredContent);
190-
if (!parseResult.success) {
191-
throw new McpError(
192-
ErrorCode.InvalidParams,
193-
`Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}`
194-
);
159+
if (tool.outputSchema && !result.isError) {
160+
if (!result.structuredContent) {
161+
throw new McpError(
162+
ErrorCode.InvalidParams,
163+
`Output validation error: Tool ${request.params.name} has an output schema but no structured content was provided`
164+
);
165+
}
166+
167+
// if the tool has an output schema, validate structured content
168+
const parseResult = await tool.outputSchema.safeParseAsync(result.structuredContent);
169+
if (!parseResult.success) {
170+
throw new McpError(
171+
ErrorCode.InvalidParams,
172+
`Output validation error: Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}`
173+
);
174+
}
195175
}
176+
} catch (error) {
177+
return this.createToolError(error instanceof Error ? error.message : String(error));
196178
}
197179

198180
return result;
@@ -201,6 +183,24 @@ export class McpServer {
201183
this._toolHandlersInitialized = true;
202184
}
203185

186+
/**
187+
* Creates a tool error result.
188+
*
189+
* @param errorMessage - The error message.
190+
* @returns The tool error result.
191+
*/
192+
private createToolError(errorMessage: string): CallToolResult {
193+
return {
194+
content: [
195+
{
196+
type: 'text',
197+
text: errorMessage
198+
}
199+
],
200+
isError: true
201+
};
202+
}
203+
204204
private _completionHandlerInitialized = false;
205205

206206
private setCompletionRequestHandler() {

0 commit comments

Comments
 (0)