{"record":{"id":"6dc83c7a825ed6a3","repo":"koala73/worldmonitor","slug":"tools-call-error-callresp-error-message","errorCode":null,"errorMessage":"tools/call error: ${callResp.error.message}","messagePattern":"tools/call error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":422,"severity":"error","filePath":"api/mcp-proxy.ts","lineNumber":620,"sourceCode":"  const { response: callResp } = await postJson(sessionUrl, {\n    jsonrpc: '2.0', id: 3, method: 'tools/call',\n    params: { name: toolName, arguments: toolArgs || {} },\n  }, headers, sessionId);\n  if (!callResp.ok) throw new McpProxyUpstreamError(`tools/call failed: HTTP ${callResp.status}`);\n  const callData = await parseJsonRpcResponse(callResp);\n  if (callData.error) throw new McpProxyUpstreamError('tools/call error: MCP server rejected request');\n  return callData.result;\n}\n\n// --- SSE transport (HTTP+SSE, older MCP spec) ---\n// Servers whose URL path ends with /sse use this protocol:\n//   1. Client GETs the SSE URL — server opens a stream and emits an `endpoint` event\n//      containing the URL where the client should POST JSON-RPC messages.\n//   2. Client POSTs JSON-RPC to that endpoint URL.\n//   3. Server sends responses on the same SSE stream as `data:` lines.\n\nfunction isSseTransport(url) {\n  const p = url.pathname;\n  return p === '/sse' || p.endsWith('/sse');\n}\n\nfunction makeDeferred() {\n  let resolve, reject;\n  const promise = new Promise((res, rej) => { resolve = res; reject = rej; });\n  return { promise, resolve, reject };\n}\n\nclass SseSession {\n  constructor(sseUrl, headers) {\n    this._sseUrl = sseUrl;\n    this._originHost = new URL(sseUrl).host;\n    this._originProtocol = new URL(sseUrl).protocol;\n    this._headers = headers;\n    this._endpointUrl = null;\n    this._endpointDeferred = makeDeferred();\n    this._pending = new Map(); // rpc id -> deferred","sourceCodeStart":602,"sourceCodeEnd":638,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/api/mcp-proxy.ts#L602-L638","documentation":"After a successful initialize, mcp-proxy sends JSON-RPC `tools/call` (id 2) with the requested toolName and arguments (defaulting to {}). This error means the server accepted the session but rejected the call itself: unknown tool, arguments failing the tool's input schema, or the tool executed and reported its own failure through the RPC error channel.","triggerScenarios":"toolName does not exist on the server (typo, renamed tool, different server version); toolArgs missing required fields or with wrong types per the tool's inputSchema; calling a tool with {} when it requires parameters; the tool ran and failed server-side, returning its error via the RPC layer.","commonSituations":"Frontend hardcodes a tool name that changed after a server update; callers assume the proxy validates arguments; passing nested objects as strings; one-shot SSE sessions used for tools that need prior session state.","solutions":["List the server's tools (tools/list through the proxy, or `worldmonitor tools`) and copy the exact name","Validate toolArgs against the tool's inputSchema — fill every required field with the right primitive type","Decode the embedded message: -32602 invalid params, -32601 unknown tool/method, other codes are tool-specific failures","If the tool itself failed, correct the inputs it names (bad country code, unknown id) and retry"],"exampleFix":"// before\nconst callResp = await session.send(2, 'tools/call', { name: toolName, arguments: toolArgs || {} });\nif (callResp.error) throw new Error(`tools/call error: ${callResp.error.message}`);\n\n// after — list tools first and fail with the available names\nconst listResp = await session.send(2, 'tools/list', {});\nconst tools = (listResp.result && listResp.result.tools) || [];\nif (!tools.some((t) => t.name === toolName)) {\n  throw new Error(`Unknown tool: ${toolName}. Available: ${tools.map((t) => t.name).join(', ')}`);\n}","handlingStrategy":"validation","validationCode":"// Fetch tools/list once, then validate name + required args before calling\nconst { result } = await mcpCallToolSse(serverUrl, '__list__', undefined, headers); // or a tools/list helper\nconst tool = tools.find((t) => t.name === toolName);\nif (!tool) throw new UsageError(`Unknown tool ${toolName}`);\nconst required = tool.inputSchema?.required ?? [];\nconst missing = required.filter((k) => args?.[k] === undefined);\nif (missing.length) throw new UsageError(`Missing args: ${missing.join(', ')}`);","typeGuard":"function isToolDescriptor(v: unknown): v is { name: string; inputSchema?: { required?: string[] } } {\n  return typeof v === 'object' && v !== null && typeof (v as any).name === 'string';\n}","tryCatchPattern":"try {\n  await mcpCallToolSse(serverUrl, toolName, args, headers);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('tools/call error:')) {\n    // server-side rejection: unknown tool or invalid args — fix inputs, do not blind-retry\n    return { retryable: false, reason: err.message };\n  }\n  throw err;\n}","preventionTips":["Cache tools/list per server and drive argument forms from the tool's inputSchema","Validate required fields and primitive types client-side before sending tools/call","Treat -32602/-32601 style rejections as permanent (fix the request), not transient","Integration-test tool names after every target-server upgrade"],"tags":["mcp","jsonrpc","tool-call","input-validation"],"backgroundTag":"mcp-tool-call-failed","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}