Skip to content

Tool approval

When an agent wants to call an external MCP tool that has no recorded decision, Axocoatl pauses the call and asks a human. This is the built-in MCP approval gate, and it is wired in globally at daemon startup — every MCP tool call passes through it unless a saved permission already says allow or deny.

  1. A tool dispatch checks the permission store for a recorded decision.
  2. If none exists, the approval gate parks the call, mints an approval_id, and fires a notifier — in the running server that’s an mcp-approval-required WebSocket frame, so the dashboard pops a modal.
  3. The tool call blocks, waiting on the human.
  4. The human clicks Allow or Deny in the modal. That resolves the approval over the WebSocket, and the tool dispatch resumes (running the tool, or returning a denial).

The approval prompt carries enough context to decide: the agent id, the server, the tool name, and a truncated preview of the call’s arguments.

When resolving, the human also picks how widely the decision applies:

  • Once — applies to this single call only; nothing is saved.
  • This agent, this tool — save the exact {agent, server, tool} decision.
  • This agent, this server — save it for any tool from this server, but only when called by this agent.
  • Any agent, this server — the most permissive scope.

A saved decision means future matching calls skip the prompt and apply the recorded allow/deny automatically.

A parked approval waits up to 5 minutes. Long enough for a human to step away and come back; short enough that a forgotten prompt doesn’t pin daemon resources forever. On timeout the decision defaults to deny (a soft deny, not persisted), and the pending entry is cleaned up. A dropped/closed connection resolves the same way — deny.

The prompt arrives as an mcp-approval-required frame on the dashboard’s WebSocket connection, which renders the approval modal. The dashboard also exposes a count of pending approvals for a “waiting” badge. The human’s allow/deny (with its persistence scope) is sent back over the same WebSocket to resolve the specific approval_id.

The MCP approval gate above is the only hook that runs today. A user-defined hooks: section in your config is experimental and not yet active — it is parsed, and a non-empty hooks: block logs a warning at daemon startup, but it does not execute. Don’t rely on custom hooks to gate tools yet; the built-in approval gate is the live mechanism.

  • Example: examples/mcp-bridge — calling an external MCP tool over stdio (cargo run -p mcp-bridge).
  • See also: Examples gallery for the tool-hooks example, which demonstrates pre/post tool hooks at the library level.