vulkro scan-mcp-server
Scan MCP server SOURCE CODE for prompt-injection-shaped and tool-poisoning vulnerabilities. Pure offline static analysis; no cloud LLM, no telemetry.
Three Vulkro subcommands work in the MCP space and they do different things. Make sure you are reaching for the right one:
| Subcommand | What it audits | Rule family |
|---|---|---|
vulkro mcp-audit | MCP host CONFIGS (Claude Desktop, Cursor, Windsurf, ...) | MCP-001..006 |
vulkro mcp serve | Makes Vulkro itself BE an MCP server | (transport) |
vulkro scan-mcp-server (this page) | MCP server SOURCE CODE (the Python / TS code that IMPLEMENTS an MCP server) | MCP-SERVER-001..008 |
Usage
vulkro scan-mcp-server src/ # scan a project root recursively
vulkro scan-mcp-server server.py # scan a single file
vulkro scan-mcp-server src/ --format json # machine-readable output
vulkro scan-mcp-server src/ --format sarif > findings.sarif
Both Python (mcp.server, FastMCP) and TypeScript / JavaScript
(@modelcontextprotocol/sdk) servers are supported in v1. Other
language SDKs (Rust, Go, C#, Swift) will follow as the official
MCP server-side SDKs in those ecosystems mature.
Flags
| Flag | Description | Default |
|---|---|---|
<path> | Path to a single source file or to a project root. Required positional argument. | (none) |
--format <FORMAT> | One of table (human), json, sarif, ndjson. | table |
--fail-on <SEVERITIES> | Comma list of severities that cause a non-zero exit. Same shape as vulkro scan --fail-on. | critical,high |
Exit codes
| Code | Meaning |
|---|---|
0 | No findings at or above --fail-on (or no MCP server source files discovered under <path>). |
1 | Findings at or above the --fail-on threshold. |
2 | Error: <path> does not exist, IO failure, or internal parse error. |
Detectors
Eight detectors run by default. Each emits a stable rule ID prefix in its finding message so SIEM / SARIF deduplication keys stay constant across releases.
MCP-SERVER-001 tool-description injection
Fires when a tool description (or its inputSchema) is built from
non-literal input. The LLM reads the description as canonical tool
documentation; an attacker who controls the interpolated value can
hide instructions inside the description text.
Triggers on f-strings, .format(...), string concatenation
(Python), template literals with ${...} placeholders, and string
concatenation in the description argument of server.tool(...)
(TypeScript / JavaScript).
Stays quiet when the description is a string literal.
MCP-SERVER-002 tool poisoning
Fires when a tool handler takes a caller-supplied file path,
URL, env-var name, or command and passes it directly into a
sensitive sink WITHOUT an allowlist or validation step. Classic
shape: def read_file(path: str): return open(path).read().
Suppression markers include explicit allow-list set membership
tests, pathlib.Path(p).resolve() plus a startswith check,
urlparse plus a host check, pydantic field validators, and
zod.parse / similar shapes in TypeScript.
MCP-SERVER-003 rug-pull risk
Fires when a tools/list handler returns tool descriptions
computed at request time (from a global, a function call, or a
lookup). Each tools/list call can return different copy: this is
the "rug pull" shape where the server advertises one description
at install time and swaps it once the client trusts it.
MCP-SERVER-004 sensitive sink in tool handler
Fires when a tool handler body contains a subprocess call, eval,
raw SQL with interpolation, or another sensitive sink. Even when
the target is constant, the surface is wide because MCP tools run
with the server process's full capability.
Emitted at Medium severity by default; bump --fail-on to include
medium if you want non-zero exits to gate on these.
MCP-SERVER-005 manifest vs handler mismatch
Fires when the advertised inputSchema does not match the handler
signature: either the handler accepts a parameter the schema does
not declare, or the schema declares a property the handler ignores.
The first shape is a covert capability surface; the second is a
silent capability drift between docs and behaviour.
MCP-SERVER-006 unbounded resource access
Fires when an MCP server is mounted at an overbroad filesystem
root (/, /home, ~, $HOME) or when a tool performs outbound
fetches with no allowed-host check or rate limit anywhere in the
file. The first is a catastrophic disk-scope misconfiguration; the
second is an SSRF / data-exfil surface.
MCP-SERVER-007 prompt data leakage
Fires when a tool handler returns env-var-shaped secrets, sensitive
identifier-shaped variables (password, api_key, ssn,
credit_card, ...), or a literal matching a real provider's
credential format (Stripe sk_live_, GitHub ghp_, AWS AKIA...,
Slack xox[bpoa]-...). Tool results land in the model's prompt
context and may be logged, displayed, or relayed by the MCP client.
MCP-SERVER-008 auth bypass on sensitive tools
Fires when a tool whose name or description advertises a
destructive capability (delete, drop, purge, admin,
exec, transfer, rotate_key, ...) has no auth check at handler
entry. The MCP runtime treats every tool as freely callable; a
destructive tool must gate itself.
Examples
Scan a FastMCP server directory and emit SARIF for GitHub Code Scanning:
vulkro scan-mcp-server ./fastmcp-server --format sarif > vulkro-mcp.sarif
Run as a pre-commit step that fails on any new MCP-SERVER finding:
vulkro scan-mcp-server src/ --fail-on critical,high,medium
Hook into CI: the JSON output is the standard Vulkro ScanResult
shape, so existing tooling that parses vulkro scan -f json works
unchanged.
Limitations
This is static analysis. Detectors err toward false negatives over
false positives so they do not bark on a well-built server. The
intended workflow is to run alongside vulkro scan: that catches
the rest of the OWASP API + LLM Top 10 surface, and
scan-mcp-server adds the MCP-specific axis.