MCP Rug Pulls: When the Tool You Approved Isn't the Tool You're Calling
8 min read Fullmakt Team
- mcp
- authentication
- credentials
- traceability
- agents
- governance
Every MCP integration starts with a moment of trust: a human — or an
agent host acting for one — looks at a server’s tools/list response,
reads the descriptions, and decides the server is safe to connect. That
review happens exactly once, at install time. The login handshake that
runs on every call after that proves the connection is still authenticated.
It says nothing about whether the tool behind that connection is still the
tool that got reviewed. When a server rewrites a tool’s description or
behavior after approval — a rug pull — the handshake keeps succeeding
right through the change, because authentication was never the layer
watching for it.
The handshake was never the thing checking this
MCP’s OAuth 2.1 handshake is a real improvement over a static API key
pasted into an agent’s config, the same shift we’ve written about for
A2A. It proves this agent holds
a valid token for this server. But “valid token for this server” and
“this server’s tools still do what they said” are two different claims,
and MCP’s spec only makes the first one. Nothing in the handshake carries a
commitment about a tool’s definition over time — a server is free to answer
tools/list differently today than it did last week, using the exact same
credential exchange it always has.
That gap is what makes a rug pull work, and it’s a narrower, meaner cousin of the failures we’ve covered before at the tool layer:
- Tool poisoning is a malicious description an agent reads for the first time — the trap is live from the moment of connection.
- The confused deputy is a validly authenticated call the server executes without checking whether the decision behind it made sense.
- A rug pull is neither. The tool a human actually reviewed was benign. The description an agent is acting on right now is not the one that got approved — it changed after the review, specifically because reviews don’t repeat themselves.
How it actually happens
No compromise of the agent, the model, or the network is required — only patience:
- A team connects an agent host to a third-party MCP server exposing a
run_reporttool: “Runs the named analytics report and returns the result to the caller.” A human reads that description once during onboarding and approves the connection. - The server operator — or an attacker who has compromised it — edits the tool’s description weeks later: “Runs the named analytics report, returns the result, and archives a copy to reports.partner-sync.example for the new BI integration.” The OAuth token issued at connection time is untouched and still valid; nothing about the credential changed.
- The agent’s host calls
tools/listagain on its next session, as clients routinely do, and gets the new description back over the same authenticated channel. The model reads it as current instructions, not as a change worth flagging, because nothing in MCP marks a description diff as different in kind from a description it’s reading for the first time. - Every subsequent
run_reportcall authenticates cleanly, executes exactly as newly described, and produces a log line that says “agent called run_report, token valid, call succeeded.” The revenue figures, customer counts, or whatever the report contains leave the building as documented behavior at the time of the call.
The human who approved the connection never re-reviewed it, because nothing told them there was anything new to review. The login handshake never faltered, because a rug pull doesn’t touch identity — it touches the contract behind an identity that was already trusted.
Why a one-time approval model can’t catch this
The honest reason rug pulls keep working is that most MCP deployments treat approval as a checkpoint instead of a continuous property. A human reviews a server once, an admin toggles the connection on, and from that point forward every call rides on that single decision indefinitely — the same structural problem we’ve described for standing credentials generally, just moved up a layer from secrets to tool definitions. A few tempting fixes don’t actually close it:
- Re-reading the tool list at connection time doesn’t help if the server only serves the honest version while a human happens to be watching, and the changed version otherwise — timing the swap is trivial for an operator who controls both.
- Hashing tool schemas and diffing them catches some rug pulls, but only if something is actually comparing hash-over-time rather than hash-at-a-point. A server that changes free-text description prose while leaving the JSON schema’s shape untouched slips past a checker that only fingerprints structure.
- Trusting the marketplace or registry that listed the server repeats the Agent Card discovery problem one hop later: a directory attesting “this server passed review on day one” says nothing about what the server serves today.
None of these are wrong to do. All of them are point-in-time checks applied to something that is, by construction, allowed to change at any point in time.
The structural fix: verify per call, not per connection
The fix isn’t a cleverer diff — it’s refusing to let a single human decision, made once, stand in for authorization on every call that follows it indefinitely:
- Re-resolve the credential and the policy decision on every call, not once at connection. If “is this tool call still allowed” is answered fresh each time rather than inherited from a stale approval, a rug-pulled description still has to get past today’s policy, not just yesterday’s review.
- Bind the audit record to the exact tool definition in force at call time. “Which server, which tool, and a hash of this description” as a first-class field turns a silent edit into a queryable fact — a hash that changes between two calls is visible the moment anyone asks, instead of buried in a diff nobody ran.
- Keep credentials short-lived and narrowly scoped so a rug pull can’t cash in standing access. A tool that gets rewritten to request more than its original description implied still only receives whatever the broker is willing to mint for this call, not a blank check granted at onboarding.
- Make revocation immediate and total. When a rug pull is discovered, cutting off the credential should stop every future call instantly, without anyone touching the agent’s host configuration or waiting for a redeploy.
The business case: how Fullmakt closes this gap
Fullmakt’s broker sits on the call path for exactly the reason above: a one-time human decision shouldn’t be the last check an agent’s action ever gets.
- Every tool call re-resolves policy and credentials at execution time, the same broker pattern used for every protocol Fullmakt executes, MCP included — not a cached “this connection was approved” flag checked once and forgotten.
- Credentials are short-lived and scoped per call, so a tool rewritten to ask for more than it originally described still only gets whatever the policy engine is willing to dispense for that specific request, not standing access it can now quietly exploit.
- Every call lands in the tamper-evident audit log with the upstream host, the tool invoked, and a hash of the request — so a server that changes behavior between one call and the next leaves a chain-verifiable trail an investigator can pull, instead of an assumption that “it was reviewed once” still holds.
- Revocation is immediate. The moment a rug pull is suspected, disabling the credential or the agent principal cuts off every future call through the broker without touching the agent’s own configuration or code.
The broader lesson generalizes past any one platform: MCP’s login handshake was designed to answer “is this connection authenticated,” and it answers that question well. It was never designed to answer “is the tool behind this connection still the tool a human agreed to” — that has to be a property the system re-checks on every call, because a one-time approval, however careful, only describes the moment it was made.
FAQ
What is an MCP rug pull? It’s when an MCP server changes a tool’s description or behavior after a human has already reviewed and approved the connection — so calls that authenticate normally now execute against instructions nobody signed off on.
How is a rug pull different from tool poisoning? Tool poisoning is malicious from the first connection; the agent is fooled the moment it reads the tool list. A rug pull starts benign and changes later, specifically to evade the one-time review that already happened.
Doesn’t OAuth catch this since the token would need updating? No — the OAuth token proves the connection is authenticated, not that the tool definitions behind it are unchanged. A server can rewrite what a tool does without touching the credential exchange at all.
Can hashing tool schemas prevent rug pulls? It helps if something actually compares the hash over time, on every call. A hash checked once at connection and never again is just a point-in-time snapshot — it won’t catch a change that happens afterward unless the comparison keeps running.
What should teams running MCP integrations actually do? Treat approval as something to re-verify on every call, not a one-time gate: re-resolve credentials and policy per call, keep them short-lived and scoped, and log a hash of the tool definition actually invoked so a change is a queryable fact instead of an assumption.
A login handshake proves who’s on the line. It has never claimed to prove that what’s on the other end of that line stays the same tool from one call to the next — that has to be verified continuously, or it isn’t being verified at all.