---
title: "Authorizing MCP tool calls at the gateway or inside the proxy, and what each one stops"
description: "Two ways to authorize MCP tool calls, in the request path or inside the proxy, and they stop different things. Covers in-path gateway enforcement against a pre-call hook, why denying a tool and hiding a tool are different security properties, fail closed behavior, and where both shapes stop working."
author: "Emre Baran"
date: "2026-08-15T10:57:00.000Z"
canonical: "https://www.cerbos.dev/blog/authorizing-mcp-tool-calls-at-the-gateway-or-inside-the-proxy"
image: "https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/Authorizing_MCP_tool_calls_at_the_gateway_or_inside_the_proxy_and_what_each_one_stops_737652c525.png"
tags: ["guide"]
source: "https://www.cerbos.dev/blog/authorizing-mcp-tool-calls-at-the-gateway-or-inside-the-proxy"
---

# 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](https://openid.net/specs/authorization-api-1_0.html) 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](https://www.cerbos.dev/features-benefits-and-use-cases/dynamic-authorization-for-MCP-servers) 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](https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/01_two_enforcement_placements_5984c6de9a.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](https://openid.github.io/authzen/authzen-coaz-framework-1_0.html) 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](https://openid.github.io/authzen/authzen-coaz-mcp-binding-1_0.html) 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](https://agentgateway.dev) 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](https://docs.cerbos.dev/synapse/latest/extensions/envoy-extension.html) 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](https://www.cerbos.dev/ecosystem/cerbos-envoy) 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](https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/02_in_path_check_vs_pre_call_hook_d3beb064ab.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](https://www.coalitionforsecureai.org/wp-content/uploads/2026/04/agentic-identity-and-access-control.pdf) 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](https://www.cerbos.dev/blog/why-audit-logs-are-important). 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](https://owasp.org/www-project-artificial-intelligence-security-verification-standard-aisvs-docs/) 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](https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/03_deny_versus_hide_38e538042e.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](https://docs.cerbos.dev/cerbos/latest/policies/resource_policies.html) and the [derived roles](https://docs.cerbos.dev/cerbos/latest/policies/derived_roles.html) 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](https://www.cerbos.dev/blog/nhi-security-how-to-manage-non-human-identities-and-ai-agents) 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](https://www.cerbos.dev/blog/why-external-authorization) pattern already running for HTTP traffic. [Cerbos Synapse](https://www.cerbos.dev/product-cerbos-synapse) handles the translation from whatever component speaks to it, and the [rules for MCP](https://www.cerbos.dev/blog/mcp-authorization) sit next to the rules for everything else.

![04-one-policy-set-several-enforcement-points.png](https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/04_one_policy_set_several_enforcement_points_bb3a6883a1.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](https://www.cerbos.dev/blog/the-kill-switch-that-never-got-pressed-what-the-open-ai-hugging-face-incident-tells-us-about-agent-authorization) 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](https://www.cerbos.dev/features-benefits-and-use-cases/agentic-authorization) reach the [coding ag ents](https://www.cerbos.dev/blog/governing-ai-coding-agents-with-cerbos-synapse) and the [outbound model traffic](https://www.cerbos.dev/blog/fine-grained-authorization-for-ai-agents-with-cerbos-and-aperture-by-tailscale) alongside them.

[**Try Cerbos**](https://hub.cerbos.cloud) to see how this works in practice, or [**book a call**](https://www.cerbos.dev/workshop) to talk through your architecture with the team.

**Go deeper:**

- [Zero Trust for AI, securing MCP servers](https://solutions.cerbos.dev/zero-trust-for-ai-securing-mcp-servers) (eBook) for the deployment patterns and policy shapes behind governed MCP access  
- [MCP permissions](https://www.cerbos.dev/blog/mcp-permissions-securing-ai-agent-access-to-tools) for a closer look at what fine grained tool access control involves

## FAQ

### Where should authorization be enforced for MCP tool calls? 

Authorization for MCP tool calls should be enforced at every hop rather than at one gateway, because placement decides what a check can physically stop. A proxy in the request path can end a call with a 403 before the MCP server ever receives it, but cannot change the request. A pre-call hook running inside the proxy the application already calls can also remove denied tools before the model sees them. Most teams end up with both, and the discipline worth building is knowing which one any given rule depends on. See the wider pattern in [MCP authorization](https://www.cerbos.dev/features-benefits-and-use-cases/dynamic-authorization-for-MCP-servers).

### Can an AI model enforce access control on its own tools?

An AI model cannot enforce access control on its own tools. Filtering which tools appear in a request shapes what the model will consider, and shaping the model's inputs is still shaping the model. OWASP's AI security verification standard requires access control decisions to be enforced by application logic or a policy engine and never by the model itself, with the decision point isolated from the agent's execution environment.

### What is COAZ and how does it apply to MCP authorization?

COAZ, which stands for compatible with OpenID AuthZEN, is a pair of Standards Track drafts dated 13 February 2026 that project any protocol's information model into an [AuthZEN](https://www.cerbos.dev/authzen) authorization request through a declarative mapping, with MCP as the reference binding. 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. It also names two conformant deployment shapes, one where a gateway enforces and one where the MCP server enforces and calls the decision point itself. The drafts come from the OpenID AuthZEN working group, where Cerbos engineers contribute.

### What does an MCP gateway not protect against?

An MCP gateway does not protect against anything that does not cross it, so an agent with direct network egress sits outside it entirely. Neither common enforcement shape filters the tool listing per principal, so the catalogue leaks tool names even though calling a listed tool the policy denies still fails. Argument level rules also depend on stable tool schemas, since renaming a field means the rule no longer matches what it was written against. More on the shape in [what an MCP gateway is](https://www.cerbos.dev/blog/what-is-an-mcp-gateway).

### Should authorization checks for AI agent tool calls fail open or fail closed?

Authorization checks for AI agent tool calls should fail closed, so an unreachable policy engine fails the call rather than passing it through. CoSAI's agentic identity guidance makes this normative for high capability agents, requiring a clearly defined safe degradation path such as reverting to human in the loop or halting agent execution. Failing the call satisfies only the first half of that, so the degradation path is still a deployment decision, and both denials and allows need to land in one [audit trail](https://www.cerbos.dev/features-benefits-and-use-cases/audit-logs).

### The difference between denying an MCP tool call and hiding a tool from the model?

The difference between denying an MCP tool call and hiding a tool is that one stops an action and the other stops an attempt. A 403 on a tool call means the agent tried, the attempt failed, and the denial is in the decision log. Removing a tool from the request before the model sees it means the attempt never happens, so the model does not plan around a capability it cannot use. Neither substitutes for the other, because an agent that assembles its own tool list or reaches the MCP server directly is stopped only by the per call check, which is what [MCP permissions](https://www.cerbos.dev/blog/mcp-permissions-securing-ai-agent-access-to-tools) covers.
