Skip to content

The MCP Confused Deputy: How 'It Has a Valid Token' Still Goes Wrong

5 min read Fullmakt Team

  • mcp
  • authentication
  • credentials
  • traceability
  • governance
  • agents

Most “AI agent gone wrong” postmortems get read as authentication failures: someone pasted a long-lived key into an agent’s environment, the key leaked or the agent got hijacked, game over. That story is real, but it undersells a quieter failure mode that shows up even when authentication is done correctly. The agent presented a valid token. The MCP server checked it and let the call through. Every log line says “authenticated.” And the agent still did something nobody wanted, because a valid credential and a wise decision are not the same thing.

This is the classic confused deputy problem, and the Model Context Protocol’s tool-calling model reintroduces it in a new shape: an LLM decides which tool to call and with what arguments, a well-authenticated MCP server executes on its behalf, and the server has no way to know whether the decision, as opposed to the identity, was legitimate.

How it actually happens

A concrete, boring version, no exotic exploit required:

  1. A support agent connects to your MCP server with a properly scoped OAuth token — read tickets, read the customer it’s currently helping. Standard, correct setup, matching the pattern we described in MCP and OAuth.
  2. The MCP server exposes one search_records tool, because building five narrow tools is more work than building one flexible one. The tool’s input schema takes a free-text query and a table name.
  3. A ticket contains a customer’s pasted error message that happens to include text shaped like an instruction — a classic indirect prompt injection, or just an ambiguous user request the model over-generalizes from (“pull up everything related to this account”).
  4. The model, doing exactly what language models do, calls search_records against the billing table instead of tickets. The token was valid for the call. The server had no tool-level concept of “this agent, in this task, should only ever touch tickets.” It just checked the bearer token and executed.

Nothing here required a leaked secret, an unpatched CVE, or a sophisticated attacker. The deputy — the MCP server, acting with real authority on the agent’s behalf — was confused about what its authority was actually for in this call, not whether the caller was who it said it was. Authentication answered “who is calling.” Nobody answered “should this call happen.”

Why OAuth scopes alone don’t close this

The instinct is to fix it with finer-grained OAuth scopes — split search_records into search_tickets and search_billing, gate each behind its own scope. That helps, and you should do it. But it only moves the confused-deputy boundary up one level, because scopes are still granted per-connection, not per-call. A token scoped to search_tickets is still valid for every ticket in the system, called with any query, at any time of day, for the entire lifetime of the token. It says nothing about this customer, this task, or this moment. An agent legitimately helping customer A that gets confused into querying customer B’s tickets is still inside its scope. Scopes bound the tool; they don’t bound the call.

That gap is exactly why data ownership, not just authentication, has to be enforced at the point of execution — and why the fix isn’t “tighter OAuth,” it’s a decision made fresh on every call.

What has to sit between the token and the tool

Closing the confused-deputy gap means adding a layer that evaluates the call, not just the credential — after MCP authentication succeeds and before the tool executes:

  • Policy at call time, not connect time. The tool name, the arguments, and the calling agent’s current task get checked against policy on every invocation — not once at token issuance. “Read tickets” is a scope; “read tickets for the account this conversation is actually about” is a policy decision, and it can only be made per call.
  • The credential injected, not handed over. The agent never holds the upstream database or API secret at all — the broker resolves it and injects it into the outbound request only after the policy check passes, following the same scoped-credential model regardless of which tool is asked for.
  • A record of the decision, not just the request. Every call — allowed or denied, and why — lands in a tamper-evident audit log, so “the agent queried the billing table” is something you can find in minutes instead of reconstructing from application logs after the fact.
  • A human in the loop for the calls that deserve one. Some tool invocations shouldn’t auto-execute even with a passing policy check — a bulk delete, a cross-tenant read, anything above a risk threshold routes to an approval step instead of running silently.

This is the shape of the gap between “MCP connection authenticated” and “MCP tool call was the right thing to do,” and it’s the reason a broker sits behind the MCP server rather than being replaced by it — the server’s job is protocol correctness; the broker’s job is the decision.

The business case

Fullmakt sits exactly in that gap. Every MCP tool call — and every A2A, HTTP, or GraphQL call an agent makes through Fullmakt — passes through the same broker: policy evaluated against the call’s actual arguments and the agent’s task context, the real upstream credential resolved and injected only after the check passes, and the decision written to a tamper-evident log whether it was allowed, denied, or escalated for approval. The MCP server still does what MCP servers are good at — discovery, schema, the wire protocol. Fullmakt does the part MCP was never designed to do: decide, on every single call, whether a validly authenticated agent should actually be allowed to make it, and prove afterward that it did or didn’t.

If your MCP tools currently trust “the token checked out” as the whole authorization story, that’s the confused deputy waiting to happen. It doesn’t need a leaked key or a jailbreak — it just needs one ambiguous instruction and one tool that’s a little more powerful than the task in front of it.