From b9ab06547202c83e044e766d1ce5d7c9cf8d4e10 Mon Sep 17 00:00:00 2001 From: rupesh-kumar-sah Date: Sat, 17 Oct 2026 15:15:55 +0545 Subject: [PATCH] fix(mcp): return structured error for unknown tool calls When an unknown tool was requested, BazaarMcpServer threw a raw Error producing an unparseable JSON-RPC fault instead of an MCP CallToolResult with isError: true. Return a structured ERR_UNKNOWN_TOOL error response containing the list of valid tool names with length-bounded, sanitized input name. Closes #415 --- src/mcp/codes.ts | 1 + src/mcp/server.ts | 10 +++++++++- tests/mcp.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/mcp/codes.ts b/src/mcp/codes.ts index caf9261d..c71bc1bb 100644 --- a/src/mcp/codes.ts +++ b/src/mcp/codes.ts @@ -23,6 +23,7 @@ export const MCP_ERROR_CODES = { // paid_call allow-list — #487 ERR_URL_BLOCKED: 'ERR_URL_BLOCKED', ERR_HOST_NOT_ALLOWED: 'ERR_HOST_NOT_ALLOWED', + ERR_UNKNOWN_TOOL: 'ERR_UNKNOWN_TOOL', ERR_INTERNAL_FAILURE: 'ERR_INTERNAL_FAILURE' } as const; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 0ab584e7..902b9aca 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -72,6 +72,9 @@ function hostOf(url: string): string { } } +export const VALID_TOOL_NAMES = ['bazaar_search', 'paid_call'] as const; +export type ValidToolName = (typeof VALID_TOOL_NAMES)[number]; + export interface McpServerConfig { fetchWithPayment: typeof fetch; /** @@ -479,7 +482,12 @@ export class BazaarMcpServer { } } - throw new Error(`Unknown tool: ${request.params.name}`); + const rawName = String(request.params.name ?? ''); + const boundedName = rawName.slice(0, 64).replace(/[\r\n\t]/g, ' '); + return this.createErrorResponse( + 'ERR_UNKNOWN_TOOL', + `Unknown tool '${boundedName}'. Valid tools are: ${VALID_TOOL_NAMES.join(', ')}.` + ); }); } diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 5a029272..d8271ce3 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -135,6 +135,55 @@ describe('BazaarMcpServer', () => { const parsedText = JSON.parse(response.content[0].text); expect(parsedText.success).toBe(true); }); + + it('should return structured ERR_UNKNOWN_TOOL error with valid tool names when unknown tool is called', async () => { + const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() }); + let handler: any = null; + (server as any).server.setRequestHandler = (schema: any, fn: any) => { + if (schema === CallToolRequestSchema) { + handler = fn; + } + }; + (server as any).setupHandlers(); + + const response = await handler({ + params: { + name: 'non_existent_tool_123', + arguments: {} + } + }); + + expect(response.isError).toBe(true); + const content = JSON.parse(response.content[0].text); + expect(content.code).toBe('ERR_UNKNOWN_TOOL'); + expect(content.reason).toContain("Unknown tool 'non_existent_tool_123'"); + expect(content.reason).toContain('bazaar_search, paid_call'); + }); + + it('should bound and sanitize unknown tool name in error response', async () => { + const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() }); + let handler: any = null; + (server as any).server.setRequestHandler = (schema: any, fn: any) => { + if (schema === CallToolRequestSchema) { + handler = fn; + } + }; + (server as any).setupHandlers(); + + const overlyLongName = 'a'.repeat(200); + const response = await handler({ + params: { + name: overlyLongName, + arguments: {} + } + }); + + expect(response.isError).toBe(true); + const content = JSON.parse(response.content[0].text); + expect(content.code).toBe('ERR_UNKNOWN_TOOL'); + expect(content.reason).toContain('a'.repeat(64)); + expect(content.reason).not.toContain('a'.repeat(65)); + }); }); describe('BazaarMcpServer bazaar_search SSRF guard (#407)', () => {