A tool call is an API call, and the model is the caller.
An agent’s tools are an API surface with no human clicking allow, called by the least trustworthy client you will ever have. The whole subject is two questions: whose identity does a tool call carry, and what can a leaked token do? Get those wrong and one injected instruction owns the bank.
Tier 1
Basics
Whose identity does the call carry?
Tier 2
Intermediate
Least privilege vs "just grant it"
Tier 3
Advanced
Assume the token leaks
A tool call is an API call
The agent calls get_balance(account_id). Choose the identity the tool runs as, then ask for a stranger’s account. Run with the server’s god credentials and the model reads anyone; run on behalf of the user and the data layer says no. Authenticating the agent is not authorizing the call.
The tool runs as
The model calls
account_id= /
)
That is the confused deputy: a powerful tool doing a caller’s bidding without checking whose data it is. The fix is identity on every call, which raises the next question: what is that identity allowed to do?
The scopes you actually grant
Authenticating the agent is easy. Authorizing it is the judgment. Grant the token its scopes below: three tasks need three narrow ones, but two broad scopes make nothing new work and turn the token into a skeleton key. Broad is the easy default, and the reason a leaked token so often owns everything.
Grant the token its scopes
Tasks the agent must do
Blast radius if the token leaks
0/24
Under-granted: 3 tasks cannot run yet. Grant exactly the scope each needs, and stop there.
Every scope you grant "to be safe" is a scope an attacker inherits the day the token leaks. Least privilege is not caution, it is the blast radius you are choosing in advance.
Mint tokens that are cheap to lose
You cannot guarantee a token never leaks, so the senior move is to make a leak worthless. Leak both kinds below and compare: a long-lived service token is a three-month skeleton key; a short-lived on-behalf-of token expires before the attacker can use it.
Lifetime
valid for 90 days
Scope
every scope, every customer
Audience
any tool in the platform
You cannot guarantee a token never leaks, so you mint tokens that are cheap to lose: short-lived, narrowly scoped, on behalf of one user, bound to one audience. The OAuth 2.1 on-behalf-of exchange is how MCP does this, and it is the difference between a stolen token being an incident and being a shrug.
Short-lived, rotated tokens
Never hand an agent a static key. Mint a fresh, minutes-long token per session so there is almost nothing to steal and nothing to leave lying in a log or a prompt.
In a bank → A 90-day service key in an agent’s config is the credential a pentest finds first. A 5-minute token is barely worth grepping for.
Per-tool human consent
For high-impact tools, the first use in a session prompts the user to approve the scope, the way a phone app asks before it touches your camera. The agent cannot silently expand what it can do.
In a bank → A customer authorizing "let the assistant see my balance" is not the same as authorizing "let it move my money". Consent separates them.
Every tool call is logged with its identity
Who called what, on whose behalf, with which scopes, is a trace event. Without it, a compromised tool call is invisible, and you cannot answer the only question that matters after an incident: what did it touch?
In a bank → This is where tool auth meets observability: the auth decision and the tool span are the same audit record.
A vetted tool registry
Agents should only reach tools from an approved, signed registry, not any MCP server a prompt names. An unvetted tool is a supply-chain hole: it can exfiltrate everything the agent hands it.
In a bank → Letting an agent connect to an arbitrary MCP URL is letting a stranger’s code run inside your trust boundary.
Tool auth in code, at three altitudes
Identity on every call is the concept; MCP’s OAuth flow and on-behalf-of token exchange are where you enforce it.
The concept
Every tool call is an authorization decision. The call must carry the end user’s identity, not the server’s, so the data layer can scope it. Authenticating the agent is not the same as authorizing the call.
# The tool call carries the USER's identity, not the server's.
def get_balance(account_id, *, caller): # caller = signed-in user
if not caller.owns(account_id): # authz, at the data layer
raise Forbidden
return db.balance(account_id)
# a stolen or injected account_id cannot cross the caller boundary.The point: caller identity, per-call scopes, and short-lived on-behalf-of tokens are the concept. MCP’s OAuth 2.1 flow, a token-exchange service, or a scoped API gateway are just where you enforce them.
The same question, three depths of answer
"How do you secure the tools an agent can call?" The depth of your answer is the level they hire you at.
Put an API key on the tool server so only the agent can call it.
Every tool call carries the end user’s identity, not the server’s, and is authorized at the data layer, with least-privilege scopes per token so a task can only touch what it needs. Authenticating the agent is not authorizing the call.
On-behalf-of OAuth token exchange so the agent acts as the user with the user’s permissions; short-lived, rotated, single-audience tokens so a leak has a tiny window and blast radius; per-tool consent for high-impact actions; every call logged with its identity and scopes as an audit span; and tools reachable only through a vetted, signed registry, not an arbitrary MCP URL. Tool auth is an identity system, not an API key.
On your real stack this week
Make one tool run on behalf of the user.
Find a tool your agent calls that currently runs with a service account, and thread the end user’s identity through so the data layer scopes the result. Then try to make the agent read another user’s record and confirm it is denied by identity, not by a prompt. That single change closes the confused-deputy hole.