ctrlrun gateway between them
and every tools/call is decided by a policy, bound to an approval where the policy says so,
reserved by effect key, relayed and recorded. The agent’s configuration changes one URL. The
server does not change at all.
pip install "ctrlrun[gateway]"; the server reachable over HTTP; a
directory for ctrlrun.yaml and the store. The gateway speaks MCP revision 2026-07-28 and
accepts 2025-11-25, 2025-06-18 and 2025-03-26.
1
Name the tools
Every tool you want decided gets an entry
mcp.<alias>.<tool>. A write gets an effect:
template over the tool’s own parameter names; a tool not listed is denied.runnable
2
Start it, and repoint the agent
http://127.0.0.1:8900/mcp. That is the change.3
What the agent sees
A refusal is a JSON-RPC error with a code the client can read, never a tool result with
A human runs
isError, because a tool result reaches the model as text and invites the retry the
refusal exists to prevent.ctrlrun approve apr_…, or answers in Slack through the webhook, and the
agent’s next identical call runs. A different call, a different namespace, a different
cluster, does not: the approval is bound to the hash of what the human saw.A relayed result carries
_meta["com.ctrlrun/receipt"] with the receipt id.4
Choose the principal, and read the note
The gateway never reads a principal off the request body, and
--principal-from-client-info
was removed by name: a client’s own claim about who it is was a way for the agent to pick
its authority. A tools/call with no principal is refused with -41007.What you get, in your terms
- No code changes.
- Any language.
- Approvals bound to the exact tool call.
- Every call leaves a receipt.
- Measure first:
mode: observeat the top of the same file.
If it didn’t work
-32020, header mismatch: the MCP header and the body disagree; the gateway routes on the body and refuses rather than guessing.unsupported protocol version: the client declared a revision outside the accepted set.- The gateway exits 2 at start: a non-loopback
--listenwithout--allow-remote, or a policy that does not load.
Next
- Put the gateway in front of MCP: the longer guide, with the lost-reply case.
- CTRLRun and MCP · Get started · Why.