Mcp Tool Rugpull Gate
Diffs every MCP server's live tool manifest against the version a human approved, and holds calls to anything that changed since.
MCP Tool Rug-Pull Gate
Diffs every MCP server's live tool manifest against the version a human approved, and holds calls to anything that changed since.
Date: 2026-11-05 Theme: Prompt Injection & Tool Poisoning Type: Web App Platform: MCP Status: Prototype
What it is
A console that keeps one approved snapshot per MCP tool — its description, inputSchema, and annotations — and re-diffs it against the live tools/list response on every scan. When a tool's behavior-relevant fields drift from that snapshot, the console flags it, explains what changed in plain language, and shows every call that was held against it until a reviewer signs off on the new version.
Who it serves
The platform or security engineer who approved an MCP server once, months ago, and has no way to know if it's still the server they approved. Also the reviewer who gets a tools/list_changed notification and needs to decide, in under a minute, whether the change is a routine update or something to escalate.
The agent security pain it addresses
MCP tool definitions are plain JSON a server controls and a client trusts after one look. Invariant Labs' April 2025 disclosure named the "rug pull": a server that passes review, then silently rewrites a tool's description, schema, or declared behavior after approval, with no mechanism in the protocol forcing a second look. OWASP's MCP Top 10 folded this into MCP03:2025, Tool Poisoning, alongside the related trick of widening a tool's reach through its annotations instead of its visible text.
The spec gives servers an optional listChanged capability and a notifications/tools/list_changed message, but the notification only says something changed — not what, and not whether it matters. The spec is also explicit that clients must treat annotation fields as untrusted unless the server is already trusted, which is exactly the gap a rug pull exploits: trust, once granted, isn't re-checked when the trusted server's own output changes. The ETDI paper (Ramesh et al., 2025) proposes closing this with cryptographic signatures bound to a specific tool version, so any edit breaks the signature and forces re-approval. This prototype borrows that enforcement idea — approve a version, gate on drift from it — without requiring the signing infrastructure, so it works against any MCP server today.
The sample data seeds three things that should jump out immediately. A CRM server's send_customer_email tool picked up a line in its description instructing an automatic BCC to an address nobody approved, plus a new optional schema field that lets a caller redirect the send. A ticketing server's close_ticket tool kept its description word-for-word but added a notify_webhook field that can POST a closed ticket's full contents, including customer data, to any URL — proof that a description-only check would have missed it. And the same CRM server is now running a bulk export_contact_list tool that was never submitted for approval at all. A call already landed against two of these before anyone looked.
How it works
- Banner. On load, the single highest-risk drifted or unapproved tool is named up front, along with whether a call has already been held against it.
- Tool inventory. Every MCP tool in scope, sorted so drifted and unapproved tools rise to the top, filterable by status and searchable by server or tool name.
- Manifest diff. Selecting a tool shows a word-level highlighted diff of its description, an added/removed/changed list for its
inputSchemaproperties, and a before/after table for its annotations with changed hints called out — because, as withclose_ticket, the description can stay identical while the schema or annotations do the damage. - Risk findings. Plain-language reasons tied to the specific fields that moved, not just a severity number.
- Call log. Every recent call attempt against the selected tool, each marked allowed or blocked with the reason, so a reviewer can see what the gate already caught.
- Review actions. "Approve new baseline" or "Approve as new tool" clears the drift and resets the approved snapshot to the current one; "Quarantine tool" blocks all further calls regardless of risk score. Both are local UI state changes only.
- Contrast case.
list_ticket_tagschanged only its display title. The gate leaves it marked unchanged, because flagging cosmetic edits teaches reviewers to ignore the gate.
What's in this folder
README.md— this filecover-image.png— cover art for the idea's card and detail pagemetadata.md— generation metadata and sourcingrequirements.md— the tool-manifest schema, risk rules, and functional requirementsindex.html— the single-page app; carries an inline copy of the sample data so it still runs iffetch()of local JSON is blockedstyle.css— layout and the navy/electric-blue/amber visual treatmentscript.js— manifest loading, the word-level description diff, schema and annotation diffing, filtering, and the approve/quarantine actionssample-data.json— 14 tools across 4 servers and 7 logged call attempts, including the three seeded drift cases above
How to run / read it
Open index.html in any modern browser. No build step, no server, no login. If your browser blocks fetch() of a local JSON file, the page falls back to an inline copy of the same data embedded in index.html.
Estimated impact
No field number — this is a prototype against sample data, not a deployment. The honest claim is about what currently has to happen by hand: a reviewer who wants to know if an approved MCP server changed has to re-read its full tools/list response against whatever they remember approving, for every server, on some manual cadence. At a few dozen MCP servers with a handful of tools each, that's a tedious diff a human will eventually skip. Automating the diff and holding calls against anything that moved turns "did this server change" from a question nobody asks into one that answers itself before a call goes through.
Adapters and scale
One adapter: any MCP server reachable over the tools/list and tools/call methods defined in the 2025-06-18 specification, plus the notifications/tools/list_changed message where a server declares it. The adapter needs read access to each server's tool manifest and a place to persist the last-approved snapshot per tool — no write access to the servers themselves. At 100+ servers and thousands of calls a day, the expensive part is the manifest polling, not the diffing: comparing two small JSON objects for a description, a handful of schema properties, and four annotation booleans is cheap per tool, so the design scales with server count and poll frequency rather than call volume. Gating calls at thousands-per-day scale means the approval-state lookup needs to sit in front of the call path, which this prototype only simulates against a static log.
Sources
- Official documentation / standards:
https://modelcontextprotocol.io/specification/2025-06-18/server/tools— the tool definition fields (name,title,description,inputSchema,annotations), thelistChangedcapability, and the requirement that clients treat annotations as untrusted unless the server is already trustedhttps://owasp.github.io/www-project-mcp-top-10/— MCP03:2025 Tool Poisoning, including rug pulls and schema poisoning as named sub-techniques
- Community / current research:
https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks— Invariant Labs' April 2025 disclosure that first named and demonstrated the rug-pull pattern this idea gates againsthttps://arxiv.org/pdf/2506.01333— ETDI, proposing cryptographic signatures bound to an approved tool version so any post-approval edit forces re-approval, the enforcement model this prototype borrows without the signing layer
Requirements
Requirements — MCP Tool Rug-Pull Gate
Purpose
Detect when an already-approved MCP tool's manifest (description, inputSchema, or annotations) changes after approval, surface what changed and why it matters, and hold tool calls against the drifted definition until a reviewer re-approves it.
Data model
Normalized tool-manifest record (sample-data.json → tools[]). Fields follow the documented MCP tool object (name/title/description/inputSchema/annotations) per modelcontextprotocol.io/specification/2025-06-18/server/tools:
id—<server_id>.<tool_name>server_id,tool_namestatus— one of:unchanged— current hash matches approved hashdrifted— behavior-relevant fields (description, input_schema, or annotations) changed; risk-bearingdrifted_benign— only cosmetic fields (title) changed; description/input_schema/annotations hash unchangednew_unapproved— tool observed on a server with no prior approval recordquarantined— reviewer action, blocks all calls regardless of drift state
risk—none|low|medium|high, assigned by the rules belowapproved_at,approved_by,approved_hash— null fornew_unapprovedcurrent_hash,observed_at— from the latest scandrift_detected_at— first scan where current_hash diverged from approved_hashapproved/current— each an object withtitle,description,input_schema(map of property →{type, required, description}),annotations(readOnlyHint,destructiveHint,idempotentHint,openWorldHint)risk_reasons— array of plain-language findings tied to specific field changes
Tool-call log record (recent_calls[]), shaped like an OCSF/OpenTelemetry span attribute set would carry it in a real deployment:
id,timestamp,tool_id,agent_name,arguments_summarygate_decision—allowed|blockedreason— plain-language tie to the manifest hash state at call time
Risk rules (illustrative — this prototype's own design, not a documented scoring standard)
new_unapproved→risk: highalways. A tool with no approval record is the clearest case: nobody has seen it.- Description text change →
risk: highif the new text adds an instruction with an external address, URL, or conditional directive (e.g. "always BCC…", "notify this webhook…"); otherwisemedium. input_schemagains a property not in the approved baseline →risk: highif the new property's description implies an external destination (email, URL, webhook); otherwisemedium.annotations.openWorldHintflipsfalse → true→ contributes at leastmedium,highif paired with a schema or description change on the same tool.- Only
titlediffers, all other fields byte-identical →status: drifted_benign,risk: none. This rule exists specifically so cosmetic edits don't desensitize reviewers to real drift — see OWASP's MCP03:2025 framing of rug pulls as a supply-chain-style trust problem, not a text-fuzzing problem. quarantinedtools always gate every call regardless of any other rule.
Functional requirements
- FR1: On load, compute
statusandriskfor every tool fromapprovedvscurrent(already pre-computed in sample data here; a live build would run this scan-side). - FR2: Render a top-level banner naming the single highest-risk tool, its server, what changed, and whether a call has already been held against it — must be visible without scrolling or clicking.
- FR3: Tool inventory table, sortable with drifted/unapproved rows first, filterable by status and free-text (server or tool name).
- FR4: Selecting a tool renders a manifest diff: word-level highlighted diff for
description, an added/removed/changed list forinput_schemaproperties, and a before/after table forannotationswith changed rows highlighted. - FR5: Selecting a tool also renders its
risk_reasonsand everyrecent_callsentry referencing it, each taggedallowed/blockedwith its reason. - FR6: "Approve new baseline" (on
drifted/drifted_benign) and "Approve as new tool" (onnew_unapproved) buttons update local state only — setstatus: unchanged,approved = current,approved_hash = current_hash— and are visibly labeled as local-only, no backend call. - FR7: "Quarantine tool" sets
status: quarantinedand visually marks every one of that tool'srecent_callsrows as held, regardless of their originalgate_decision. - FR8: No tool, server, or agent name may resemble a real company, product, or person.
Non-functional
- Pure static site:
index.html+style.css+script.js+sample-data.json. No build step, no external auth, no CDN dependency required for the core diff/gate logic. fetch()ofsample-data.jsonmust fall back to an inline<script type="application/json">copy embedded inindex.html, sincefile://fetches are blocked in some browsers.- Any artifact resembling a wire-protocol object (manifest hash, gate decision, annotations) is structurally illustrative and must be validated against a real MCP client/server pair before use — this prototype does not implement MCP transport, cryptographic signing, or OAuth.