DEV Community

Zayd Mulani
Zayd Mulani

Posted on

I built a static + runtime security scanner for MCP servers (Rust)

I built a static + runtime security scanner for MCP servers (Rust)

TL;DR: MCP (Model Context Protocol) has had 40+ CVEs disclosed just this year, and there wasn't a purpose-built scanner for it, so I built one. sentrymcp does static analysis (path traversal, injection, tool poisoning, missing auth) plus a runtime proxy mode that catches "rug pulls" — servers that silently change a tool's description after you've already approved it. Rust, MIT, Docker one-liner. Repo: https://github.com/zaydmulani09/sentrymcp

Why this exists

I've been building MCP tooling for a while (a reverse proxy, some agent infra), and at some point I went looking for something to check my own servers for basic security mistakes before shipping them. There wasn't really anything.

That surprised me, because the CVE numbers aren't small. Endor Labs analyzed over 2,600 real MCP implementations and found 82% use file operations prone to path traversal, 67% use APIs related to code injection, and 34% use APIs susceptible to command injection. Separately, researchers have put the no-auth rate at around 38-40% of scanned servers — meaning close to 2 in 5 MCP servers in the wild have no authentication at all.

The vulnerability classes aren't exotic either. Anthropic's own reference implementation, mcp-server-git, shipped a path traversal bug where a repo_path argument was never validated against a configured boundary — the exact "developer forgot one check" pattern that shows up constantly in these audits.

So: static analysis for the code-level stuff, and since MCP has an attack class that literally can't exist in source code (a server that changes its tool description mid-session, after the user already approved the original), a runtime mode to catch that too.

What it catches

Category What it detects Example
Code vulnerabilities Path traversal, command/shell injection, unsafe eval/code-injection sinks Unvalidated file path args, string-interpolated exec() calls
Tool poisoning Hidden instructions embedded in tool descriptions, unicode/homoglyph obfuscation "ignore previous instructions" phrasing, Cyrillic lookalike characters hiding in description text
Auth & permissions Missing auth on HTTP/SSE transports, hardcoded credentials, credentials leaked via logging api_key = "sk-..." as a literal, unauthenticated SSE endpoints
Runtime (proxy mode) Tool description changes after initial approval ("rug pulls"), unexpected outbound connections A tool's description silently changes between the first and second tools/list call in a session

The tool-poisoning detection is the one I'm most interested in feedback on. MCP tool descriptions get read directly into the model's context as trusted content, so an attacker who controls a description can hide instructions the LLM will act on while the user only sees a benign-looking label. sentrymcp checks for imperative hidden-instruction phrasing, zero-width/bidi-control unicode, and homoglyph mixing (Latin text with Cyrillic or Greek lookalike characters slipped in).

Usage

Static scan:

mcpaudit scan ./some-mcp-server
Enter fullscreen mode Exit fullscreen mode

Output is ranked by severity with a CWE or OWASP MCP Top 10 reference and a one-line remediation for each finding, split into three sections — code vulnerabilities, tool poisoning, and auth/permissions.

Runtime proxy mode, sitting between your MCP client and the real server:

sentrymcp proxy -- <your-server-command>
Enter fullscreen mode Exit fullscreen mode

This baselines every tool definition it sees on session start and diffs every later tools/list response against it. Any change gets flagged and logged as a JSON event, with a human-readable summary at session end.

Docker, no local Rust toolchain needed:

docker build -t sentrymcp .
docker run --rm -v $(pwd):/scan sentrymcp scan /scan
Enter fullscreen mode Exit fullscreen mode

How it's built

Rust workspace, four crates: a core scan engine, a rules crate that loads detection patterns from TOML files (so adding a new check doesn't require recompiling), a CLI, and the proxy. Rule-driven design was a deliberate choice — path traversal, injection, and tool-poisoning heuristics are all just declarative pattern definitions with a severity, a reference (CWE or OWASP MCP Top 10 id), and remediation text, which makes the ruleset easy to extend as new attack patterns show up.

Limitations (being upfront about these)

  • Detection is regex/pattern-based, not AST or taint-tracking. It'll miss vulnerabilities that flow through an intermediate variable before hitting a sink — this is a known, documented gap, and proper taint analysis is the natural next step.
  • Description extraction for tool-poisoning checks is targeted, not a full parser — it handles the common registration patterns in Python/TS/JS but isn't exhaustive.
  • The runtime proxy's network-connection monitoring is currently Windows-only and best-effort (polls netstat, not a real sandbox).
  • Heuristic findings (like "no visible auth pattern anywhere in the file") are marked with a confidence flag separate from severity, since absence-of-pattern checks are inherently noisier than presence-based ones.

Try it / feedback wanted

Repo's here: https://github.com/zaydmulani09/sentrymcp — MIT licensed, contributions welcome. If you run MCP servers, I'd genuinely appreciate people trying the scanner against their own and telling me what it misses or false-positives on. The corpus of test cases is still small and real-world feedback is the fastest way to make the ruleset actually useful instead of just theoretically correct.

Top comments (0)