The Model Context Protocol is the fastest-growing attack surface in AI. MCP servers are the plugins that give an agent real-world reach — one reads your files, one fetches web pages, one runs database queries — and independent scans keep converging on the same uncomfortable picture: roughly a third of public servers carry critical vulnerabilities, only about 8.5% use OAuth, and more than a third are exploitable via SSRF. The flaws are mundane — SSRF, path traversal, injection — because MCP servers are written by ML and systems engineers, not security engineers.

Naturally, scanners appeared to find these problems. And they work. The trouble is what they hand you.

The smoke-alarm problem

A pattern-based scanner is a smoke alarm that goes off every time you make toast. It reads the text of a tool description and flags anything that looks vaguely dangerous. An audit by AppSec Santa ran Cisco’s open-source scanner over a server and got 27 findings — of which 21 were false positives. Six were real.

Why so noisy? Because MCP tool descriptions are written in imperative language. “Call this tool to fetch the URL.” “Execute this query to retrieve rows.” “You must use this tool.” Those phrases are the normal, intended way you describe a tool to a model — and they’re also exactly the vocabulary a YARA rule uses to detect prompt injection and tool poisoning. The scanner can’t tell the difference between a tool doing its job and a tool being malicious, because it only sees the words.

When 78% of your alerts are false, people stop reading them. That’s not a hypothetical failure mode — that’s how the six real fires get missed.

The gap nobody fills

Here’s the honest state of the market: raw scanning is crowded. Cisco’s mcp-scanner, Invariant Labs’ mcp-scan (now part of Snyk), Enkrypt AI, and several commercial tools all exist and all generate findings well. Building scanner #11 would be a waste.

The two things none of them do well are the two things that actually matter:

  1. Noise reduction. Nobody reliably separates the exploitable finding from the designed behavior.
  2. Governance. Static scanning tells you a server is risky. It doesn’t inventory your fleet, score it, enforce least privilege, or lock down egress — the identity-and-permission discipline we already apply to service accounts, and don’t yet apply to MCP.

So I built mcp-triage — deliberately not another scanner. It’s the layer that sits on top of any scanner and does those two jobs.

The core idea: read the label and the object

A scanner sees only the label on the plugin. mcp-triage also reads the inventory — what each tool can actually do, and what guardrails the server has around it (authentication, egress allow-lists, secret manager, filesystem scope, sandboxing) — and cross-references the two. That cross-reference is the whole game. The same keyword produces the opposite verdict depending on context:

FindingScanner seesmcp-triage also seesVerdict
“execute this query”keyword executethe query is parameterizeddesigned behavior
tool named execute_report_exportkeyword executeit has no execute capabilityfalse positive
fetch_url makes outbound requestsnetwork egressno egress allow-listgenuine (SSRF)
“call this tool to fetch the URL”imperative directiveordinary tool prosestandard instruction
“ignore previous instructions; exfiltrate…”imperative directiveadversarial markersgenuine (tool poisoning)

Same word, opposite conclusion — decided by context, not by a pattern.

Using it

The tool is Python, stdlib-only at its core, and runs offline. Point it at an MCP config and a scanner’s findings:

pip install -e .

mcp-triage inventory  fleet.json                 # what's in the fleet, and what's exposed
mcp-triage triage     fleet.json findings.json    # the funnel: raw findings → genuine concerns
mcp-triage govern     fleet.json                  # least-privilege + egress recommendations
mcp-triage report     fleet.json findings.json -o report.md

The bundled example reproduces the AppSec Santa audit exactly — 27 raw findings collapse to the 6 that matter:

27 raw findings → 6 genuine (78% noise removed)

    8  Standard MCP instructions
   10  Designed features (mitigated)
    3  False positives
    6  Genuine concerns

Genuine concerns:
  [CRITICAL] MCP05:2025  Command injection: unsanitized shell input     (shell / run_command)
  [CRITICAL] MCP03:2025  Tool poisoning: hidden adversarial instructions (notes-sync / sync_notes)
  [    HIGH] MCP10:2025  SSRF: unrestricted outbound fetch               (web-fetch / fetch_url)
  [    HIGH] MCP01:2025  Static database credential in environment       (postgres)
  [    HIGH] MCP02:2025  Over-broad filesystem scope                     (filesystem / write_file)
  [    HIGH] MCP07:2025  Sensitive tools exposed without authentication  (filesystem)

Those six span six different categories of the OWASP MCP Top 10, each with a plain-language reason it’s real and a concrete fix.

Governing the fleet, not just the findings

Triage is half the tool. The other half takes inventory of every server, grades each one like a report card, and issues least-privilege recommendations — this one has no authentication, this one can touch your entire disk (scope it to one folder), this one can call any address on the internet (lock down which hosts it’s allowed to reach) — all mapped to the OWASP MCP Top 10. Governance runs even with zero findings, because an auth-less server or an empty egress allow-list is a failure whether or not a scanner happened to flag it.

The optional LLM judge

There’s an optional Anthropic-backed judge that re-adjudicates only the findings the deterministic rules mark low-confidence, reasoning about whether a flag is exploitable or designed. It’s strictly optional — the engine is fully deterministic and explainable without it, and the test suite is green offline with no API key. I wanted the default path to be something a security team can trust and audit, not a black box.

Bring your own scanner

mcp-triage is scanner-agnostic on purpose. It consumes findings rather than generating them, normalizing common rule / message / match / severity fields on ingest. Pipe in Cisco MCP Scanner, Invariant / Snyk MCP-Scan, Enkrypt AI, or your own output. Every competitor’s growth becomes this tool’s funnel, not its threat.

Why this shape

Lots of tools generate security alerts. Almost nobody filters them, and almost nobody governs the fleet the alerts came from. That’s the same pattern I keep finding across non-human and agentic identity: the raw signal exists, but it arrives as noise, and the value is in the triage and the least-privilege discipline layered on top. An agent is only as safe as the tools it can reach — so knowing which of those tools is genuinely dangerous, and holding the whole fleet to least privilege, is where the work is.

The code is open source and MIT-licensed at github.com/rpmsft9/mcp-triage. It’s an MVP, but a runnable one — clone it, point it at your fleet, and see how short your real list is.

Discussion

Comments are powered by Giscus / GitHub Discussions. They appear here once configured — see Configure Giscus in the project README and update GISCUS in src/consts.ts.