mcp-trust-scanner
MCP Trust Proxy
The problem
Most MCP security scanners check a tool's declared name, description, and input schema once - at install/connect time - and never again. That misses the attacks that actually matter: a tool's description silently changing after it's been approved (a "rug pull"), two individually-safe tools that become dangerous when combined (e.g. a file-reader plus a message-sender is an exfiltration path even if neither tool ever changes), and a live call that's malicious in the moment even though the tool's declaration looks perfectly innocent. A one-time static check catches none of these.
How it works - four layers
Trust scanner (
scanner/scan.py,scanner/hashing.py,scanner/storage.py) - hashes every tool's{name, description, input_schema}and stores it in SQLite. Every subsequent scan compares today's hash against the last trusted one: NEW / OK / CHANGED. A CHANGED tool prints a readable diff - of the description if that's what moved, or of the pretty-printed input schema if the description is identical but the schema still changed underneath it.Blast-radius composition scoring (
scanner/blast_radius.py) - tags every tool by capability (file_read, file_write, network_egress, credential_access, code_execution, destructive) using whole-word keyword matching, then looks at the union of capabilities across every connected server and flags dangerous combinations - like a file-reader and a message-sender together forming an exfiltration path - even when no single tool's hash has changed.Runtime policy proxy (
scanner/proxy_server.py,scanner/runtime_policy.py) - a real MCP server that sits in front of the backend servers. Every call is policy-checked before it's forwarded: a read of a sensitive-looking path (.ssh,.env, credentials, private keys) gets WARNed but still allowed through; a message whose body looks like it contains credential material, or whose recipient isn't on an allowlist, gets BLOCKed outright - the backend never sees it.Optional LLM explain layer (
scanner/incident_explainer.py) - only runs on WARN/BLOCK verdicts, and only ifANTHROPIC_API_KEYis set. It turns an already-made rule-based verdict into one plain-English sentence for a human reading the log. It never makes the ALLOW/WARN/BLOCK decision - that stays fully deterministic. No key set -> this layer is skipped instantly, no network call attempted, and the log just shows the rule-based reason instead.
Quickstart
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python -m scanner.scan # baseline every tool, flag drift + risky combos
python run_live_demo.py # fire real calls through the proxy: ALLOW / WARN / BLOCK
python -m scanner.report --open # generate + open the audit dashboardFirst run of scanner.scan against any server: everything comes back NEW
(nothing trusted yet). Every run after that: tools are compared against the
stored baseline.
Demo the rug-pull attack
python toy_servers/mutate_demo.py # silently poisons send-message-server
python -m scanner.scan # catches it, shows the diff
python toy_servers/mutate_demo.py --undo # restore clean stateWhat's in here
toy_servers/- three minimal MCP servers (weather, file-reader, send-message) standing in for real ones. Pointscanner/config.pyat real MCP servers (filesystem, Slack, whatever Cursor is connected to) when you're ready - same interface, nothing else changes.scanner/- the actual product: trust scanner, blast-radius scorer, runtime policy proxy, and the optional LLM explain layer.test_core.py- plain-assert smoke tests (no pytest) for the deterministic core:python test_core.py.trust_store.db- created on first run. Delete it to reset the baseline.
Known limitations
test_core.pycovers basic smoke checks (hash determinism/change detection, one capability tag, one policy verdict) - it is not a full test suite and doesn't cover every rule or edge case.Blast-radius tagging and runtime policy checks are keyword/pattern-based (word-boundary regex), not semantic. They're fast, deterministic, and auditable, but a sufficiently reworded description or argument can still slip past them - this is a v1 scope cut, not a claim of full coverage.
proxy_server.py's exposed tool schemas are hand-maintained to match the real backend servers, not fetched from them dynamically. If a backend tool's signature changes, the proxy needs a manual update to match - it doesn't automatically mirror backend drift into its own declared schema.The LLM explain layer only visibly does anything if
ANTHROPIC_API_KEYis set before running the demo; without it, WARN/BLOCK log entries show the same rule-based reason with or without that layer.Only watches tool definitions (name/description/schema) and specific runtime calls matched by the two policy rules - a server that returns injected instructions in a response, or a malicious call that doesn't match either rule pattern, won't be caught by this version.
Single-machine trust store (SQLite), not shared across a team.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/aca22akn/mcp-trust-scanner'
If you have feedback or need assistance with the MCP directory API, please join our Discord server