Governing AI Agent MCP Tool Calls with OPA/Rego
OPA Rego permissions for AI agent MCP tool calls: how to interpose a policy-as-code decision before every call, based on role, data sensitivity, and time.
An MCP (Model Context Protocol) server gives an agent access to real tools: reading files, calling internal APIs, querying databases. That is exactly what makes the agent useful, and exactly what makes access control non-negotiable. The MCP specification describes how a tool is exposed and invoked; it does not describe who is allowed to call it, in what context, or with what data. That responsibility falls entirely on whoever deploys the server.
Open Policy Agent (OPA) and its Rego language address that gap with a simple principle: authorization policy becomes code, versioned and tested, separate from the business logic. Applied to MCP tool calls, this lets you ask a precise question before every execution: does this role, on this data, at this time, have the right to call this tool?
The problem: real tools without contextual authorization
Most MCP integrations shipped with default configuration apply a binary control: the server is reachable or it is not, the tool is registered or it is not. Once the connection is established, the agent can call any exposed tool, on any resource, at any time. There is no native concept of role, of the sensitivity of the data being touched, or of an allowed time window.
The concrete risk: a conversational agent equipped with a generic query_database tool can, in theory, run a query against a table holding HR data or application secrets, simply because the tool exists and nothing limits its use based on who is asking. Application-level sandboxing (isolating executed code, restricting available system calls) protects against arbitrary code execution, but it does not answer the business question: is this specific call, in this specific context, legitimate?
The enforcement point: interposing a decision before execution
The answer fits in one sentence: every MCP tool call must pass through a decision point before it reaches the tool itself. Concretely, the MCP server (or a proxy sitting in front of it) queries an OPA engine with the call's context, and only executes the tool if the response is allow: true.
Before (no enforcement point):
agent -> MCP server -> "query_database" tool -> direct executionAfter (OPA interposed):
agent -> MCP server -> OPA decision request (role, tool, sensitivity, time)
-> allow == true -> tool execution
-> allow == false -> denial + loggingWhat matters is that this enforcement point cannot be bypassed: it is not an optional check on the agent side (which the agent could sidestep by rephrasing its request), but a barrier on the server side, between receiving the call and actually executing it.
Writing the Rego policy
A Rego policy for this use case takes as input an input object describing the requested tool, the role of the user or the agent's execution context, the sensitivity of the targeted resource, and the time of the call:
# Example input evaluated by OPA on every tool call
input:
tool: "query_database"
user:
role: "analyst"
resource:
sensitivity: "internal"
timestamp: "2026-07-17T14:32:00Z"The policy itself, in Rego:
package mcp.authz
import future.keywords.in
default allow := false
# An analyst can read a file, unless the resource is classified as critical.
allow if {
input.tool == "read_file"
input.user.role in {"analyst", "admin"}
input.resource.sensitivity != "critical"
}
# An admin can query the database, only during business hours.
allow if {
input.tool == "query_database"
input.user.role == "admin"
business_hours
}
business_hours if {
hour := time.clock(time.now_ns())[0]
hour >= 7
hour < 20
}Each allow rule explicitly combines the requested tool (input.tool), the role, and a contextual condition (sensitivity or time window). Nothing is authorized by default: what matters is the deliberate construction of the rule that grants access.
Deny-by-default, decision logging, testing with opa test
The default allow := false line is not a stylistic detail: it is the property that guarantees any newly exposed tool, any role combination nobody anticipated, results in a denial rather than a silent grant. It is the opposite of a blocklist, which always misses a case.
Every decision, positive or negative, must be logged with its full context (tool, role, resource, outcome, reason) to allow after-the-fact audit. A policy with no traceability of its own decisions is as risky as no policy at all: nobody can verify what was actually authorized.
Finally, a Rego policy is tested like application code, with opa test:
package mcp.authz_test
import data.mcp.authz.allow
test_analyst_can_read_internal_file if {
allow with input as {
"tool": "read_file",
"user": {"role": "analyst"},
"resource": {"sensitivity": "internal"}
}
}
test_analyst_cannot_read_critical_file if {
not allow with input as {
"tool": "read_file",
"user": {"role": "analyst"},
"resource": {"sensitivity": "critical"}
}
}opa test policies/ -vThis test cycle should run in continuous integration, exactly like any unit test: a policy change that breaks a covered case must be caught before deployment, not after an incident.
Complementary to application sandboxing
OPA does not replace application-level sandboxing, it complements it, and the two barriers answer different questions. Sandboxing (execution isolation, restricting the system and network calls available to executed code) answers: what can this code technically do, no matter what? An OPA policy answers a different question: is this specific action, for this specific role, on this specific data, authorized right now?
Confusing the two creates either a useless duplicate barrier or, worse, a false sense of security: a well-designed sandbox has no business knowing the business sensitivity of a piece of data, and a high-level policy has no business reimplementing execution isolation. Each layer stays in its lane, and that separation is what makes the whole thing auditable rather than redundant.
A ready-to-use policy pack
Writing these policies from scratch for every MCP scenario (file reads, database queries, outbound API calls, write actions) is repetitive work that few teams have time to do properly, along with the tests and logging that go with it. That is exactly what we ship in the OPA/Rego policy pack for MCP agents: contextual control rules (role, sensitivity, time, tool) already written, tested with opa test, and ready to sit in front of your MCP servers.
Related articles
Google observes clusters attacked within 18 minutes and escapes via privileged pods. Here is runtime detection with Falco and admission guardrails with OPA.
How to red-team an MCP server against indirect prompt injection: verify a poisoned document cannot reach a tool call, file access, or command execution.
Policy as code with OPA Rego and Kubernetes lets you deny a privileged pod before it ever starts. OPA/Rego vs Kyverno, real examples, testing, and NIS2/DORA proof.
We detonated a live Phobos sample. Here is what it does, deleting shadow copies, killing the firewall, and the Sigma rule that catches it, validated across multiple samples with zero false positives.