gitbutlerapp/gitbutler · error · anyhow::Error

Tool '{name}' not found

Error message

Tool '{name}' not found

What it means

WorkspaceToolset::call_tool_inner looked up the requested tool name in its BTreeMap of registered tools and found nothing. The toolset only contains tools explicitly registered via register_tool, so the caller (usually an LLM tool-call loop or SDK consumer) used a name that was never registered on this instance.

Source

Thrown at crates/but-tools/src/tool.rs:41

}

impl<'a> WorkspaceToolset<'a> {
    pub fn new(ctx: &'a mut Context) -> Self {
        WorkspaceToolset {
            ctx,
            tools: BTreeMap::new(),
            commit_mapping: HashMap::new(),
        }
    }

    fn call_tool_inner(
        &mut self,
        name: &str,
        parameters: &str,
    ) -> anyhow::Result<serde_json::Value> {
        let tool = self
            .get(name)
            .ok_or_else(|| anyhow::anyhow!("Tool '{name}' not found"))?;
        let params: serde_json::Value = serde_json::from_str(parameters)
            .map_err(|e| anyhow::anyhow!("Failed to parse parameters: {e}"))?;
        tool.call(params, self.ctx, &mut self.commit_mapping)
    }
}

impl Toolset for WorkspaceToolset<'_> {
    fn register_tool<T: Tool>(&mut self, tool: T) {
        self.tools.insert(tool.name(), Arc::new(tool));
    }

    fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
        self.tools.get(name).cloned()
    }

    fn list(&self) -> Vec<Arc<dyn Tool>> {
        self.tools.values().cloned().collect()
    }

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Call toolset.list() and use only returned tool names when issuing calls
  2. Register the missing tool on the WorkspaceToolset before calling it
  3. Check for renames between versions of but-tools if the name worked before

Example fix

// before
toolset.call_tool("list_branches", "{}");
// after — discover registered names first
let names: Vec<String> = toolset.list().iter().map(|t| t.name()).collect();
assert!(names.contains(&"list_branches".to_string()), "available: {names:?}");
toolset.call_tool("list_branches", "{}");
Defensive patterns

Strategy: validation

Validate before calling

let available: std::collections::HashSet<String> =
    toolset.list().iter().map(|t| t.name()).collect();
if !available.contains(requested_name) {
    return Err(anyhow!("tool '{requested_name}' not registered; available: {available:?}"));
}

Type guard

fn is_registered(toolset: &WorkspaceToolset<'_>, name: &str) -> bool {
    toolset.get(name).is_some()
}

Try / catch

match toolset.call_tool_inner(name, params) {
    Ok(v) => v,
    Err(e) if e.to_string().contains("Tool '") && e.to_string().contains("not found") => {
        // restrict LLM tool list to toolset.list() output and re-prompt
        json!({"error": e.to_string(), "available": toolset.list().iter().map(|t| t.name()).collect::<Vec<_>>()})
    }
    Err(e) => return json!({"error": e.to_string()}),
}

Prevention

When it happens

Trigger: Calling toolset.call_tool("get_workspace", ...) on a WorkspaceToolset built without registering that tool; an LLM hallucinating a tool name not in the registered set; version skew where a tool was renamed or not yet registered.

Common situations: Agent/LLM integration invokes a tool from a different toolset version; a consumer copies a tool name from docs but forgets to call register_tool for it first.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/22f72cd50d9b8ccd. Report an issue: GitHub.