Skip to content
Contextely
Academy6 min readBy The Contextely Team

MCP Authorization: Govern Access Per Person, Not Server

Why mcp authorization has to sit between the agent and the data, not at the server connection, and how per-person entitlement actually works.

A keycard reader mounted beside a locked door, standing in for mcp authorization checked at the door rather than at the building entrance

Photo: Jakub Zerdzicki on Pexels

Key takeaways

Connect two different people's agents to the same MCP server and, in most deployments today, they get back the exact same answer to the exact same question, because the server was given one shared credential and never asked to tell them apart. That gap between "this agent may open a session" and "this specific asker may see this specific record" is what mcp authorization actually has to close, and the protocol spec does not close it for you.

What the MCP spec actually covers

The Model Context Protocol's authorization specification describes how an MCP client obtains a token to talk to an MCP server, built on OAuth 2.1 with PKCE. It is a real and useful piece of engineering: it standardises how a client proves it is allowed to open a session, and it is optional for STDIO transports precisely because a local process already gets its credentials from the environment it runs in.

What it does not describe is what happens after the session opens. Once a client holds a valid token, the spec is silent on whether every tool call against that session should return the same data for every caller. That question, whether the person on the other end of this specific call may see this specific record, is left entirely to whoever wrote the server. Most servers answer it by not asking. The token authenticates the connection once, and every tool call after that runs with the same access as the token itself.

Does OAuth for MCP solve authorization?

Not on its own, and the terminology is worth being precise about. Authentication answers "who is calling." Authorization answers "what may they see or do." MCP's OAuth layer is squarely aimed at the first question. A support agent's key and an engineering lead's key can both pass every check in the spec and still be, functionally, the same key from the server's point of view, if nothing downstream tells them apart.

Per person, not per server

The alternative is to resolve every call to the specific person whose credential it carries, and check their entitlement against the specific thing they are asking for, not against the server as a whole. Concretely, that means:

That is a small, deliberately boring piece of algebra. It does not need a bespoke permissions DSL or an external policy engine to express most of a real org chart: department heads with a subtree wildcard, individual contributors with a handful of specific leaves, everyone else with nothing for that data at all.

"Authorization is OPTIONAL for MCP implementations. Implementations that do use authorization... SHOULD conform to this specification."
Model Context Protocol specification, Authorization, June 2025 revision

Read literally, that line is about whether a server bothers with OAuth at all. Read practically, it is a reminder that the spec treats authorization as a connection-level nicety a server can adopt or skip, and says nothing about what happens for the calls that come after.

Where the check actually has to run

The place that matters is not the connection handshake. It is the instant a tool call is about to return a result, and the cleanest way to enforce it is to make entitlement a factor in ranking rather than a filter bolted on afterwards:

score = relevance × (1 − w + w × freshness) × entitlementFactor

entitlementFactor is either 1 or 0, never a partial value. A record the caller is not entitled to see scores exactly zero and is dropped by the same cut that removes results that were simply irrelevant to the question asked. There is no separate redaction pass afterwards, and no synthesis step that ever receives the withheld object in the first place, because a model already holding a record cannot reliably be trusted to leave it out of its answer on request.

Where the check runs What it catches What it misses
At the connection (a bearer token, checked once) Whether this caller may talk to the server at all Everything about which specific records this caller may see
As a prompt instruction ("do not reveal salary data") Nothing reliably; it is advice to a model that has already read the data The actual disclosure, if the model does not follow the instruction
As a multiplicative factor inside the ranking function Every call, every record, with no reliance on the model's judgement Nothing structural, though it depends on every record correctly declaring its own required scopes

Table 1: three places to put an authorization check, and what each one actually stops.

What this looks like end to end

An agent calling context_search with a natural-language question gets back a handful of small memory objects, each already filtered to what the caller may see. The reply also carries entitlement.withheld_for_entitlement, a count, above zero when relevant material existed that this specific asker could not see. That number is not decorative. An agent that ignores it will answer a question as though it had the full picture when it did not, and the honest response is usually to say so and point the person at a colleague who holds the missing scope, not to quietly work around the gap.

What happens to a record I am not entitled to see?

It is not returned, not summarised, and not mentioned by content, only by count. The distinction matters because a system that answers "forbidden" for something that exists but is hidden has already disclosed that it exists. The honest answer, and the one that gives an attacker or an over-curious colleague nothing to work with, is the same not_found response you would get for a record that never existed at all.

Common pitfalls when adding this to an existing MCP server

If you are evaluating this for your own stack, the MCP pillar page states plainly what is and is not in scope for a permissioned layer, /security has the entitlement ordering written out against the actual code, and /self-host lets you run the same engine against your own Postgres to see the scoring in action. For the retrieval side of the same guarantee applied to a broader RAG setup rather than specifically MCP, permission aware rag covers the general pattern this post is one instance of.

Frequently asked questions

Does OAuth for MCP solve mcp authorization on its own?

It solves a different, earlier problem: whether a client is allowed to open a session with a server at all, using OAuth 2.1 and PKCE. It says nothing about what that session may retrieve once it is open. A support agent and an engineering lead who both hold a valid OAuth token for the same server are, as far as the protocol is concerned, equally authorized to call every tool it exposes. Per-person governance has to be added on top, by whoever is answering the tool call.

What happens to a record an agent asks about but the asker cannot see?

It scores exactly zero and is never returned, never summarised and never passed to a model. There is no separate redaction step and no prompt telling the model to keep quiet about it, because a model that has already read something cannot reliably be trusted not to allude to it. The object simply never crosses the boundary.

Is per-person mcp authorization the same thing as an API key per person?

A key per person is necessary but not sufficient. It identifies who is calling. What matters is what happens next: whether the server resolves that identity to a set of scopes and applies them before deciding what a tool call returns, or whether every key just gets the same answer regardless of who holds it.

Does this slow down retrieval?

No, because the entitlement check is not a separate pass. It is folded into the same scoring function that ranks results for relevance, so checking whether an object is visible costs the same as checking whether it is relevant, because it is the same multiplication.

Free for 500 retrievals a month, and self-hostable with no limits.