Skip to main content

vulkro scan-mcp-server

Scan MCP server SOURCE CODE for prompt-injection-shaped and tool-poisoning vulnerabilities. Pure offline static analysis; no cloud LLM, and nothing about your code leaves the machine.

Three Vulkro subcommands work in the MCP space and they do different things. Make sure you are reaching for the right one:

SubcommandWhat it auditsRule family
vulkro mcp-auditMCP host CONFIGS (Claude Desktop, Cursor, Windsurf, ...)MCP-001..006
vulkro mcp serveMakes 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

FlagDescriptionDefault
<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

CodeMeaning
0No findings at or above --fail-on (or no MCP server source files discovered under <path>).
1Findings at or above the --fail-on threshold.
2Error: <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.

Command reference

Generated from vulkro help scan-mcp-server on vulkro 0.26.0. This block is the authoritative flag, usage, and exit-code reference for this command; the prose above is the friendly explanation. Do not edit this block by hand; run npm run docs:cli after a release.

Scan the source code of an MCP server for prompt-injection and tool-poisoning bugs.

Scan MCP server SOURCE CODE for prompt-injection-shaped and tool-poisoning vulnerabilities. Distinct from `vulkro mcp-audit` (which audits MCP host configs like `~/.cursor/mcp.json`) and from `vulkro mcp serve` (which makes Vulkro itself speak MCP). THIS subcommand walks a project root and scans the Python (Anthropic SDK + FastMCP) or TypeScript / JavaScript (`@modelcontextprotocol/sdk`) code that IMPLEMENTS an MCP server.

Rules: MCP-SERVER-001 tool-description injection MCP-SERVER-002 tool poisoning (caller-controlled side-effect targets) MCP-SERVER-003 rug-pull risk (descriptions that mutate at runtime) MCP-SERVER-004 sensitive sink inside tool handler MCP-SERVER-005 manifest vs handler mismatch MCP-SERVER-006 unbounded resource access MCP-SERVER-007 prompt data leakage (secrets / PII flow into tool result) MCP-SERVER-008 auth bypass on sensitive tools

Exit codes: `0` no findings (or no MCP server source files discovered under <path>). `1` findings at or above `--fail-on` threshold. `2` error: <path> does not exist, IO failure, internal parse error.

Usage: vulkro scan-mcp-server [OPTIONS] <PATH>

Arguments:
<PATH>
Path to a single source file or to a project root. When a directory is given, the scanner walks recursively and audits every Python / TS / JS file that imports an MCP SDK

Options:
-f, --format <FORMAT>
Output format

Possible values:
- table
- json
- sarif
- gh-pr
- gh-pr-inline-comments: GitHub PR per-finding **inline review comments**. NDJSON, one `{path, line, side, severity, rule_id, fingerprint, body}` object per line. Designed to be piped straight to a `gh api` loop so `vulkro gate` can drop comments next to the offending line on the Files Changed tab without going through a GitHub App. See the GitHub CLI integration guide at vulkro.com/docs
- github-annotations: GitHub Actions / GitLab CI **PR annotations**. One workflow-command line per finding (`::error file=...,line=...,endLine=...,title=<rule id>::<message> (<helpUri>)`). Printed straight to a CI job's stdout, GitHub turns each line into an inline annotation pinned to `file:line` on the Files Changed tab; GitLab CI's annotation parser reads the same grammar. When run against a baseline (`--gate-vs` / a `gate` flow) only NEW findings are annotated so a first run does not paper the PR
- gitlab-mr: GitLab Merge-Request comment: GitLab-flavored Markdown with collapsible blocks
- bitbucket-pr: Bitbucket Pull-Request comment: flat Markdown (Bitbucket does not render `<details>`)
- azure-pr: Azure DevOps Pull-Request comment: flat Markdown (shared with Bitbucket)
- junit
- csv
- cyclonedx: CycloneDX 1.6 JSON SBOM (uses `ScanResult.packages`)
- cyclonedx-1.7: CycloneDX 1.7 JSON SBOM. Same component shape as `cyclonedx`; emits the newer `specVersion`. Offered alongside 1.6 (which stays default)
- spdx: SPDX 2.3 JSON SBOM
- spdx3: SPDX 3.0.1 JSON-LD SBOM (`@context` + `@graph` of typed elements). Offered alongside SPDX 2.3, which stays the default for `--format spdx`
- cbom: CycloneDX 1.6 CBOM (Cryptographic Bill of Materials): one `cryptographic-asset` component per detected weak algorithm (MD5, SHA-1, ECB, RC4, DES, static IV, insecure RNG), with file:line occurrences inlined under `evidence.occurrences`. Compliance buyers (FedRAMP, post-quantum readiness reviews) ask for this as a distinct artefact from the library SBOM
- openvex: OpenVEX 0.2.0 exploitability statements for each dependency CVE (`affected` / `not_affected` / `under_investigation`), with `not_affected` backed by reachability analysis. Pairs with an SBOM
- cyclonedx-vex: CycloneDX 1.6 VEX: the same exploitability verdicts as `openvex`, in a CycloneDX `vulnerabilities[].analysis` document
- cbom-1.7: CycloneDX 1.7 CBOM. Same crypto-asset grouping as `cbom` plus a richer post-quantum descriptor. Offered alongside 1.6 (default)
- pdf: PDF render of the executive HTML report (requires `wkhtmltopdf` on PATH)
- ropa-md: GDPR Article 30 Records-of-Processing template - Markdown
- ropa-html: GDPR Article 30 Records-of-Processing template - HTML
- ndjson: Newline-delimited JSON: one finding per line plus a trailing summary line. Designed for SIEM ingestion and `jq` filter pipelines
- evidence-graph: Evidence-graph JSON (`evidence-graph/1.0`): a stable, versioned, AI-consumable document that composes endpoints, taint source -> sink flows, reachability verdicts, findings, and the dependency SBOM into one graph. Meant to be handed to an external AI agent as deterministic ground truth (Vulkro embeds no model; the agent brings its own). Pairs with `vulkro aggregate` for cross-repo linking. See `docs/ai-tool/`

[default: table]

--offline
Hard-disable every outbound network call for this run (sets VULKRO_OFFLINE=1). Blocks the CVE feed, the license heartbeat, the update check, webhooks, and any cloud AI endpoint; a loopback model (http://127.0.0.1, http://localhost) is still allowed. Equivalent to exporting VULKRO_OFFLINE=1, and the flag wins when both are set

--fail-on <SEVERITIES>
Severities that should produce a non-zero exit (comma list). Default `critical,high`. Same shape as `vulkro scan --fail-on`

[default: critical,high]

-h, --help
Print help (see a summary with '-h')