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
15 changes: 15 additions & 0 deletions .changeset/fix-unknown-tool-protocol-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@modelcontextprotocol/core": minor
"@modelcontextprotocol/server": minor
---

BREAKING: Fix error handling for unknown tools and resources per MCP spec

**Tools:** Unknown or disabled tool calls now return JSON-RPC protocol errors with
code `-32602` (InvalidParams) instead of `CallToolResult` with `isError: true`.
Users who checked `result.isError` for unknown tools should catch rejected promises instead.

**Resources:** Unknown resource reads now return error code `-32002` (ResourceNotFound)
instead of `-32602` (InvalidParams), per the MCP specification.

Added `ErrorCode.ResourceNotFound` to the ErrorCode enum.
2 changes: 1 addition & 1 deletion packages/client/test/client/auth-extensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ describe('createPrivateKeyJwtAuth', () => {

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

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export enum ErrorCode {
// SDK error codes
ConnectionClosed = -32000,
RequestTimeout = -32001,
ResourceNotFound = -32002,

// Standard JSON-RPC error codes
ParseError = -32700,
Expand Down
20 changes: 11 additions & 9 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,17 @@ export class McpServer {
);

this.server.setRequestHandler(CallToolRequestSchema, async (request, extra): Promise<CallToolResult | CreateTaskResult> => {
try {
const tool = this._registeredTools[request.params.name];
if (!tool) {
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
}
if (!tool.enabled) {
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
}
// Unknown tool is a protocol error per MCP spec.
// Check before try/catch block so it propagates as a JSON-RPC error instead of CallToolResult.
const tool = this._registeredTools[request.params.name];
if (!tool) {
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
}
if (!tool.enabled) {
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
}

try {
const isTaskRequest = !!request.params.task;
const taskSupport = tool.execution?.taskSupport;
const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler<ZodRawShapeCompat>);
Expand Down Expand Up @@ -557,7 +559,7 @@ export class McpServer {
}
}

throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
throw new McpError(ErrorCode.ResourceNotFound, `Resource ${uri} not found`);
});

this._resourceHandlersInitialized = true;
Expand Down
129 changes: 112 additions & 17 deletions test/integration/test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1707,25 +1707,68 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {

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

const result = await client.request(
{
method: 'tools/call',
params: {
name: 'nonexistent-tool'
}
},
CallToolResultSchema
);
// Unknown tool should return a JSON-RPC protocol error per MCP spec.
await expect(
client.request(
{
method: 'tools/call',
params: {
name: 'nonexistent-tool'
}
},
CallToolResultSchema
)
).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
message: expect.stringContaining('nonexistent-tool')
});
});

expect(result.isError).toBe(true);
expect(result.content).toEqual(
expect.arrayContaining([
/***
* Test: McpError for Disabled Tool
*/
test('should throw McpError for disabled tool', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});

const client = new Client({
name: 'test client',
version: '1.0'
});

const tool = mcpServer.tool('test-tool', async () => ({
content: [
{
type: 'text',
text: expect.stringContaining('Tool nonexistent-tool not found')
text: 'Test response'
}
])
);
]
}));

// Disable the tool
tool.disable();

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

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

// Disabled tool should return a JSON-RPC protocol error per MCP spec.
await expect(
client.request(
{
method: 'tools/call',
params: {
name: 'test-tool'
}
},
CallToolResultSchema
)
).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
message: expect.stringContaining('disabled')
});
});

/***
Expand Down Expand Up @@ -2753,7 +2796,56 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
ReadResourceResultSchema
)
).rejects.toThrow(/Resource test:\/\/nonexistent not found/);
).rejects.toMatchObject({
code: ErrorCode.ResourceNotFound,
message: expect.stringContaining('not found')
});
});

/***
* Test: McpError for Disabled Resource
*/
test('should throw McpError for disabled resource', async () => {
const mcpServer = new McpServer({
name: 'test server',
version: '1.0'
});
const client = new Client({
name: 'test client',
version: '1.0'
});

const resource = mcpServer.resource('test', 'test://resource', async () => ({
contents: [
{
uri: 'test://resource',
text: 'Test content'
}
]
}));

// Disable the resource
resource.disable();

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

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

// Disabled resource should return a JSON-RPC protocol error.
await expect(
client.request(
{
method: 'resources/read',
params: {
uri: 'test://resource'
}
},
ReadResourceResultSchema
)
).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
message: expect.stringContaining('disabled')
});
});

/***
Expand Down Expand Up @@ -3667,7 +3759,10 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
},
GetPromptResultSchema
)
).rejects.toThrow(/Prompt nonexistent-prompt not found/);
).rejects.toMatchObject({
code: ErrorCode.InvalidParams,
message: expect.stringContaining('not found')
});
});

/***
Expand Down
Loading