DEV Community

The Agent Loop
The Agent Loop

Posted on

Why your MCP approval gate never fires (and what to do instead)

Drafted with AI help, human-reviewed by The Agent Loop.

Short version: MCP can describe a tool as dangerous. Almost nothing in the stack is required to treat it that way. Annotations get dropped, hosts gate at the UI, servers assume someone else enforced it. I read the spec-shaped complaints, one arXiv study, and a Vercel issue that shipped a checkout tool with zero approval events. The gap is not “we forgot one config flag.” It is that permission was never a protocol primitive.

For skimmers

  • MCP auth today ≈ optional transport OAuth, not per-tool policy
  • destructiveHint / readOnlyHint are hints; a client can ignore them (see repro below)
  • Host “approve?” dialogs live in the UI; MCP servers often never hear about them
  • Scanners: 71% of 100 MCP servers scored F on one 2026 framework (vendor research, label it)
  • Today’s fix: enforce server-side (or a gateway), default deny for write/delete, log every call

What MCP actually specifies

The Model Context Protocol standardizes how models find and call tools. It does not standardize:

  • which agent may call which tool
  • read vs write vs destroy as a capability the runtime must enforce
  • call budgets, rate limits, or session budgets
  • an audit schema every server must emit

A practitioner who shipped five security MCP servers (EDR, proxies, bug-bounty APIs) wrote the same conclusion I keep hitting: every server invents its own ACL because the protocol leaves authorization to the implementer (johnmatrix, Jul 2026). That is a design choice, not a temporary bug.

Tool metadata does include safety-ish fields in the schema (readOnlyHint, destructiveHint, idempotentHint). See the MCP schema. Metadata is not enforcement.

Diagram: MCP annotations are optional; without gateway policy, side effects fire with no approval


The repro that made me stop

Vercel AI SDK issue #20184 walks through an official-style MCP commerce example:

Arm Setup Result
1 Example wiring as shipped submit_order created (2500¢), approval events: 0
2 Inspect converted tool no needsApproval; destructive annotations gone
3 Explicit policy wrap approval gate present, execution blocked

The conversion path kept annotations.title and dropped the rest. The server said “destructive, not idempotent.” The client never asked a human.

That is the whole blog post in three rows: declare ≠ decide ≠ enforce.


Numbers worth knowing (with labels)

Claim How to treat it
97.1% of 856 tool descriptions had ≥1 “smell”; 56% unclear purpose Peer-style arXiv study of 103 servers (2602.14878)
71% of 100 servers scored F; 163 hallucination-based vulns Vendor scanner research (AgentsID, 2026): directionally useful, not neutral audit
~8.5% of servers implement OAuth Secondary industry write-up; treat as order-of-magnitude
1,862 public MCP servers answered unauthenticated July 2025 scan cited in CSA note
Tool-poisoning peak success 72.8% (mean 36.5%) AAAI 2026 paper as summarized in secondary coverage

I do not need all five to make the point. Even one exploded description field plus one zero-approval checkout is enough: the dangerous path is the default path.


Why host “Allow” is not a security boundary

Interactive hosts (Claude Code, desktop apps, IDEs) ask you to approve tool calls. Good UX. Three leaks:

  1. Different process. The MCP server may only see “a tool ran,” not “a human clicked Allow on this argument set.”
  2. Different rules. If the host forgets the gate (example above), nothing downstream re-checks.
  3. Persuasion. Prompt injection (our last post) aims at the reasoning layer. Policy that lives only in the model’s head can be talked into a “yes.” Execution-time checks do not care how nice the story was.

What to do before MCP grows a real permission model

I would not wait for a standards working group.

  • Default deny writes. Read tools can be broad; create / delete / send / pay start blocked.
  • Enforce outside the model. Gateway or server-side policy (OPA, Cedar, even a boring allowlist in config). Bedrock AgentCore and Vercel’s @ai-sdk/policy-opa exist because people hit this wall.
  • Never ship mcpClient.tools() straight into the model loop for anything with side effects. Filter, wrap, or gate first.
  • Pin and re-approve tool lists. Rug pulls change definitions after day-one approval; re-prompt on schema diff.
  • Scope credentials per server. One god-API-key in an env var is a prompt injection away from your whole cloud account.
  • Log tool name + args + policy verdict. If you cannot answer “what did it call last night,” you cannot incident-respond.

Bottom line: treat MCP like a plugin system for a probabilistic process. Annotations are comments. Something boring and deterministic has to say no.


FAQ

Does MCP have built-in permissions?

No first-class per-tool permission model. Auth work targets transport identity (OAuth), and authorization is largely optional / implementation-defined.

What do destructiveHint and friends actually do?

They declare intent in the tool metadata. Clients are supposed to use them. Issue #20184 shows a conversion path that did not, and a checkout tool ran with no approval.

So is MCP insecure?

MCP is a connection protocol. Security is whatever your host, server, and gateway implement. Most of the ecosystem implements less than the spec’s own “shoulds.”

Where do I start if I run an MCP server tomorrow?

Server-side authz on every mutating tool, unique credentials, deny-by-default for dangerous verbs, audit log. Then argue about protocol politics.


Sources


Related on The Agent Loop

Over to you: Has an MCP approval gate ever silently not fired for you? What broke? Reply below — I read every comment.

Top comments (0)