CherryHQ/cherry-studio · error · Error
Tool not found
Error message
Tool not found
What it means
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.
Source
Thrown at src/main/ai/mcp/servers/fetch.ts:226
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { arguments: args } = request.params
const validatedArgs = RequestPayloadSchema.parse(args)
if (request.params.name === 'fetch_html') {
return await Fetcher.html(validatedArgs)
}
if (request.params.name === 'fetch_json') {
return await Fetcher.json(validatedArgs)
}
if (request.params.name === 'fetch_txt') {
return await Fetcher.txt(validatedArgs)
}
if (request.params.name === 'fetch_markdown') {
return await Fetcher.markdown(validatedArgs)
}
throw new Error('Tool not found')
})
class FetchServer {
public server: Server
constructor() {
this.server = server
}
}
export default FetchServer
View on GitHub (pinned to 726446b54c)
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.
Example fix
// before
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { arguments: args } = request.params
const validatedArgs = RequestPayloadSchema.parse(args)
if (request.params.name === 'fetch_html') return await Fetcher.html(validatedArgs)
// ...
throw new Error('Tool not found')
})
// after — normalize name, return a tool result on miss
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const name = request.params.name
const handlers: Record<string, (a: RequestPayload) => Promise<unknown>> = {
fetch_html: Fetcher.html, fetch_json: Fetcher.json,
fetch_txt: Fetcher.txt, fetch_markdown: Fetcher.markdown
}
const handler = handlers[name]
if (!handler) {
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
}
return await handler(RequestPayloadSchema.parse(request.params.arguments))
}) Defensive patterns
Strategy: validation
Validate before calling
// Validate the tool name against the registered list before dispatching.
const FETCH_TOOLS = new Set(['fetch_html', 'fetch_json', 'fetch_txt', 'fetch_markdown'])
function isValidFetchTool(name: unknown): name is string {
return typeof name === 'string' && FETCH_TOOLS.has(name)
}
if (!isValidFetchTool(request.params.name)) {
return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true }
} Type guard
function isKnownFetchTool(name: string): boolean {
return ['fetch_html', 'fetch_json', 'fetch_txt', 'fetch_markdown'].includes(name)
} Try / catch
// Wrap the dispatch so a miss returns a tool result, matching the filesystem server.
try {
if (!isKnownFetchTool(name)) {
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
}
return await handlers[name](validatedArgs)
} catch (e) {
return { content: [{ type: 'text', text: (e as Error).message }], isError: true }
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unknown tool: ${name}
- Unknown tool: ${name}
- Unknown tool: ${name}
- Failed to fetch ${url}: ${e.message}
- Invalid arguments for delete: ${parsed.error}
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/93a9395c78d03238.
Report an issue: GitHub.