Skip to main content
A listener in return mode files a review and denies at once with its id. The agent resends the identical statement after a person approves it, so it needs a way to learn the decision. 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 the 403. 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 answers GET /.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.
The path needs no credential, the same as the listener port. A caller who reaches the listener and holds a review id reads that review’s status, listener name and approval rule. It never reads the statement.
Other protocols have no path to reserve, so they read the status over MCP.

The MCP server

The mcp block starts a Model Context Protocol server with three read-only tools. It serves every listener of the Sidecar, whatever the protocol.
config.yaml
  • listen is 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.
Add the block to the Sidecar’s config file, beside 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.
The MCP endpoint has no authentication, the same as the listener ports. A caller reads every review of this Sidecar: its id, status, listener name and approval rule, never the statement. Bind it where only the agent reaches it: loopback when the agent runs on the same host, a ClusterIP Service on Kubernetes, never a public load balancer. The server refuses cross-origin browser requests, so a web page cannot use it from a visitor’s browser.

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 to next 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.
For one developer:
For a team, commit .mcp.json at the repository root. claude mcp add --scope project writes it:
.mcp.json
An agent that uses two Sidecars needs two entries, for example 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

Set listen: 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
The agent’s URL is 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.