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-mappingwith a PDP and authorisetools/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
- COAZ: A Framework for Mapping Information Models to AuthZEN Authorization Requests (draft)
- COAZ-MCP: COAZ Binding for the Model Context Protocol (draft)
- PR #541, which split COAZ into the Framework and the MCP binding (merged in July 2026; what I read for this article is the post-merge layout)
- OpenID Foundation advances authorization for the agent era with new AuthZEN Working Group Drafts (OpenID Foundation)
- Looking into AuthZEN, a standard specification for authorisation (previous article)
Footnotes
-
Common Expression Language. A small expression language originating at Google, also used in Kubernetes admission control, among other places. ↩