learntutorialintro5 minsince 2.18.0
Detect and block MCP tool rug pulls (tool description and schema drift)
Updated October 13, 2026
The problem
An MCP server sends its tool list, with descriptions and input schemas, every time a client connects, and nothing in the protocol says that list must match the one you approved. A server that changes a tool’s description after you trusted it has rewritten the instructions your model follows. The call looks the same; the tool behind it is different.
The deny
Hangar keeps a SHA-256 digest of each tool’s name, description, input schema
and output schema, and refuses a call whose tool no longer matches it. Below,
the demo server from the repository first serves an honest echo, and is then
restarted with one sentence added to the description. The same call is made
both times, through mcp-hangar 2.27.2:
# before: the call is allowed
{"is_error": false, "text": "[{\"type\": \"text\", \"text\": \"hi\" ...}]"}
# after the description changed: the call is refused before the server is asked
{"is_error": true, "text": "[{\"type\": \"text\", \"text\": \"Tool 'echo' schema does not match its pinned digest\" ...}]"}
pin --check names the drift and exits 1, so the same check belongs in CI:
$ mcp-hangar pin --config config.yaml --check
drift demo.echo
pinned: 2970199253016cbcebf2d4b43d194f2cdd4c28a8517381ae8a81aecc5edff245
serving: cac088cafdfe089cce3a7bd29ad6882124dd566ccbf7a2168b11a05a2bb5383a
The refusal is also an audit record (gate=digest_pin,
gateReason=digest_mismatch), in whichever
SIEM format you export.
The config that does it
mcp-hangar pin --write asks each server what it serves today and writes the
digests into your config:
mcp_servers:
demo:
mode: subprocess
command: [python, examples/rugpull/server.py]
tool_projection:
pins:
echo: 2970199253016cbcebf2d4b43d194f2cdd4c28a8517381ae8a81aecc5edff245
A pinned tool whose digest no longer matches is refused. To adopt a change you
wanted, run pin --write again and review the diff like any other dependency
bump. Since 2.25.0 a pinned, running server is also re-listed every
tool_projection.pin_recheck_interval_s (60 seconds by default), so drift is
logged as tool_digest_pin_drift_detected before anyone calls the tool.
What it does not do
- It does not judge intent. A digest says the contract changed, not that the change is hostile. Every change to a pinned tool is refused until you re-pin it, the harmless ones included.
- A missing event does not mean no drift. The periodic re-check sees a change at most one interval after it happens, and not while the server is cold, degraded, dead, or failing to list its tools. The call-time check is the one that refuses.
- It covers what the tool advertises, not what it does. A server that keeps its description and changes its behaviour passes the pin. Pair it with egress policy for what the server can reach.
- Unpinned tools are not refused. A tool with no pin is served as it comes, so pin every server you depend on.