Skip to main content
ctrlrun mcp-operator exposes the operator’s own commands as MCP tools. The person who has to answer an approval asks their assistant what is waiting, reads the action and its arguments, and answers — with no checkout, no shell and no store path. The answer goes through the same two store calls ctrlrun approve and ctrlrun deny make, so there is one approval record and one place its state changes.
Prerequisites: pip install "ctrlrun[gateway]", and a directory holding the ctrlrun.yaml and the store your agents already use. It speaks MCP revision 2026-07-28 and accepts 2025-11-25, 2025-06-18 and 2025-03-26.

Start it

It binds 127.0.0.1:8901 and there is no flag that changes that. Its read tools answer without a credential, so it must not be the process that opens a port to a network; put a proxy in front of it on the same host, terminating authentication there and overwriting both headers on every request. That proxy is what makes the headers worth anything. There is no --principal either. A fixed name would attribute every approval to the same string whoever gave it, and an approver that distinguishes nobody is not attribution. With --identity-jwt the approver comes from a verified bearer token instead, and --identity-jwt-user-claim says which claim names the person.

Point a client at it

In a real deployment the client sends a bearer token and the proxy sets those two headers from whoever it authenticated. Headers a client sets for itself are a demonstration, not a control.

A transcript

Asking what is waiting needs no credential:
Answering does. Without one:
With one:
The agent then runs the refund it was waiting on, and the receipt says who let it:

What it will not do

  • Make an agent act. No tool proposes, executes or resumes an action, and there is no auto-approve, dry-run or development mode.
  • Approve an action other than the one the request names. The grant carries the action_hash stored when the request was created; change an argument and the call is refused.
  • Take an answer from a machine. A credential naming an agent and no person is -41013.
  • Take an expired credential. -41014, distinct from -41007, because refreshing a token and obtaining one are different fixes.
  • Decide whether you were allowed to answer. It authenticates who answered and records it. Any human whose credential the provider verifies can answer any pending request, exactly as any human who can run ctrlrun approve can. That is attribution, not authorization.

If it did not work

  • Exits at start naming --user-header or --identity-jwt-user-claim: a write tool refuses a credential that names no person, so a configuration that could never write is refused early.
  • Exits at start naming loopback: there is no --allow-remote; put a proxy in front instead.
  • -41003 with reason expired: the request timed out. The agent proposes it again.
  • -32020: a mirrored MCP header disagrees with the body. The body is believed and the request is refused rather than guessed at.

Next