←All concepts

learntutorialintro5 minsince 2.1.0

approvalhuman-in-the-loopenforcementaudit

Require human approval for MCP tool calls

Updated October 13, 2026

The problem

Some tool calls should not run on a model’s word alone: a refund, a deletion, a message sent on someone’s behalf. Asking in the chat is not a control, because the model that wants the call is the one asking. The hold has to sit between the agent and the server, where the agent cannot answer for the human.

The deny

With echo on the approval list, a call to it is held. While it waits, it is listed over the REST API (mcp-hangar 2.27.2):

$ curl -s localhost:8000/api/approvals
[{"approval_id": "7f800dc8-...", "provider_id": "demo", "tool_name": "echo",
  "arguments": {"text": "approve-me"}, "state": "pending",
  "expires_at": "2026-10-13T20:48:48+00:00", "expires_in_seconds": 119, ...}]

Approved, the held call runs and the agent gets its result:

$ curl -s -X POST localhost:8000/api/approvals/7f800dc8-.../resolve \
    -d '{"decision": "approve", "reason": "refund checked"}'
{"approval_id": "7f800dc8-...", "state": "approved"}

"success": true, "result": {"content": [{"text": "approve-me", "type": "text"}]}

Denied, the call never reaches the server, and the agent is told why:

$ curl -s -X POST localhost:8000/api/approvals/5142217c-.../resolve \
    -d '{"decision": "deny", "reason": "not this account"}'
{"approval_id": "5142217c-...", "state": "denied"}

"success": false, "error": "not this account", "error_type": "approval_denied"

Captured on a laptop with the demo echo server; approval ids are shortened and the agent’s side is the matching hangar_call result.

The config that does it

mcp_servers:
  payments:
    mode: remote
    endpoint: https://payments.example.com/mcp
    tools:
      approval_list: ["refund_*", "issue_credit"]
      approval_timeout_seconds: 600
approvals:
  enabled: true
  channel: event_stream      # or an installed adapter, such as slack

A tool on the list stays visible and is held on every call; one that matches no list runs straight through. A denial or an expiry refuses the call: the gate fails closed. Resolving needs the approval:resolve permission.

What it does not do

  • The approver sees a redacted copy of the arguments. At dispatch, Hangar re-checks the approval, its expiry and a hash of the raw arguments, so a call cannot be swapped after approval, but the person did not see secrets the redaction removed.
  • Approved is not the same as done. An approval that arrives after the caller’s batch deadline is recorded cancelled and the call does not run.
  • It says nothing about who should approve. Which people hold approval:resolve, and how they are notified, is yours to set.
  • deny_list wins. A tool on both lists is hidden, not held.