Cisco mcp-scanner: Zero Keys, Zero Config, One Command to Scan All Your MCP Servers

The one-line summary

Before you install an MCP server, put it through security screening. mcp-scanner pulls out every place an MCP server can hide something — tool descriptions, prompts, resources, server instructions — and scans each one, with nine analyzers covering different ground. What sets it apart from its peers is not the number of rules but one interface contract: when the scan is incomplete, is_safe returns None, not True.

What it scans

Code is not the only poisonable surface in an MCP server. A tool’s name / description / inputSchema is text the model reads directly; prompts and resources are text the server injects into context; even the server instructions in the handshake’s InitializeResult count. mcp-scanner gives each surface an entry point: remote, stdio, config, known-configs (scans the Cursor / Claude / VS Code config files on your machine), prompts, resources, instructions, plus offline static, package scanning via pypi-scan / npm-scan, and source reading via supplychain.

Three analyzers run by default: --analyzers api,yara,llm. The api engine is Cisco AI Defense’s commercial inspect endpoint and needs a key; yara has zero dependencies and works out of the box; llm needs a model key and goes through LiteLLM — OpenAI, Bedrock, Azure or a local Ollama all work.

Nine analyzers, each owning one slice

YARA is not scanning binaries here. All 10 rule files are regexes run over tool description text. tool_poisoning.yara catches “hidden second action” phrasing: also / while processing / for caching purposes followed by collect|send|upload|log and friends. The rule comments even keep the false-positive fix on record — legitimate mail tools describe themselves as “search emails”, the broad verb list flagged all of them, so search|find was removed from that rule’s verbs. The value and the limit of this rule set are the same thing: it reads the manual, so rephrase it or write it in another language and it slips past.

The LLM analyzer’s system prompt is a single sentence — “you are a security expert, follow the provided framework” — with all the judgment living in the analysis framework inside the user prompt. The failure path is what matters: if retries still return an empty completion, it does not silently pass the tool; it records an INFO-severity infrastructure finding that writes “this leg did not run” into the report.

behavioral is the only one that truly reads code. tree-sitter builds CFGs, dataflow and cross-file call graphs across 10 languages including Python, TypeScript, Go, Java and Rust, comparing what the docstring claims against what the implementation does, with an LLM alignment check on top. The bundled evals: 141 self-authored malicious samples across 14 categories, 135 detected under azure/gpt-4.1, i.e. 95.7%. The benign side holds 3 files; what the false positives look like, the repo does not say.

readiness asks a different question: is this production-grade? 20 HEUR rules check timeouts, retries, error schemas and description quality, plus 3 Rego policies and one LLM judge pass. The Rego leg needs the opa binary on PATH; without it, policy evaluation is silently skipped.

prompt_defense works in reverse. It does not look for attacks in the description; it looks for defenses missing from it — 12 attack vectors, each checked for a corresponding protective statement. Pure regex, and the patterns include Traditional Chinese (不要假裝, 跨語言, 不可見字元). The README says it “always runs by default”; in the code it only runs when you pass --analyzers prompt_defense explicitly (scanner.py:1440). Docs and code disagree; trust the code.

The remaining three: virustotal looks up SHA256 hashes of binary files bundled with the server; vulnerable-package wraps pip-audit for dependency CVEs; meta needs an explicit --enable-meta and does exactly one job — deleting other analyzers’ false positives. It only removes, never adds, and every removal leaves a meta_analysis audit block stating which findings were dropped and why.

The part worth copying

is_safe is a three-valued property (result.py:88): findings make it False; only status == "completed" with an empty analyzer_errors makes it True; everything else is None. The aggregation buckets results into safe / unsafe / incomplete, with incomplete listed on its own. Any analyzer exception lands in analyzer_errors and drops the status to partial, and the exception message keeps only the class name — provider errors can carry URLs, headers or even the scanned input, so none of that is echoed.

The same discipline shows up in two more places. The REST API accepts only api / yara / llm / meta; requests naming other analyzers are rejected with a 422 at the boundary, because the API response shape would silently drop those analyzers’ findings — better to refuse, in the code’s words, than to let an operator believe a scan ran. The Docker-sandboxed pypi / npm scans behave the same way: unless the scan status is completed, is_safe is None, with the comment a degraded scan can’t masquerade as is_safe=True.

Where it falls short

Know before using

The zero-dependency path (yara + readiness + prompt_defense) reads description text and metadata only; it never touches the implementation. The one analyzer that reads code, behavioral, requires an LLM key, and its detection number comes from synthetic samples the project wrote itself — swap the model and you get a different number. The readiness leg is looser than the main scan: missing opa is skipped silently, an LLM judge failure is only a warning, the score is computed anyway and the status never degrades to partial. Meta costs an extra LLM round per scan and is off by default.

The repo was created on 2025-09-24; Cisco’s official blog post followed on October 23 the same year; the current version is v4.8.6 under Apache-2.0. Its relationship to Cisco AI Defense is open-source front end over commercial engine: without an AI Defense subscription you lose one of the three default engines, and the other two still work.

Verdict

Run mcp-scanner --analyzers yara,readiness before installing an MCP server: zero keys, zero cost, and it catches poisoning written plainly in the description plus a batch of production hazards. Add an LLM key when you want semantic judgment. But the thing worth copying from this project is its reporting contract: “did not finish” and “finished clean” must be two different values. In the scanner business, few projects dare to list incomplete as its own bucket.

At a glance
  • Repo: cisco-ai-defense/mcp-scanner, Apache-2.0, created 2025-09-24, current version v4.8.6
  • Analyzers: 9 (api / yara / llm / behavioral / virustotal / readiness / prompt_defense / vulnerable_package / meta), default api,yara,llm
  • YARA: 10 rule files applied to tool description text; behavioral covers 10 languages
  • Verdict: is_safe three-valued (False / True / None), aggregated into safe / unsafe / incomplete
  • Evals: 95.7% detection on 141 synthetic malicious samples (azure/gpt-4.1), 3 benign samples
  • Entry points: CLI, Python SDK, FastAPI REST, Claude Code plugin, PyInstaller single-file spec
评论Comments