GET /api/sidecars/reviews/<id> on the Control Plane answers, but it takes the Sidecar token, which an agent must not hold. The Sidecar gives the agent three ways that need no credential:
The status path and the MCP server need a Control Plane, because the review status lives there. None of the three returns the statement, and none approves anything.
Find the review in the denial
On http and grpc, a review denial carries the review in structured fields, so a client does not parse the message. http sends them as headers on the403. grpc sends the same keys, in lowercase, as trailing metadata beside PERMISSION_DENIED.
The body stays the text message, so a client that knows nothing about reviews still reads it. kubectl prints it as
Error from server (Forbidden): ....
On postgres, mysql, mssql and mongodb, the review id is in the error text only:
Read the status on an http listener
An http listener answersGET /.well-known/hoop/reviews/<id> itself, for a review filed on that listener. The agent asks the same host and port it sends its requests to:
next says what the agent does, so it never interprets a status on its own. See Status and next. rejection_reason is present on a rejection that gave one. expires_at is present only when the approval rule sets a time limit.
An error body is
{"message": "..."}. Every answer sends Cache-Control: no-store and closes the connection.
The whole /.well-known/hoop/ prefix belongs to the Sidecar. Every http listener keeps it, with or without a Control Plane, and never forwards it to the upstream. A route the upstream serves under that prefix is not reachable through the Sidecar.
The Sidecar answers it only as the first request on a connection. Behind another request, the listener closes the connection unanswered, because HTTP/1.1 pairs responses with requests by order. curl and Go clients resend on a new connection.
Other protocols have no path to reserve, so they read the status over MCP.
The MCP server
Themcp block starts a Model Context Protocol server with three read-only tools. It serves every listener of the Sidecar, whatever the protocol.
config.yaml
listenis required. Remove the block to turn the server off.- The Sidecar needs a Control Plane. A config with the block and no Control Plane is refused at startup.
- A reload does not start, stop or move the server. Restart the Sidecar after you add, remove or change the block.
- A bind failure stops the Sidecar, as a listener’s bind failure does.
listeners, then restart the Sidecar. On a Sidecar connected to a Control Plane, the file applies on the first import, or when the Sidecar runs from its config file.
Tools
review_status and review_wait return one review and what to do next. review_list returns {"reviews": [...]} of the same shape, and status filters on PENDING, APPROVED, REJECTED, REVOKED, EXECUTED or EXPIRED.
instruction is next in words, for an agent that reads prose. timed_out and waited_seconds come from review_wait only. timed_out: true is not an error: call again.
Keep the 60-second default for review_wait. Some MCP clients drop a tool call that blocks for longer than 60 to 120 seconds. review_wait sends a progress notification every 2 seconds to a client that asks for them.
When the config has an mcp block, a return-mode denial leads with the review id and names the tool:
Status and next
The status path and the MCP tools map a status tonext the same way:
The resend runs through the listener like any statement, so the analyzer, the audit trail and masking apply, and the resend uses the approval. See The retry contract.
Two tool answers are errors:
Connect an agent
Add one MCP entry per Sidecar the agent uses, next to the connection string it pairs with. One entry covers every listener and every replica of that Sidecar: the server keeps no session, so it can sit behind a load balancer.- Claude Code
- Cursor
- Other clients
For one developer:For a team, commit
.mcp.json at the repository root. claude mcp add --scope project writes it:.mcp.json
hoop-reviews-payments and hoop-reviews-ledger. The denial does not name the Sidecar, so name each entry after the connection string it pairs with, and tell the agent to call the entry of the Sidecar that denied.
On Kubernetes
Setlisten: 0.0.0.0:8765 in the config file, so the Service reaches the pod. Then publish the port with one laneServices entry in the chart values:
values.yaml
http://<fullname>-mcp.<namespace>.svc:8765/mcp. <fullname> is <release>-hoopsidecar, or the release name alone when it already contains hoopsidecar. kubectl get svc shows it.
Every pod in the cluster can reach a ClusterIP Service. Add a NetworkPolicy that admits only the agent’s pods.
Next
Risk Analysis
Holding a statement for a person, return mode, and the retry contract.
Config File
Every other section of the config, and what startup refuses.