MCP server scanner: shield-mcp scan

npm audit for MCP. Before you point an agent at a third-party MCP server, audit its tool, resource, and prompt metadata for the attacks that live in the description text: prompt-injection hidden in a tool description, over-broad capabilities, and payloads hidden under an encoding.

Table of contents
  1. Scan before you connect; gate at runtime
  2. Install and run
  3. For indie developers: the 60-second workflow
  4. What it checks (offline, no model, no network)
  5. CI gate
    1. GitHub Actions
    2. Local pre-commit hook
  6. Connected mode (model verdict)

Scan before you connect; gate at runtime

Two complementary layers:

  • Scan (static, pre-flight): shield-mcp scan inspects a server’s advertised tools, resources, and prompts before an agent ever calls them. Free, offline, runs on a laptop or in CI.
  • Gateway (runtime): the MCP gateway enforces RBAC, screens input, and sanitizes output on every live tool call.

The scanner catches the class of attack where the metadata itself is the payload: a tool whose description tells the model reading it to ignore its instructions, exfiltrate secrets, or read ~/.ssh. This is known as tool poisoning.

Curious how well-known servers score? See the MCP security registry, which rates public MCP servers 0 to 100 from these same scans.

Install and run

No install, via npx (JS/TS devs, no Python toolchain needed):

npx @votal/mcp-scan stdio:'python my_server.py'
npx @votal/mcp-scan http:https://example.com/mcp --json

Or install the Python CLI:

pipx install shield-mcp        # or: pip install shield-mcp

shield-mcp scan stdio:'python my_server.py'
shield-mcp scan sse:https://example.com/sse
shield-mcp scan http:https://example.com/mcp --json

The npx wrapper runs a local shield-mcp if you have one, otherwise it downloads a pinned, checksum-verified standalone binary. Same detection engine either way.

For indie developers: the 60-second workflow

You found a useful MCP server on GitHub or a registry and you are about to add it to Claude Desktop, Cursor, or your own agent. Audit it first.

1. About to add a server? Point the scanner at exactly what your client would launch. If your claude_desktop_config.json would run npx some-mcp-server, scan that same command:

shield-mcp scan stdio:'npx -y some-mcp-server'

A clean exit (0) means no poisoned descriptions, no over-broad tools hiding in the metadata. A non-zero exit means read the report before you connect it.

2. Building your own server? Run it against your own server before you publish, so a contributor cannot slip a poisoned description past review:

shield-mcp scan stdio:'python -m my_server' --fail-on high

3. Wire it into your repo so every change is checked automatically (below).

What it checks (offline, no model, no network)

Category Severity Catches
tool-poisoning critical instructions aimed at the model hidden in a description
encoded-content critical the same, hidden under base64, hex, or ROT13
over-broad-permission high exec, shell, delete, wire_transfer style capability
suspicious-metadata medium zero-width or bidi control characters, HTML comments, huge descriptions
shadow-capability info tools with no description (a blind spot that cannot be audited)

CI gate

shield-mcp scan exits non-zero when findings meet or exceed --fail-on (default critical):

exit meaning
0 clean, or only findings below the threshold
2 findings at or above --fail-on
3 target unreachable, or MCP handshake failed
4 usage error

GitHub Actions

Drop this in .github/workflows/mcp-audit.yml to fail a PR that introduces a poisoned or over-broad MCP server:

name: mcp-audit
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: votal/mcp-scan-action@v1
        with:
          target: "stdio:python -m my_server"
          fail-on: high

The action exposes verdict, exit-code, and the full report as step outputs. For connected mode, add shield-url and api-key: $ (the key is masked). If you would rather not depend on the action, install the CLI directly instead:

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pipx install shield-mcp
      - run: shield-mcp scan stdio:'python -m my_server' --fail-on high

Local pre-commit hook

Block a poisoned description before it is even committed. Add to .git/hooks/pre-commit (and chmod +x):

#!/usr/bin/env bash
shield-mcp scan stdio:'python -m my_server' --fail-on high || {
  echo "MCP scan failed. See findings above."; exit 1;
}

Connected mode (model verdict)

Offline mode never leaves your machine. Connected mode adds Shield’s model-backed guardrails on top:

shield-mcp scan stdio:'python my_server.py' \
  --shield-url https://shield.example.com --api-key "$SHIELD_API_KEY"

Each description is POSTed to {shield-url}/guardrails/input; a non-passing guardrail merges in as a source: model finding. Only the description text is sent, never the target server’s credentials, env, or tool-call arguments. If Shield is unreachable it fails open: the scan degrades to the offline verdict with a note, so a network blip never fails a clean CI run.


Source and full options: packages/shield-mcp/. Spec: docs/spec-mcp-scanner.md.