How to authorize every MCP tool call (not just the login)
Most MCP security write-ups stop at the login. The client gets an OAuth token, the server accepts it, done. That answers one question: who is the user? It doesn't answer the one that matters once an agent is running: sh
Most MCP security write-ups stop at the login. The client gets an OAuth token, the server accepts it, done. That answers one question: who is the user?
It doesn't answer the one that matters once an agent is running: should this agent call this tool, with these arguments, for this person, right now?
An agent can make hundreds of tool calls from one instruction. If access is decided once at sign-in, every one of those calls inherits that decision. Here is how I think about closing the gap, in four steps.
1. Get authentication right first
For remote (HTTP) MCP servers, the spec already gives you a solid base. As of the 2026-07-28 version of the MCP authorization spec:
The MCP server is an OAuth 2.1 resource server. It doesn't issue tokens, it checks them.
When a client calls without a token, the server answers 401 and points to its Protected Resource Metadata (RFC 9728), which names the authorization server to use.
The client signs the user in with PKCE, and asks for a token bound to that one server with the resource parameter (RFC 8707). A token minted for server A shouldn't work on server B.
The server must not pass the client's token through to upstream APIs. That's the confused-deputy problem: the upstream API would trust a token that was never meant for it.
Local stdio servers are different: they get credentials from the environment, and the spec's OAuth flow doesn't apply.
If you do only this, you already beat the common setup, which is a long-lived API key pasted into a config file on every laptop.
2. Give every agent run its own identity
Here's the first thing the login doesn't solve. If Jane runs Claude Code, Cursor and a cron script, and all three use Jane's token, your logs say "Jane" for everything. You can't block the script without blocking Jane.
What you want on every call is two identities:
The person who delegated the work (Jane).
The agent instance doing it (this Claude Code session, on this machine, started at 9:02).
Concretely, that means the token the MCP server sees carries both: the user as the subject, and the agent instance as a separate claim (an actor, or a client/instance ID). Now you can revoke one agent instance and leave Jane and her other agents alone. And your audit log can say "Jane's Cursor session deleted 40 rows," not just "Jane."
3. Decide on every call, down to the arguments
Role-based rules usually stop at the tool name: "the Support role can use crm.update_record." That's too coarse for agents. The risky part is often in the arguments:
crm.update_record on a ticket the user owns: fine.
The same tool, with account_id of a customer the user has never touched: not fine.
db.query with a SELECT: fine. With DROP TABLE: never.
So the decision point needs to see the whole call: who (user + agent), which server, which tool, and the arguments. In pseudo-policy:
Illustrative only, not a real product syntax
- allow: tool: github.create_pull_request when: repo in user.repos
- deny: tool: github.delete_repository # destructive: never via an agent
- allow: tool: db.query when: args.sql starts_with "SELECT"
Two details make this work in practice:
Deny by default. A call with no matching allow rule is blocked, so a new tool doesn't silently become available.
Filter the tool list too. If a user can never call delete_repository, don't show it to their agent at all. The model can't misuse a tool it never sees.
4. Keep no standing credentials, and record every decision
Two last pieces:
Short-lived tokens, per user, per server. The agent never holds a long-lived API key. Upstream calls carry the user's own access, so their existing permissions in GitHub or Salesforce still apply.
A record of every decision, including denials. Allowed calls tell you what happened. Denied calls tell you what an agent tried to do, which is often the more interesting signal.
Where this logic lives
You can build steps 2 to 4 into every MCP server you run. That works for one or two servers. It doesn't scale to twenty, owned by different teams.
That's why most enterprises put an MCP gateway in front: one endpoint the agents connect to, which signs users in through the company's existing identity provider, attaches the agent's identity, makes the per-call decision and writes the audit record, then forwards allowed calls to the real servers.
When you evaluate gateways, the questions I'd ask are:
Does it identify the agent instance, or only the user?
Does policy see the arguments, or only the tool name?
Are denied calls logged with the rule that denied them?
Can you revoke one agent without signing the user out everywhere?
Does it use your existing IdP, or a new user directory?
Disclosure
I work at SecureAuth, where we build Agent Authority, an MCP gateway designed around these steps. Each agent run gets its own identity tied to the delegating user, each tool call is checked against policy down to its arguments, and short-lived OAuth tokens replace API keys. But the steps above apply whatever you use, including if you build it yourself.
If you're working on MCP security, I'd like to hear what's been hardest for you: identity, policy, or getting teams to stop pasting API keys into config files.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.