Externalizing MCP Tool Authorization: Policy Engines, PDP/PEP, and AuthZEN

Most MCP servers start with authorization written inline. The tool handler checks the scope, checks the tenant, checks one or two roles, and calls the API. For a single server with a dozen tools that is the right amount of machinery. It stops being right at a predictable point: when a second server appears, when a customer asks for a rule your code does not have a place to put, or when a reviewer asks to see every rule that governs a tool and the only honest answer is "read the handlers".

At that point the pattern to reach for is externalized authorization: a policy decision point that answers yes or no, and enforcement code that asks it. This is a guide to doing that for MCP tool calls, including the parts that are worse than the marketing suggests.

The split

The vocabulary is older than MCP and reviewers know it. A policy enforcement point (PEP) sits in the request path, gathers facts, asks for a decision, and enforces it. A policy decision point (PDP) holds the rules and answers. A policy information point supplies attributes the PEP did not carry. A policy administration point is where rules are authored and versioned.

In an MCP server the PEP is one place: the layer between token validation and the tool handler. Every tools/call passes through it. Every tools/list passes through it too, because a tool the caller may not invoke should not be advertised. If you have two PEPs you have two behaviors, and the second one will be the one that leaks.

The PDP can be a library in your process, a sidecar, or a shared service. Start in-process. A network hop on the hot path of every tool call buys you central administration and costs you latency and an availability dependency, and you should only pay that when more than one server is asking.

What the PEP must send

A decision request is a question about four things: who, what, on which resource, in what context. For MCP, fill them from facts the server controls, never from model-chosen arguments alone.

  • Subject. The user and tenant from the validated access token claims: sub, the tenant claim, group or entitlement claims, and the authentication context if the IdP supplies one. Also the client identity, because "which MCP client is this" is a real input.
  • Action. The tool name, and whether it reads or writes. Use a stable identifier, not the display name, and keep it stable across renames.
  • Resource. The records the call will touch, with their owning tenant and any classification the record carries. This is the expensive part: the PEP often has to resolve arguments to resources before it can ask, which means a lookup before the decision.
  • Context. Scopes present in the token, request time, source, whether a human is attached to the session, and the arguments themselves once validated against the schema.

The rule that matters: the subject and tenant come from the token, and the resource's tenant comes from your data store. If both came from arguments, a prompt-injected argument decides its own authorization. That is the same failure described in designing MCP tools that cannot exceed the human, moved one layer down.

Scopes, policy, and who owns the ceiling

Externalizing policy does not retire OAuth scope, and reviewers will ask how the two relate. Keep the division clean: the scope in the token is the ceiling the user consented to, and the policy decides everything under it. A policy engine cannot grant what the token never carried, and a token that carries a scope does not entitle a call the policy denies. Say that in one sentence in your documentation; it settles a question that otherwise consumes a review cycle. The scope design itself is a separate exercise, covered in designing OAuth scopes for MCP servers.

Languages and interfaces

There are two mature choices and one standard worth knowing.

Cedar is a typed policy language with a schema, permit and forbid statements, and forbid winning. The type checker catches policies that reference an attribute that does not exist, which is the most common authoring bug. It suits entity-shaped models: users, groups, tenants, records.

Rego, the Open Policy Agent language, is more general and will express anything, including things you should not express in a policy engine. It has a large operational footprint of tooling, bundles, and decision logs. Its flexibility is real and so is the risk of business logic migrating into policy where nobody tests it.

OpenID AuthZEN standardizes the wire format between PEP and PDP rather than the language: a request carrying subject, action, resource, and context, and a response carrying a decision. It matters here because it decouples your MCP server from a vendor's SDK and because an enterprise customer may already run a PDP they expect you to call. If you are building a gateway for a customer who has one, speaking their interface is worth more than picking your favorite engine.

Pick on one criterion first: can a reviewer read a policy file and tell you what it does. If not, no amount of correctness helps you in a security review.

Fail closed, and say where

A remote PDP will be unavailable at some point. Decide the behavior before it happens, per tool, and write it down.

Default to deny on any decision failure: timeout, malformed response, or unreachable PDP. The alternative is an outage that silently becomes an authorization bypass. Where denying every call during a PDP outage is unacceptable, the safe compromise is a signed, short-lived policy bundle cached locally with a hard expiry, so the server keeps deciding from rules it already holds and stops when they age out. Cache decisions only on an exact cache key of subject, action, resource, and relevant context, with a short lifetime, and invalidate on revocation events. A stale permit outliving an offboarded user is the finding this whole design was supposed to prevent.

Decision logs are audit evidence

Every decision should emit a record: timestamp, subject, tenant, client, action, resource identifier, decision, the policy or rule that produced it, and the policy version. Denials matter more than permits, because a burst of denials is the signal that something is probing your tools.

These records are not a second logging system. Join them to the tool-call events described in audit events for MCP servers with a shared request identifier, so a reviewer can follow one call from token to decision to result. Policy version in the record is what lets you answer "what rule was in force on the day of the incident" without guessing.

Changing policy is a change to the security boundary

Once rules live in files, they become the most dangerous deployable you own. Treat them accordingly: policies in version control, review required, a test suite of allow and deny cases that runs in CI, and staged rollout with the ability to evaluate a new policy in shadow mode against live traffic before it enforces. Shadow mode is cheap and it is the only way to learn that a tightened rule breaks a customer's daily workflow before the customer learns it.

If you let customers author rules, you now run a multi-tenant policy service: tenant isolation of policy data, limits on evaluation cost, and a review path for rules that could deny an administrator out of their own tenant. Most products should not offer this in the first year. A fixed capability model with tenant-configurable group mapping covers the majority of requests at a fraction of the cost.

What to hand a reviewer

  • A diagram showing one tool call passing through token validation, the PEP, the PDP, and the handler.
  • The policy catalog: each tool, the rule that governs it, and whether it reads or writes.
  • The fail-closed behavior, per tool, and the cache lifetime with its invalidation triggers.
  • Two worked examples: one permitted call and one denied call, each with the decision record and the rule that produced it.
  • The change process for policies, including who approves and how policies are tested.

When you do not need this

If you run one MCP server with a small tool set and one permission model, keep authorization in code. A clear check at the top of each handler, tested, is easier to review than a policy engine nobody on the team can debug at two in the morning. Externalize when you have multiple servers sharing a model, a customer who requires their own PDP, or rules that change faster than you ship code.

Where the case becomes strong is a gateway fronting several servers for one enterprise, because that is where one rule has to hold across teams with different permission models and where central administration pays for its latency. That is the shape of a governed MCP platform. If you are not there yet, an audit of the server you already run will tell you whether your authorization problem is architectural or three missing checks.