What the MCP spec covers, and what it leaves to you
- Authorization is optional in MCP. When it is used over HTTP transports, a protected MCP server acts as an OAuth 2.1 resource server and the MCP client acts as an OAuth 2.1 client. (MCP spec, Authorization)
- MCP servers must validate that access tokens were issued for them as the intended audience (RFC 8707), and return HTTP 401 for invalid or expired tokens. (same source)
- STDIO transports should not follow the HTTP flow and instead get credentials from the environment. (same source)
That gets you an authenticated, correctly scoped token. It does not answer questions like: may this agent call refund_payment for $9,000 right now, on behalf of this support rep, outside business hours? That is a per-action authorization decision, and it belongs in policy.
Step by step
- Add a policy enforcement point (PEP) in your MCP server middleware or MCP gateway, before the tool handler runs.
- Build the decision input for every
tools/call: agent identity, the human or service it acts for, tool name, arguments, the token's validated claims, and context (time, environment, approval state). - Ask the policy engine locally. Run OPA as a sidecar or library next to the server so the check doesn't add a network hop.
- Deny by default and fail closed. If the engine errors or times out, the tool call doesn't run.
- Require human approval for high-impact tools. Mark tools like payments, deletes and permission changes as high impact and require a recorded approval from someone other than the requester.
- Record every decision with its input and result, so you can show later what each agent was allowed to do and why.
- Re-check when context changes, for example a revoked role or an expired approval, instead of trusting one earlier allow.
Example Rego policy
package mcp.authz
import rego.v1
default allow := false
# Analysts' agents may read inventory, never write.
allow if {
input.method == "tools/call"
input.tool.name == "get_inventory"
"analyst" in data.principals[input.on_behalf_of].roles
}
# High-impact tools need an approval from a different person.
allow if {
input.method == "tools/call"
input.tool.name in data.high_impact_tools
input.context.approval.status == "approved"
input.context.approval.approver != input.on_behalf_of
}OAuth scopes vs. policy checks
| Comparison | OAuth scopes (MCP spec) | Per-call policy check |
|---|---|---|
| Decided | When the token is issued | At each tool call |
| Sees tool arguments | No | Yes |
| Knows who the agent acts for | Only if encoded in the token | Yes, as decision input |
| Can require human approval | No | Yes |
| Decision record | Token issuance logs | One record per tool call |
Use both: scopes to authenticate and bound the client, policy to decide each action.
Where EnforceAuth fits
EnforceAuth is an OPA control plane for apps, APIs, people and AI agents. It extends OPA; it doesn't replace the OPA runtime. Writ governs Rego policy, bundles and audit. Verdict requires human approval for high-impact agent actions. Evidence IQ keeps a durable, searchable record of authorization decisions; it records decisions and never makes or changes them. Herald is EnforceAuth's authorization MCP server, so agents and IDEs can query and operate the same policy control plane as humans. Free tier: 1 million authorization decisions a month. (MCP authorization server, AI agent authorization, Pricing)
