Authorizing MCP tool calls at the gateway or inside the proxy, and what each one stops

EEmre BaranAugust 15, 20269 min read
Authorizing MCP tool calls at the gateway or inside the proxy, and what each one stops

The industry has largely settled where an authorization decision gets made and how it gets asked. The AuthZEN Authorization API reached final specification in January 2026 and standardizes the call between the component that enforces a decision and the component that makes it, on a subject, action, resource and context model that defaults to closed. What it deliberately does not settle is where the enforcement point sits, or how the decision point gets the facts it needs.

For AI agents that omission is load bearing. An agent is a non-deterministic process acting with a user's implicitly delegated access, which is the precise setup that produces confused deputy failures, now running at machine speed. The interesting question is not only whether an agent may call a tool. It is where the check happens, because placement decides what the check is physically able to stop.

Two proxies in front of the same MCP server make the point. One sits in the request path and delegates the decision outward. The other runs a hook inside itself and can change the request before it leaves. Both enforce the same rules. They do not enforce them the same way, and the difference is not cosmetic.

01-two-enforcement-placements.png

The standards have started to describe this check

The clearest signal is COAZ, a pair of Standards Track drafts dated 13 February 2026, drafted in the OpenID AuthZEN working group where Cerbos is a contributor. COAZ stands for compatible with OpenID AuthZEN, and the framework is protocol neutral. It takes the information model of any protocol, an HTTP request, a JSON-RPC message, a described API route, and projects it into an AuthZEN request through a declarative mapping. The MCP binding is the reference instantiation.

One detail in the framework is worth stating plainly. It designates CEL as the default expression language for those mappings, leaving a binding free to name a different one. The mapping the gateway integration below uses to turn an incoming request into a Cerbos check is written in that same language. That is alignment on the shape of the problem rather than a conformance claim, since neither integration here has been verified against the binding.

That binding exists for a specific reason. OAuth answers whether a token holder may reach an endpoint. It cannot say which records a call may touch under dynamic policy, and when the token is issued to the agent it has no standard way to carry which human the agent is acting for. COAZ-MCP puts the human user in the subject and the agent identity in the context, and maps a tool call so the tool name and the invocation arguments both reach the decision point. Whether this agent, acting for this user, may call this tool with these arguments is now something a specification describes rather than something every team invents privately.

The binding also names two conformant deployment shapes. A gateway can act as the enforcement point, reading a tool's declared mapping out of the listing response it already proxies. Or the server can enforce its own mapping and call the decision point itself. That is the same choice the two proxies below make, arriving as a conformance question rather than a design preference. Cerbos engineers contribute to that working group, which is worth saying plainly rather than leaving implied.

How in-path MCP gateway enforcement stops a tool call

agentgateway sits in the request path. One route forwards OpenAI compatible traffic to the upstream provider, another proxies the MCP server. Each route carries an external authorization policy pointed at Cerbos Synapse over gRPC, configured to include the request body so the decision reads the model name and the JSON-RPC payload rather than only the path and headers.

Synapse implements the Envoy external authorization API natively, so nothing sits between the gateway and the policy engine. A CEL mapping turns the incoming check into a Cerbos check. Path and method select the resource kind, and for a tool call the upstream server name becomes the resource, the local tool name becomes the action, and the arguments object rides through as a resource attribute. The integration is configuration, in the same way an Envoy edge integration is.

This placement gives a hard stop with no logic in the enforcement point. A denial ends the request at the gateway with a 403, so the MCP server never receives the call, and on the model route the check runs before the forward so the upstream provider is never billed for a completion the policy refused. What it does not give you is any ability to change the request. The gate is binary. It can say no. It cannot say yes to part of it.

02-in-path-check-vs-pre-call-hook.png

Hook enforcement can change the request, which is a different power

LiteLLM occupies a different position. It is the proxy the application already calls, and it exposes a pre-call hook that runs after authentication and before the upstream call. Because the check runs inside the proxy rather than beside it, it can rewrite the request instead of only accepting or rejecting it.

That produces three decision points rather than one. The model listing returns only the catalogue the caller can use. On a chat completion the model gate is a hard denial, while each declared tool is checked as its own entry in one batch call and denied tools are removed from the request. On a tool invocation the name and arguments are read from the call before it is forwarded.

No protocol mapping is needed on this path. Synapse exposes the standard Cerbos API on the same port it serves everything else, so the hook posts a check and reads the decisions back. If the policy engine cannot be reached the hook fails the call rather than passing it through. CoSAI's agentic identity guidance makes that normative for high capability agents, requiring enforcement gateways to be configured fail closed with a clearly defined safe degradation path, and giving reverting to human in the loop or halting agent execution as the examples. Failing the call is only the first half of that. The degradation path is a decision the deploying team still has to make.

Denying a call and hiding a tool are different security properties

The two shapes make a distinction that is easy to blur, and blurring it is how a gateway ends up with a false sense of coverage. Returning 403 on a tool call stops an action. The agent tried, the attempt failed, and it is in the decision log. Removing a tool from the request before the model sees it means the attempt never happens at all.

Both are useful and neither substitutes for the other. Exposure filtering shapes what the model will consider. A model that never sees a refund tool in its list will not plan around it and will not spend turns retrying a call that was never going to land. A completion carrying one allowed and one denied tool returns a normal response with the denied tool stripped, which is more useful than rejecting a request whose remaining half is legitimate.

Shaping the model's inputs is still shaping the model, and the model is not an enforcement point. OWASP's AI verification standard states this directly, that access control decisions must be enforced by application logic or a policy engine and never by the model itself, and that the decision point must be isolated from the agent's execution environment. COAZ-MCP encodes the same instinct structurally, treating everything a server or client declares, other than the verified subject identity, as untrusted input that policy must not grant privilege on. The tool list in a completion request is client controlled. An agent that assembles its own list, or reaches the MCP server directly, is stopped only by the per call check.

The filter therefore narrows what gets attempted, and the per call decision is the thing that holds. Silent stripping carries an observability cost too, because the caller gets a success response without necessarily knowing a tool was removed, so the drop needs logging at the proxy as well as deciding in policy.

03-deny-versus-hide.png

What this changes about the architecture

Per model gating, per tool exposure and per argument limits all end up at the proxy, so every agent behind it inherits the same rules without shipping any authorization code. Swapping one framework for another, or adding a second agent next month, does not mean reimplementing the constraints or trusting the new implementation got them right.

It also collapses two planes into one vocabulary. The model resource policy and the derived roles are identical across both integrations, and the tool rules are the same rules under a different resource kind because one proxy namespaces tools by upstream server and the other does not. Decisions across both planes land in one audit trail with one shape, which matters when the question asked afterwards is what an agent identity was permitted to do rather than what it authenticated as.

Neither enforcement point holds any policy logic, which is what serious reference architectures design for. Because the enforcement point is a proxy rather than the application, this is the same externalized authorization pattern already running for HTTP traffic. Cerbos Synapse handles the translation from whatever component speaks to it, and the rules for MCP sit next to the rules for everything else.

04-one-policy-set-several-enforcement-points.png

Where both shapes stop working

A proxy only authorizes what crosses it. The fifth design principle in that same CoSAI paper is blunt about the consequence, requiring that authentication and authorization be enforced at each hop in an agentic chain and at the final enforcement point, the tool, the API, the data system, rather than only at the initial gateway, precisely to prevent confused deputy and privilege escalation. An agent with direct network egress sits outside both shapes here. Egress control is the other half, and the kill switch question tests whether you have it.

Argument level rules also depend on the tool schema being stable. Rename a field and the rule no longer matches what it was written against. Denying on a missing attribute turns schema drift into a denial rather than a silent allow, but a denial is still an outage, so tool schemas need versioning and review with the same care as a public API.

Neither proxy filters the tool listing per principal. One delegates authorization per JSON-RPC method without rewriting the listing response, and the other expects permission objects held in a database. The catalogue leaks tool names rather than privileges, because calling a listed tool the policy denies still fails. COAZ-MCP is interesting here for a different reason, since it uses that same listing to carry a tool's declared mapping to whichever gateway is enforcing. Filtering the listing per caller remains a separate problem that somebody still has to solve.

Reading the request body has a cost too. The gateway buffers a bounded portion before calling out, so the decision is only as complete as what fits inside that limit. Fine for tool calls, worth checking before pointing the same pattern at something that streams.

The placement is the decision

Where the check sits decides what it can do. In the path, it can stop a call and nothing more, which is exactly what you want at a boundary and exactly why it should hold no logic. Inside the proxy, it can also change what the model is offered, which is worth having and is not a substitute for the stop. Most teams will end up with both, and the discipline worth building is knowing which one any given rule depends on. The same policies reach the coding ag ents and the outbound model traffic alongside them.

Try Cerbos to see how this works in practice, or book a call to talk through your architecture with the team.

Go deeper:

FAQ

Tagged in

Free policy workshop

Get your first Cerbos policy written by our team.

Book a session to talk through your requirements and walk away with a working policy.

Book a session