Hmbown/CodeWhale · error · ServerError
unknown_tool
unknown_tool
Error message
unknown tool "${step.tool}" What it means
run_actions validates each step's tool name against the server's registered TOOL_NAMES set. Any step referencing a tool the MCP server does not expose (typo, removed/renamed tool, or the nesting-disallowed run_actions itself caught earlier) throws unknown_tool. It exists so a batched sequence fails fast at validation rather than mid-dispatch.
Solutions
- Check the server's tool list (tools/list or TOOL_NAMES) and use an exact registered name for every step
- Fix the typo in step.tool
- Split the batch: run only steps with valid tools and replace or drop the invalid one
- If a needed tool is genuinely missing, upgrade the computer-use plugin/server to a version that exposes it
Example fix
// before
steps: [{ tool: "screenshots" }]
// after
steps: [{ tool: "screenshot" }] Defensive patterns
Strategy: validation
Validate before calling
const TOOL_NAMES = new Set(["screenshot","zoom","left_click","type","key","get_app_state","find_elements","focus","run_actions","app_script"]); // use the server's actual tool list
if (!steps.every(s => typeof s?.tool === "string" && TOOL_NAMES.has(s.tool))) throw new Error("run_actions step has unregistered tool"); Type guard
const isValidStep = (s) => !!s && typeof s.tool === "string" && TOOL_NAMES.has(s.tool);
Try / catch
try { await runActions({ steps }) } catch (e) { if (e.code === "unknown_tool") { /* fix or drop steps where !TOOL_NAMES.has(step.tool) */ } else throw e; } Prevention
- Fetch tools/list at session start and validate step names against it
- Centralize tool-name constants instead of hardcoding strings in prompts
- Re-validate cached step scripts after server upgrades
When it happens
Trigger: Calling run_actions with a steps array whose step.tool is not in TOOL_NAMES — e.g. misspelled 'screenshots', a tool name from a different MCP server, or an alias the server never registered.
Common situations: Model or client hallucinating tool names; renaming tools across server versions while cached prompts still reference old names; copy-pasting step lists between computer-use servers with different tool catalogs.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- app_denied
- bad_target
- click supports left with 1-3 clicks, right x1 or middle x1
- computer_observation_required
- consent_required
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/35187010249e03eb.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/mcp/server.mjs:856
let binding;
let dispatched = false;
try {
binding = await bindComputer(computer);
if (binding.needsObservation && !ROUTE_INSPECTION_TOOLS.has(name)) {
throw new ServerError("computer_observation_required", "Computer route changed — call screenshot or get_app_state on the registered target before acting");
}
// Per-app consent: the first call that targets an application on the local
// computer must carry a recorded user decision. open_application returns
// the grant so its resolved identity can be aliased below.
const gateResult = await consentCheck(computer, name, args);
if (name === "run_actions") {
const steps = args.steps;
if (!Array.isArray(steps) || steps.length < 1 || steps.length > 8) throw new ServerError("bad_args", "run_actions needs 1..8 steps");
const results = [];
for (const [i, step] of steps.entries()) {
if (!step || typeof step.tool !== "string") throw new ServerError("bad_args", `step ${i} needs a tool name`);
if (step.tool === "run_actions") throw new ServerError("bad_args", "run_actions cannot nest");
if (!TOOL_NAMES.has(step.tool)) throw new ServerError("unknown_tool", `unknown tool "${step.tool}"`);
const result = await callTool({ name: step.tool, arguments: { ...(step.arguments ?? {}), computer: computer.id } });
const body = JSON.parse(result.content[0].text);
results.push({ tool: step.tool, ok: body.ok !== false, receipt: body });
if (body.ok === false || result.isError) {
return { content: [{ type: "text", text: JSON.stringify(fail(computer, body.error?.code ?? "step_failed", body.error?.message ?? "step failed", { tool: "run_actions", switched, stopped_at: i, steps: results })) }], isError: true };
}
}
return { content: [{ type: "text", text: JSON.stringify(receipt(computer, { ok: true, tool: "run_actions", switched, steps: results })) }] };
}
if (name === "find_elements") {
const st = args.state_id ? appStates.get(args.state_id) : null;
if (args.state_id && !st) throw new ServerError("unknown_state", `state_id "${args.state_id}" is unknown or expired — call get_app_state again`);
if (st) {
if (st.computerId && st.computerId !== computer.id) {
throw new ServerError("state_wrong_computer", `state_id "${args.state_id}" belongs to computer "${st.computerId}", not "${computer.id}"`);
}
const filtered = filterElements(st.elements, {
detail: "summary", query: args.query, role: args.role,View on GitHub (pinned to 73e0f67d83)