Skip to content

Commit 108f2f3

Browse files
fix: correct error handling for unknown tools and resources (#1389)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
1 parent ad4b2f9 commit 108f2f3

File tree

5 files changed

+123
-23
lines changed

5 files changed

+123
-23
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@modelcontextprotocol/core": minor
3+
"@modelcontextprotocol/server": major
4+
---
5+
6+
Fix error handling for unknown tools and resources per MCP spec.
7+
8+
**Tools:** Unknown or disabled tool calls now return JSON-RPC protocol errors with
9+
code `-32602` (InvalidParams) instead of `CallToolResult` with `isError: true`.
10+
Callers who checked `result.isError` for unknown tools should catch rejected promises instead.
11+
12+
**Resources:** Unknown resource reads now return error code `-32002` (ResourceNotFound)
13+
instead of `-32602` (InvalidParams).
14+
15+
Added `ProtocolErrorCode.ResourceNotFound`.

packages/client/test/client/authExtensions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ describe('createPrivateKeyJwtAuth', () => {
305305

306306
const params = new URLSearchParams();
307307
await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
308-
/Invalid character/
308+
/cannot be part of a valid base64|Invalid character/
309309
);
310310
});
311311

packages/core/src/types/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export enum ProtocolErrorCode {
233233
InternalError = -32_603,
234234

235235
// MCP-specific error codes
236+
ResourceNotFound = -32_002,
236237
UrlElicitationRequired = -32_042
237238
}
238239

packages/server/src/server/mcp.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,15 @@ export class McpServer {
166166
);
167167

168168
this.server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult | CreateTaskResult> => {
169-
try {
170-
const tool = this._registeredTools[request.params.name];
171-
if (!tool) {
172-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
173-
}
174-
if (!tool.enabled) {
175-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
176-
}
169+
const tool = this._registeredTools[request.params.name];
170+
if (!tool) {
171+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
172+
}
173+
if (!tool.enabled) {
174+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
175+
}
177176

177+
try {
178178
const isTaskRequest = !!request.params.task;
179179
const taskSupport = tool.execution?.taskSupport;
180180
const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler<AnySchema>);
@@ -506,7 +506,7 @@ export class McpServer {
506506
}
507507
}
508508

509-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} not found`);
509+
throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, `Resource ${uri} not found`);
510510
});
511511

512512
this._resourceHandlersInitialized = true;

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

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,22 +1777,59 @@ describe('Zod v4', () => {
17771777

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

1780-
const result = await client.request({
1781-
method: 'tools/call',
1782-
params: {
1783-
name: 'nonexistent-tool'
1784-
}
1780+
await expect(
1781+
client.request({
1782+
method: 'tools/call',
1783+
params: {
1784+
name: 'nonexistent-tool'
1785+
}
1786+
})
1787+
).rejects.toMatchObject({
1788+
code: ProtocolErrorCode.InvalidParams,
1789+
message: expect.stringContaining('nonexistent-tool')
17851790
});
1791+
});
17861792

1787-
expect(result.isError).toBe(true);
1788-
expect(result.content).toEqual(
1789-
expect.arrayContaining([
1793+
/***
1794+
* Test: ProtocolError for Disabled Tool
1795+
*/
1796+
test('should throw ProtocolError for disabled tool', async () => {
1797+
const mcpServer = new McpServer({
1798+
name: 'test server',
1799+
version: '1.0'
1800+
});
1801+
1802+
const client = new Client({
1803+
name: 'test client',
1804+
version: '1.0'
1805+
});
1806+
1807+
const tool = mcpServer.registerTool('test-tool', {}, async () => ({
1808+
content: [
17901809
{
17911810
type: 'text',
1792-
text: expect.stringContaining('Tool nonexistent-tool not found')
1811+
text: 'Test response'
17931812
}
1794-
])
1795-
);
1813+
]
1814+
}));
1815+
1816+
tool.disable();
1817+
1818+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1819+
1820+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
1821+
1822+
await expect(
1823+
client.request({
1824+
method: 'tools/call',
1825+
params: {
1826+
name: 'test-tool'
1827+
}
1828+
})
1829+
).rejects.toMatchObject({
1830+
code: ProtocolErrorCode.InvalidParams,
1831+
message: expect.stringContaining('disabled')
1832+
});
17961833
});
17971834

17981835
/***
@@ -2797,7 +2834,51 @@ describe('Zod v4', () => {
27972834
uri: 'test://nonexistent'
27982835
}
27992836
})
2800-
).rejects.toThrow(/Resource test:\/\/nonexistent not found/);
2837+
).rejects.toMatchObject({
2838+
code: ProtocolErrorCode.ResourceNotFound,
2839+
message: expect.stringContaining('not found')
2840+
});
2841+
});
2842+
2843+
/***
2844+
* Test: ProtocolError for Disabled Resource
2845+
*/
2846+
test('should throw ProtocolError for disabled resource', async () => {
2847+
const mcpServer = new McpServer({
2848+
name: 'test server',
2849+
version: '1.0'
2850+
});
2851+
const client = new Client({
2852+
name: 'test client',
2853+
version: '1.0'
2854+
});
2855+
2856+
const resource = mcpServer.registerResource('test', 'test://resource', {}, async () => ({
2857+
contents: [
2858+
{
2859+
uri: 'test://resource',
2860+
text: 'Test content'
2861+
}
2862+
]
2863+
}));
2864+
2865+
resource.disable();
2866+
2867+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
2868+
2869+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
2870+
2871+
await expect(
2872+
client.request({
2873+
method: 'resources/read',
2874+
params: {
2875+
uri: 'test://resource'
2876+
}
2877+
})
2878+
).rejects.toMatchObject({
2879+
code: ProtocolErrorCode.InvalidParams,
2880+
message: expect.stringContaining('disabled')
2881+
});
28012882
});
28022883

28032884
/***
@@ -3693,7 +3774,10 @@ describe('Zod v4', () => {
36933774
name: 'nonexistent-prompt'
36943775
}
36953776
})
3696-
).rejects.toThrow(/Prompt nonexistent-prompt not found/);
3777+
).rejects.toMatchObject({
3778+
code: ProtocolErrorCode.InvalidParams,
3779+
message: expect.stringContaining('not found')
3780+
});
36973781
});
36983782

36993783
/***

0 commit comments

Comments
 (0)