diff --git a/.changeset/fix-jsonrpc-error-nonexistent-tool.md b/.changeset/fix-jsonrpc-error-nonexistent-tool.md new file mode 100644 index 000000000..46020cb1e --- /dev/null +++ b/.changeset/fix-jsonrpc-error-nonexistent-tool.md @@ -0,0 +1,7 @@ +--- +"@modelcontextprotocol/sdk": patch +--- + +fix: return JSON-RPC error for nonexistent tool calls + +When `callTool` is invoked with a tool name that doesn't exist in the registered handlers, the server now returns a proper JSON-RPC error with code -32601 (Method not found) instead of returning an error in the content array. This aligns with JSON-RPC 2.0 specification and provides clearer error handling for clients. diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 27b308285..6ca589a96 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -198,9 +198,20 @@ export class McpServer { await this.validateToolOutput(tool, result, request.params.name); return result; } catch (error) { + // Per MCP spec, tool not found should return a JSON-RPC error, not isError result + // https://modelcontextprotocol.io/specification/2025-11-25/server/tools#error-handling + if ( + error instanceof ProtocolError && + (error.code === ProtocolErrorCode.InvalidParams || error.code === ProtocolErrorCode.MethodNotFound) && + error.message.includes('not found') + ) { + throw error; + } + // URL elicitation errors should propagate if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) { - throw error; // Return the error to the caller without wrapping in CallToolResult + throw error; } + // Other errors (execution, validation) get wrapped in CallToolResult with isError: true return this.createToolError(error instanceof Error ? error.message : String(error)); } }); diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 091e4ac21..566cde805 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1812,6 +1812,7 @@ describe('Zod v4', () => { /*** * Test: ProtocolError for Invalid Tool Name + * Per MCP spec, calling a nonexistent tool should return a JSON-RPC error, not a result with isError */ test('should throw ProtocolError for invalid tool name', async () => { const mcpServer = new McpServer({ @@ -1837,25 +1838,17 @@ describe('Zod v4', () => { await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); - const result = await client.request( - { - method: 'tools/call', - params: { - name: 'nonexistent-tool' - } - }, - CallToolResultSchema - ); - - expect(result.isError).toBe(true); - expect(result.content).toEqual( - expect.arrayContaining([ + await expect( + client.request( { - type: 'text', - text: expect.stringContaining('Tool nonexistent-tool not found') - } - ]) - ); + method: 'tools/call', + params: { + name: 'nonexistent-tool' + } + }, + CallToolResultSchema + ) + ).rejects.toThrow(/Tool nonexistent-tool not found/); }); /***