Skip to main content
Glama
aca22akn

mcp-trust-scanner

by aca22akn

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

  1. 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.

  2. 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.

  3. 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.

  4. Optional LLM explain layer (scanner/incident_explainer.py) - only runs on WARN/BLOCK verdicts, and only if ANTHROPIC_API_KEY is 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 dashboard

First 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 state

What's in here

  • toy_servers/ - three minimal MCP servers (weather, file-reader, send-message) standing in for real ones. Point scanner/config.py at 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.py covers 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_KEY is 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

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