Skip to content
Hironobu Iga

COAZ-MCP: authorising MCP tool calls with AuthZEN

Notes on COAZ-MCP, the working group draft that maps MCP messages onto AuthZEN decision requests — the problem it tries to solve, how default and declared mappings work, and where it stands today.

Published

Introduction

In my previous article I organised what I had found while looking into AuthZEN, the standard specification for authorisation. There I noted that two new working group drafts had been published in June 2026. Of the two, the one that authorises MCP (Model Context Protocol) tool calls was the one I was curious about, so I decided to read it.

When I opened “AuthZEN Profile for Model Context Protocol Tool Authorization” from the announcement, however, there was almost no body text. All that remained was a notice saying the document had been replaced.

As before, this is me organising things at the start of learning; these are my reading notes as of August 2026.

The name COAZ and the two drafts

Following the trail, the draft that had been a single document in June had been split, in the working group’s repository, into the following two drafts.

  • COAZ Framework: a protocol-independent framework. It defines the common rules for projecting any protocol’s messages into AuthZEN decision requests (the shape of a mapping, the distinction between literals and expressions, and conformance requirements)
  • COAZ-MCP: the MCP binding of that framework. It defines how MCP’s JSON-RPC messages are mapped into AuthZEN decision requests

COAZ stands for Compatible with OpenID AuthZEN, and according to the draft it is pronounced “cozy”.

The problem it tries to solve

MCP already has an authorisation mechanism built on OAuth 2.1 in the first place (an optional mechanism defined for HTTP-based transports; for stdio connections the recommendation is not to use it but to take credentials from the environment). Why is another specification needed? The draft lists two ways in which OAuth alone falls short.

The first is granularity. What access token scopes express is the coarse division of “which range of operations is this client allowed to perform”, and scopes alone cannot standardise decisions that depend on the arguments, such as “may this user access the customer record this call is actually about to touch”. The decision itself remains, so where it lives and how it is assembled varies from implementation to implementation. It is the MCP version of the “authorisation logic scattered across the app” problem I wrote about in the previous article.

The second is distinguishing the principal. In configurations where an agent acts autonomously, the access token may be issued to the agent itself. MCP’s authorisation machinery did not define a way to convey, per request, “who the agent is” separately from “on whose behalf it is acting”.

COAZ-MCP tries to answer both by projecting MCP messages into AuthZEN decision requests (subject / action / resource / context) and asking a PDP (Policy Decision Point, the side that makes the authorisation decision).

What the default mappings cover

What surprised me when reading it is that the target is not just “tool calls”. COAZ-MCP defines a default mapping for each client-to-server request other than ping, so not only tools/call but also resources/read, prompts/get, and so on go through the PDP’s decision. Some things do not go through. ping and notifications are defined as pass-through and never call the PDP, and unknown methods matching no mapping fall to a deny (fail closed). In the opposite direction, requests from the server to the client (sampling/createMessage and so on) do not fit a framework that authorises with the client’s token, and are out of scope in this version.

The default mapping for tools/call has the following shape.

{
  "evaluation": {
    "subject": { "type": "identity", "id": "$token.sub" },
    "context": { "agent": "$token.?client_id" },
    "action": { "name": "tools/call" },
    "resource": { "type": "tool", "id": "$params.name" }
  }
}

A leading $ marks an expression: $token refers to the claims of the access token, and $params to the JSON-RPC params. In other words, by default the decision request becomes “may the principal identified by the token’s sub call this tool name”. The foundation is that a decision request can be constructed in this shape without preparing a mapping per method.

Declared mappings that override per tool

This alone, however, does not answer the first problem, granularity: the resource in the default mapping stops at the tool name, so “which customer’s record” cannot be used in the decision. An MCP server can therefore define a declared mapping limited to a particular tool by writing x-authzen-mapping in the tool’s inputSchema. Only string values beginning with a single $ are evaluated as CEL1 expressions; everything else is used as a literal (to write a plain string that begins with $, it is escaped as $$).

{
  "name": "get_customer",
  "inputSchema": {
    "type": "object",
    "properties": {
      "id": { "type": "string" }
    },
    "required": ["id"],
    "x-authzen-mapping": {
      "evaluation": {
        "subject": { "type": "identity", "id": "$token.sub" },
        "action": { "name": "get_customer" },
        "resource": { "type": "customer", "id": "$params.arguments.id" },
        "context": { "agent": "$token.?client_id" }
      }
    }
  }
}

With this, a call whose token sub is alice and whose id argument is cust-12345 becomes a decision request at the granularity of “may alice perform get_customer on customer cust-12345”. For tools where a single invocation needs multiple decisions (a copy operation checking both read on the source and write on the destination, for example), there is also a form that bundles multiple decisions into one request.

The point I found well designed is that this mapping travels in the tools/list response. In a configuration that places a gateway between MCP clients and servers, the gateway acts as the PEP (Policy Enforcement Point, the side that requests a decision and applies the result), and merely by relaying tools/list it obtains each tool’s mapping, so no out-of-band configuration is needed to distribute the mappings. Even a gateway run by a platform team that knows nothing of the server’s business logic can mechanically enforce the authorisation the server declared — that is the division of labour.

That said, not having to write per-tool mappings when the defaults suffice, or not needing out-of-band configuration to distribute declared ones, is not the same as working with no preparation. What this machinery presupposes is a COAZ-aware PEP, a CEL evaluator, a PDP that returns decisions, and a verifiable JWT-formatted access token with agreed claims. $token refers to JWT claims, while OAuth 2.1 itself does not restrict access tokens to JWTs.

Distinguishing the principal, and how far a declaration can be trusted

The second problem, distinguishing the principal, is built into the very shape of the mappings: the subject of a decision request is “the user behind it”, and the agent goes into agent in the context. The source of subject.id is the token’s sub by default, and for token-issuing arrangements where sub identifies the agent itself, a deployment can agree to use a different claim that carries the on-behalf-of relationship instead.

That said, when I asked myself whether this shape alone makes a decision like “allow this user, but not via this agent” safe, I could not read it that way. To begin with, the client_id the default mapping copies into agent is an OAuth client registration identifier, not a value that proves the identity of an individual agent. The only field with a defined check against the token is subject.id; the context.agent a declared mapping constructs is outside the scope of verification. To rely on the agent’s identity, the PEP or the PDP has to verify the agent ID through a separate channel. Whether context.agent too should carry such backing is a question filed as an open issue.

A line is also drawn around trust in the declared mapping itself. The one who writes the mapping is the MCP server, that is, “the party being authorised”. So the draft recommends that subject.id be matched to a claim of the validated token. It is SHOULD rather than MUST because room is left to set it from an unverifiable source, for configurations where the user’s identity cannot be carried in the token. Every other attribute, including subject.type and context, is treated as untrusted input that must not serve, on its own, as evidence of identity or privilege.

Even so, whether the declared action and resource correctly represent what the tool actually does is something the PEP cannot verify. How far to trust the server as the author of the mapping has to go into the threat model. A rule to prevent the operation from changing between the permit and the forwarding is still only a proposal.

Current status

Both the COAZ Framework and COAZ-MCP are first versions of working group drafts. Unlike the Authorization API 1.0 (a Final Specification) covered in the previous article, they have not even become Implementer’s Drafts, and are documents at the stage most likely to change. Indeed, both the name and the way the documents are divided have already changed once.

What’s more, these are drafts on the side of the OpenID AuthZEN Working Group: they are neither part of the MCP specification itself nor an adopted MCP extension. On the MCP side, a proposal issue is open in the extensions repository.

What I want to try next

I now want to add the following to the verification candidates I listed last time.

  • Combine an MCP server whose tool definitions carry x-authzen-mapping with a PDP and authorise tools/call
  • Check how far the default mappings alone can protect, in a configuration with a gateway as the PEP

Since the drafts are still moving, rather than chasing the details, I think the better approach is to first run the default mapping shape as it is and get a feel for it.

References

Footnotes

  1. Common Expression Language. A small expression language originating at Google, also used in Kubernetes admission control, among other places.