Integration
AIControl integrates as a gateway. Your agent's MCP client points at
/mcp/{server_id}/... instead of the real downstream MCP server. AIControl
evaluates Cedar policy on every tools/list and tools/call, audits the
result, and forwards allowed calls to the real server.
The integration pattern
Admin registers the downstream server once
↓
POST /mcp-servers → {"id": "<server_id>", "status": "pending_review"}
↓
PATCH /mcp-servers/<server_id> {"status": "active"}
↓
Agent's MCP client points at /mcp/<server_id>/
↓
Agent calls tools/list or tools/call
↓
AIControl evaluates Cedar policy, writes the audit event
↓
allow → forwarded to the real downstream server, response returned
deny → JSON-RPC error result returned, never forwarded
review → a pending review is created and Slack is notified (if configured);
an isError result carrying the review id is returned immediately —
your agent does not waitAn agent in observe mode (the default for newly registered agents) gets the
same audit trail, but no call is blocked: deny and review decisions are recorded
with enforced = false and the call is forwarded. Switch the agent to govern
(PUT /agents/{agent_id} with {"governance_mode": "govern"}) to enforce.
Every call — regardless of decision — is recorded in the audit log before the response is returned.
Registering a downstream server
Registration is admin-only and metadata-only — no credential is stored on the
row. Whatever Authorization header your agent sends on the gateway call is
forwarded to the downstream server unchanged (pure passthrough).
curl -X POST http://your-aicontrol-host:8001/mcp-servers \
-H "Authorization: Bearer $AICONTROL_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "invoicing-mcp", "base_url": "https://invoicing.internal/mcp", "approved_tools": ["get_invoice", "create_invoice"]}'Response 201:
{
"id": "3f2a1c8b-...",
"name": "invoicing-mcp",
"base_url": "https://invoicing.internal/mcp",
"status": "pending_review",
"approved_tools": ["get_invoice", "create_invoice"],
"created_at": "2026-09-09T10:00:00Z"
}New registrations start in status: "pending_review" and are not reachable via
the gateway until activated:
curl -X PATCH http://your-aicontrol-host:8001/mcp-servers/<server_id> \
-H "Authorization: Bearer $AICONTROL_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "active"}'status is one of pending_review, active, or blocked. The gateway
rejects any call to a server that isn't active with 403. The dashboard's
MCP Servers page does the same (Approve / Block).
approved_tools on the server registration (separate from the per-agent
approved_tools set via PATCH /agents/{agent_id}/approved-tools) scopes
which of the downstream server's tools are visible/callable through this
gateway at all — an empty list means every tool the downstream server exposes
is reachable, subject to Cedar policy and the agent's own approved-tools list.
Environment setup
AICONTROL_URL=http://your-aicontrol-host:8001 # required
AICONTROL_TOKEN=eyJhbGc... # required — agent-scoped JWTGateway calls require an agent-scoped token — a token issued with
agent_id set (via scripts/public/onboard_agent.py, the dashboard's Register
agent dialog, scripts/public/issue_token.py --agent-id, or POST /tokens with
agent_id). An admin token, or an agent-role token issued without an
agent_id, is rejected with 403 — the gateway derives caller identity
entirely from the token, so it must be attributable to exactly one registered
agent.
Pointing your MCP client at the gateway
Configure your agent's MCP client with:
- Base URL:
http://your-aicontrol-host:8001/mcp/<server_id> - Auth header:
Authorization: Bearer <agent-scoped-token>
The gateway exposes exactly two routes under that base URL, both POST with a
JSON-RPC body: /tools/list and /call_tool. call_tool requires
session_id and sequence_number in params (see below), which MCP clients
do not send on their own, so the client, or a thin wrapper around it, has to
add them. The worked example in the next sections is the complete wire format.
The gateway forwards your Authorization header to the downstream server
unchanged, and that header is also what authenticates your agent to AIControl.
The downstream server therefore receives the AIControl agent token; there is no
separate field for a downstream credential.
The tools/list request
POST /mcp/{server_id}/tools/list
Authorization: Bearer <agent-scoped-token>
Content-Type: application/json{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}Response: the downstream server's own tools/list response, filtered to the
server registration's approved_tools if that list is non-empty.
If the downstream server is unreachable, AIControl returns a JSON-RPC error rather than forwarding a timeout or connection error upstream to your agent:
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": "Downstream MCP server unreachable"}}The same -32000 error is returned when the downstream server accepts the connection and then drops it. If the downstream server answers with an HTTP error status, AIControl returns -32001 (Downstream MCP server returned HTTP <status>) with data.status_code and the first 500 characters of the body in data.detail. If it responds with a body that is not valid JSON (for example a proxy's HTML error page), AIControl returns -32002 ("Downstream MCP server returned an invalid response") instead.
The call_tool request
POST /mcp/{server_id}/call_tool
Authorization: Bearer <agent-scoped-token>
Content-Type: application/json{
"jsonrpc": "2.0",
"id": 1,
"params": {
"name": "query_credit_bureau",
"arguments": {
"applicant_id": "A-10042",
"report_type": "full"
},
"session_id": "b3e1f9a2-...",
"sequence_number": 3,
"workflow": "loan_application_review",
"input_tokens": 812,
"output_tokens": 143,
"cost_usd": 0.0091
}
}Field (under params) | Type | Required | Notes |
|---|---|---|---|
name | string | yes | The tool being called — matched against policies |
arguments | object | yes | Full tool arguments; stored in the audit record |
session_id | UUID | yes | Groups all tool calls within one agent run. Missing or invalid returns a -32602 JSON-RPC error |
sequence_number | integer | yes | Call order within the session. Missing or invalid returns a -32602 JSON-RPC error |
workflow | string | no | Business-process workflow name. Defaults to "unassigned" if omitted |
input_tokens / output_tokens / cost_usd | number | no | Feed token_budget policy conditions — omit them and any such policy never fires for this call, since there's nothing to sum |
Generate session_id once per agent run, not per tool call — reuse it across
every call in that run so calls group correctly in the audit log and so
session-scoped rate-limit/budget policies accumulate correctly.
The call_tool response
Allowed, forwarded successfully — the downstream server's own
tools/call response is returned unchanged:
{
"jsonrpc": "2.0",
"id": 1,
"result": {"content": [{"type": "text", "text": "..."}]}
}Denied by a server-level or agent-level approved-tools gate, a Cedar policy, or a response
scan — a JSON-RPC result with isError: true, not an HTTP error:
{
"jsonrpc": "2.0",
"id": 1,
"result": {"content": [{"type": "text", "text": "Denied by policy: policy_matched:block_dangerous_tools"}], "isError": true}
}Routed to human review — same isError: true shape, with a message
naming the pending review and the review id in _meta.review_id. Poll
GET /reviews/{review_id} with the same agent token for its status:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{"type": "text", "text": "Pending human review: policy_matched:require_review_for_external_calls"}],
"isError": true,
"_meta": {"review_id": "f1e2d3c4-..."}
}
}Malformed request (session_id or sequence_number missing/invalid) — a
JSON-RPC protocol-level error, not a result:
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32602, "message": "Invalid params: session_id is required"}}Downstream server unreachable — same fail-closed error as tools/list:
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": "Downstream MCP server unreachable"}}A dropped connection returns the same -32000 error; a downstream HTTP error status returns -32001; a non-JSON downstream body returns -32002. Each is recorded as an audit event. If the downstream server returns a JSON-RPC error object, it is relayed unchanged and the audit event has decision: error and reason upstream_jsonrpc_error:<code>.
A call to a server that is not registered or not active returns 404 / 403 and is also audited (reason malformed_request:server_unavailable:...).
Malformed params — params.name missing, params.arguments present but not a JSON object, or params itself not an object, returns a -32602 error and is audited. Arguments must be an object because policy conditions are evaluated against them.
Response scanning
After a call is forwarded and the downstream server responds, AIControl scans
the response before returning it to your agent (app/services/response_scanner.py
— the same deterministic, pattern-based scanner used elsewhere in the product,
not an LLM). An allowed call writes its audit event after the scan, so the event
records the scan outcome: allow with reason response_scan_clean for a clean
response. With the default MCP_RESPONSE_SCAN_POLICY=block, a flagged response is
blocked with isError: true even though the downstream call itself already
happened; the event is decision: deny with decision_reason: response_scan_flagged:<categories>.
With MCP_RESPONSE_SCAN_POLICY=log, the flagged response is delivered to your agent unchanged and the event is recorded with decision: allow and the same response_scan_flagged:<categories> reason, so the flag is visible in the audit log without blocking.
Handling responses in your agent
Check result.isError on every call_tool response. true means the tool
did not execute as your agent expects — treat it the same way you'd treat a
denied tool call from any policy engine: don't retry blindly, and surface the
text content (which names the policy or reason) to whatever is orchestrating
the agent.
Checklist: integration complete
✓ AIControl deployed and GET /health returns "status":"ok"
✓ Downstream server registered (POST /mcp-servers) and status set to "active"
✓ Agent-scoped token issued (scripts/public/onboard_agent.py or POST /tokens with agent_id)
✓ Client pointed at http://your-aicontrol-host:8001/mcp/<server_id> (tools/list, call_tool)
✓ session_id generated once per agent run, reused across tool calls
✓ sequence_number increments with each tool call in the session
✓ Agent switched from observe to govern when you want denials enforced
✓ result.isError checked on every call_tool response
✓ First call verified — audit event visible in dashboardFramework and platform guides
For MCP-client configuration notes per framework (LangChain, CrewAI, LangGraph, and others) and for the four named agent-platform insertion points (Amazon Bedrock, Azure AI Foundry, Vertex AI, Microsoft Copilot Studio), see Platform & Framework Guide and Platform Discovery.