{"record":{"id":"93a9395c78d03238","repo":"CherryHQ/cherry-studio","slug":"tool-not-found-93a939","errorCode":null,"errorMessage":"Tool not found","messagePattern":"Tool not found","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/fetch.ts","lineNumber":226,"sourceCode":"\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n  const { arguments: args } = request.params\n\n  const validatedArgs = RequestPayloadSchema.parse(args)\n\n  if (request.params.name === 'fetch_html') {\n    return await Fetcher.html(validatedArgs)\n  }\n  if (request.params.name === 'fetch_json') {\n    return await Fetcher.json(validatedArgs)\n  }\n  if (request.params.name === 'fetch_txt') {\n    return await Fetcher.txt(validatedArgs)\n  }\n  if (request.params.name === 'fetch_markdown') {\n    return await Fetcher.markdown(validatedArgs)\n  }\n  throw new Error('Tool not found')\n})\n\nclass FetchServer {\n  public server: Server\n  constructor() {\n    this.server = server\n  }\n}\nexport default FetchServer\n","sourceCodeStart":208,"sourceCodeEnd":236,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/fetch.ts#L208-L236","documentation":"Thrown by the fetch server's CallToolRequest handler when the incoming tool name is not one of fetch_html, fetch_json, fetch_txt, or fetch_markdown. Unlike the filesystem server, this handler has no surrounding try/catch, so the throw propagates out of the request handler to the MCP SDK transport, which converts it into a JSON-RPC error response rather than a tool result. It indicates a mismatch between the tools the server advertised in ListTools and what the client actually called.","triggerScenarios":"An MCP client dispatches a CallToolRequest with request.params.name set to a typo ('fetch-html', 'html', 'fetch_html '), an unregistered tool, or a tool name from a different server version. Also reachable if a client caches an old tool list and calls a since-removed tool.","commonSituations":"Client/server version skew after adding or renaming a fetch tool; a model hallucinating a tool name; a proxy or orchestrator routing the request to the wrong server instance; trailing whitespace or case differences in the name field.","solutions":["Compare the name sent by the client against the four registered names (fetch_html, fetch_json, fetch_txt, fetch_markdown) exactly — note underscores, not hyphens.","Ensure the client refreshes its tool list via ListTools before calling; do not rely on a cached schema.","If introducing or renaming a tool, update both ListToolsRequestSchema output and the dispatch chain in the same change, and bump the server version.","Wrap the handler in try/catch to return an isError tool result instead of a transport-level JSON-RPC error, matching the filesystem server's pattern at server.ts:80-117."],"exampleFix":"// before\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n  const { arguments: args } = request.params\n  const validatedArgs = RequestPayloadSchema.parse(args)\n  if (request.params.name === 'fetch_html') return await Fetcher.html(validatedArgs)\n  // ...\n  throw new Error('Tool not found')\n})\n\n// after — normalize name, return a tool result on miss\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\n  const name = request.params.name\n  const handlers: Record<string, (a: RequestPayload) => Promise<unknown>> = {\n    fetch_html: Fetcher.html, fetch_json: Fetcher.json,\n    fetch_txt: Fetcher.txt, fetch_markdown: Fetcher.markdown\n  }\n  const handler = handlers[name]\n  if (!handler) {\n    return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }\n  }\n  return await handler(RequestPayloadSchema.parse(request.params.arguments))\n})","handlingStrategy":"validation","validationCode":"// Validate the tool name against the registered list before dispatching.\nconst FETCH_TOOLS = new Set(['fetch_html', 'fetch_json', 'fetch_txt', 'fetch_markdown'])\nfunction isValidFetchTool(name: unknown): name is string {\n  return typeof name === 'string' && FETCH_TOOLS.has(name)\n}\nif (!isValidFetchTool(request.params.name)) {\n  return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true }\n}","typeGuard":"function isKnownFetchTool(name: string): boolean {\n  return ['fetch_html', 'fetch_json', 'fetch_txt', 'fetch_markdown'].includes(name)\n}","tryCatchPattern":"// Wrap the dispatch so a miss returns a tool result, matching the filesystem server.\ntry {\n  if (!isKnownFetchTool(name)) {\n    return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }\n  }\n  return await handlers[name](validatedArgs)\n} catch (e) {\n  return { content: [{ type: 'text', text: (e as Error).message }], isError: true }\n}","preventionTips":["Keep the ListTools response and the dispatch chain in one place (a single record/map).","Have the client always re-fetch the tool list before calling after a server upgrade.","Normalize the tool name (trim, lowercase) before comparison to catch trivial typos.","Return an isError tool result on miss rather than throwing, so the SDK transport stays consistent."],"tags":["mcp-tool","dispatch","api-contract"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}