vulkro mcp serve
Run Vulkro as a Model Context Protocol server. Once configured, Claude Code, Claude Desktop, Cursor, Windsurf, Continue, VS Code's MCP client, and any other JSON-RPC 2.0 MCP client can call the scanner directly from inside the editor: tell the model "scan this project" and it will invoke Vulkro, parse the findings JSON, and walk through them inline.
The sibling subcommand vulkro mcp-audit audits MCP
host configs (kept at its original top-level path for backwards
compatibility) and is unrelated to mcp serve.
Usage
vulkro mcp serve # stdio transport (default)
vulkro mcp serve --port 9100 # SSE transport on http://127.0.0.1:9100/sse
Flags
| Flag | Description | Default |
|---|---|---|
--port <PORT> | When set, binds an HTTP listener on 127.0.0.1:<PORT> and exposes /sse (event stream) and /messages (POST endpoint for JSON-RPC requests). When omitted, the default stdio transport is used. | (omitted, stdio) |
--offline | Hard-disable every outbound network call for this run (sets VULKRO_OFFLINE=1). The flag wins when both are set. | off |
Exit codes
| Code | Meaning |
|---|---|
0 | Clean shutdown. The host closed stdin (stdio mode) or Ctrl-C was hit (SSE mode). |
1 | The server started but encountered a feature refusal on a tool call that the caller treated as fatal. |
2 | Operational error: bind failure, transport-level IO error, bad argument to mcp serve. |
Tools exposed
Sixteen tools are surfaced over JSON-RPC. Schemas are advertised via the
standard tools/list method; the per-tool input shapes below match what
hosts render in the tool picker. Every tool is deterministic and calls no
model.
| Tool | What it does | Tier |
|---|---|---|
scan_project | Full project scan, returns a scan_id | Free |
scan_file | Findings for one source file | Free |
get_findings | Re-filter a prior scan by severity | Free |
prove | The hop-by-hop proof chain for one finding | Free |
explain | Markdown explainer for a rule id | Free |
list_rules | The rule catalogue | Free |
suggest_fixes | Fix suggestions and remedies for a prior scan | Free |
verify_fix | Check a proposed diff on a temporary copy | Free |
verify_sarif | Check another tool's SARIF results against Vulkro's proof | Free |
inspect_repo | Malicious-capability review of a just-cloned repo | Free |
scan_diff | Findings on the lines a git diff touched | Pro (changes) |
assess_change | Blast radius, tests and new findings for a change | Pro (changes) |
code_graph | Scan-free ranked code map and blast radius | Pro (code-structure) |
from_trace | Map a stack trace onto code and findings | Pro (code-structure) |
evidence_graph | The evidence-graph/1.0 document for a repo | Pro (evidence-formats) |
aggregate | Link evidence graphs across repos | Pro (portfolio) |
Every language is Free on every tool. A Free-tier call to a Pro tool
does not stop the server: the call fails with JSON-RPC error -32001
(License required) and a data object carrying reason: "pro_required", the capability id from the table above, and a message
that names the capability and how to unlock it. The session stays up and
every Free tool keeps working. See Accounts.
scan_project
scan_project({path, format?, offset?, limit?})
Run a full Vulkro scan on the project root at path. The result carries a
scan_id that get_findings, suggest_fixes, prove and verify_fix can
use without re-scanning.
For an AI agent, pass format: "compact". It returns the findings as one
compact table (TOON text: the field names once, then one row per finding),
most severe first, each row carrying the flagged line of code, with as many
rows as fit in about 10,000 tokens. page.next_offset says where the next
page starts. The default json is the full ScanResult (the same shape
vulkro scan -f json emits), which is often larger than an agent host accepts
as one tool result.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the project root. |
format | "compact" | "json" | "summary" | no | compact returns the agent-sized findings table (recommended for agents); json (default) returns the full ScanResult; summary returns counts only. |
offset | integer | no | compact only: the first row to return, from the previous page's page.next_offset. |
limit | integer | no | compact only: the most rows on a page. By default a page holds as many rows as fit in about 10,000 tokens. |
A compact row has finding_id, severity, confidence, disposition,
rule, cwe, file, line, message and code (the flagged line).
scan_file
scan_file({path})
Detect the project containing path, run the scanner, and return only
the findings whose source file matches. The response carries a gate
block: gating is how many of these rows may fail a build, and
withheld_not_examined is how many carry "disposition":"not-examined"
(a class Vulkro flags structurally but cannot decide offline, such as
object-level and function-level authorization). Withheld rows are
reported, not cleared, and must not be used to fail a build.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to a source file. |
get_findings
get_findings({scan_id, severity?, finding_ids?, format?, offset?, limit?})
Re-read findings from a prior scan_project call using the returned
scan_id. Filter by severity, or pass the finding_id values from compact
rows to get the full record (evidence, code snippet, trace, remediation) of
just those findings.
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | yes | A scan_id returned by a prior scan_project call in the same MCP session. |
severity | "critical" | "high" | "medium" | "low" | "info" | no | When set, return only findings at this severity. Case-insensitive. |
finding_ids | string array | no | When set, return only these findings. |
format | "json" | "compact" | no | json (default) returns full records; compact returns the compact table. |
offset, limit | integer | no | compact only: paging, as for scan_project. |
prove
prove({finding_id? | file?, line?, scan_id? | path?})
Return the deterministic, hop-by-hop proof chain Vulkro computed for one
finding, so the agent can check a reported issue against evidence rather
than a bare claim. The payload carries the ordered taint, sanitizer-gap
and related-context hops (each with file, line, a phrase and the captured
source line), the reachability verdict, the exploitability grade, the
supporting evidence rows, whether the finding's class has a runnable
proof shape, and the data flows that touch the finding's file. Same
evidence as vulkro prove.
An empty proof chain means no proven data flow was found for this
finding. It never means the code is safe: proven: false is
"unproven", not "clean".
| Field | Type | Required | Description |
|---|---|---|---|
finding_id | string | one of finding_id / file | The finding's stable, content-addressed id (<rule>:<fingerprint>). Preferred: it is deterministic across runs. |
file | string | one of finding_id / file | Locate the finding by source file. Ignored when finding_id is set. |
line | integer | no | 1-based line to disambiguate when several findings share file. |
scan_id | string | no | Reuse a scan from a prior scan_project or scan_diff call in this session. |
path | string | unless scan_id is set | Absolute path to the project root, to scan fresh. |
explain
explain({rule_id})
Return a markdown explainer for the named rule. Accepts:
- OWASP-category slugs (
api1-broken-object-level-auth, ...) - MCP-audit rules (
MCP-001...MCP-006) - Extension-audit rules (
EXT-001...EXT-003) - Supply-chain compromise families
(
SUP-COMPROMISE-001...SUP-COMPROMISE-005)
Same content the CLI vulkro explain <ID> produces, rendered as
portable markdown (no ANSI).
list_rules
list_rules()
Enumerate every rule Vulkro knows about. Returns
{ id, title, owasp_category, owasp_category_display, severity, description, rule_page } for each. The OWASP catalogue, MCP-audit
rules, extension-audit rules, and supply-chain compromise families
are all included in one call.
suggest_fixes
suggest_fixes({scan_id, finding_id?, rule?, verify?})
Return git-apply-ready fix suggestions for a prior scan_project call.
This closes the fix loop inside MCP: scan_project -> suggest_fixes ->
the agent applies the diff -> scan_project again to confirm the
finding is gone. Same envelope as vulkro fix --format json: each fix
carries finding_id, rule, file/line, a prose explanation, and a
unified diff (patch_format: unified-diff).
An additive remedies array covers the knowledge-base classes (SQL
injection in the driver's bound-parameter form, command injection, path
traversal, open redirect, XSS, weak random, cookies, CORS, TLS and JWT
verification, missing authentication; SSRF and mass assignment as advice)
for JavaScript, TypeScript, Python, Go and Java. Each remedy has steps, a
before / after example and, when the code shape is recognised, a patch
whose verdict comes from applying it to a temporary copy of the project
and scanning again: fixed, not-fixed or regressed (unverified
when verify is false). Vulkro never applies a diff to your working
tree.
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | yes | A scan_id returned by a prior scan_project call in the same MCP session. |
finding_id | string | no | Only this finding (its id or stable finding_id). |
rule | string | no | Only findings of this rule id (for example js-taint-sql-001). |
verify | boolean | no | Re-scan a patched temporary copy to give each remedy patch a verdict. Default true. |
verify_fix
verify_fix({diff, path? | scan_id?, finding_id?})
Re-check a fix the agent proposes before it is applied. Vulkro applies
the unified diff (as git diff writes it; one or more files; each hunk is
located by its context, so positions may be approximate) to a temporary
copy of the project, scans the copy and a fresh copy of the original,
and returns a verdict:
fixed: the target finding is gone and no new Medium-or-higher finding appeared in the changed lines.not-fixed: the re-scan still reports it.regressed: the change raised a new Medium-or-higher finding in the lines it changed, or the patched file no longer parses.
It also returns the cleared and remaining finding ids and any new
findings. The working tree is never modified. Without finding_id,
every finding in the touched files is a target.
| Field | Type | Required | Description |
|---|---|---|---|
diff | string | yes | The unified diff, with --- a/<file> / +++ b/<file> headers relative to the project root. |
path | string | unless scan_id is set | Absolute path to the project root. |
scan_id | string | no | A scan_id from a prior scan_project call; its project root is used when path is absent. |
finding_id | string | no | The finding the diff is meant to fix (its id or stable finding_id). |
verify_sarif
verify_sarif({sarif, path})
Check another scanner's SARIF 2.1.0 results (any SARIF-producing tool, including an AI security review that writes SARIF) against Vulkro's deterministic proof. Each result is mapped to a vulnerability family (CWE first, then rule id, then message), the repository is scanned once, and every result gets a verdict:
proven: a Vulkro finding of the same family at or within 3 lines of the result carries machine-checkable proof. The proof chain, entry point and the risk model's actor, asset and severity are attached.not-provable: Vulkro analysed the location and holds no proof;reasonsays what it found instead (a heuristic-only finding, an adequate guard, a parameterised query, no recognised source, no flow).not-examined: outside this build's coverage (unsupported language, skipped or missing file, unmapped family).
Returns the vulkro-verify/1 report. not-provable is not a
false-positive verdict, and not-examined says nothing about the result.
| Field | Type | Required | Description |
|---|---|---|---|
sarif | string | yes | Absolute path to the SARIF 2.1.0 file to verify. |
path | string | yes | Absolute path to the repository the results were produced on. Relative result paths resolve against it. |
inspect_repo
inspect_repo({path})
Vulkro Inspect as a pre-run check for a just-cloned repository: call it before running any of the repo's code (install scripts, build steps, examples). It surfaces malicious-capability shapes (reverse shells and instance-metadata grabs, credential reads and exfiltration, install and build hooks, obfuscated or packed source, trojan-source text, logic-bomb environment gates, worm behaviour, wallet access, remote code load, persistence, committed opaque binaries, dependency-integrity anomalies, and prompt-injection payloads aimed at an AI agent), grouped by capability, with the file and line of each site.
It runs over the full unfiltered finding set (no confidence floor), so a
low-confidence shape is never dropped. It is a human-review surface, not
a gate: it never certifies code safe, clean, or trusted, returns no
proceed / allow / approve verdict, and every response carries a
top-level disclaimer.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the just-cloned repository root. |
scan_diff
scan_diff({path, base_ref?}). Pro: changes.
Verify your own change: scan the project, then return only the findings
on the lines the diff added or modified against a git ref (the same
changed-line scoping vulkro scan --gate-vs <ref> uses). It does not
compare two scans. The whole tree is analysed so cross-file taint stays
coherent, then findings are narrowed to the changed lines, so the agent
sees what its edit introduced rather than the existing backlog.
Returns the standard ScanResult shape (findings and stats recomputed for
the diff scope), a scan_id for get_findings, suggest_fixes and
prove, and a diff block with the base ref, the changed files, and how
many findings fell inside and outside the diff. An empty result means no
new issue was found on the changed lines, not that the change is safe:
findings on unchanged lines are deliberately hidden here.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the git project root. |
base_ref | string | no | Git ref to diff against (main, origin/main, a SHA). The diff is <base_ref>...HEAD. Default HEAD~1, the change introduced by the most recent commit, so commit the edit first or pass a ref. |
assess_change
assess_change({path, symbol?, base?}). Pro: changes.
Assess whether a change is safe and non-breaking. It fuses the structural code graph with a full security scan, and is read-only.
- Before the change: pass
symbolto get its blast radius (the transitive set of symbols that depend on it), which tests to run, and whether it already carries findings or sits on a taint path. - After the change: omit
symbolto assess the diff againstbase. It returns the dependents and tests the change touches plus the findings on the changed lines, with a "safe to proceed" or "review N" verdict.
Structural impact is a floor: unresolved calls are not counted.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the project root. |
symbol | string | no | Before-change mode: the symbol about to be edited, as file::name or a bare name. Omit for after-change (diff) mode. |
base | string | no | After-change mode: git ref to diff against. Default HEAD (the uncommitted working-tree change); for example HEAD~1 or origin/main. |
code_graph
code_graph({path, mode?, symbol?, query?, substring?, context?, offset?, max_symbols?, max_tokens?, format?}).
Pro: code-structure.
A ranked, symbol-level code graph for a project, built without running a
security scan and cached between calls, so it is fast. Use it to navigate
code and to see the blast radius of a change before editing. The same modes
are available from the command line as vulkro code-graph.
mode: "map"(default): the most important symbols, ranked. Use it to get oriented in an unfamiliar codebase.mode: "search"withquery: every place a name is defined, called, referenced (mocks, values) or imported, with the code lines and the enclosing function, production files before tests, paged withoffset. It also finds calls the graph could not resolve. Use it instead of grep plus file reads.mode: "body"withsymbol: the source of that one function, with line numbers, its callees and its callers, instead of its whole file.mode: "impact": the transitive set of symbols that reachsymbol(its blast radius), split into tested and untested.mode: "callers"/mode: "callees": the direct neighbours ofsymbol.
Pass format: "toon" to get the same content as compact text (lists as
tables), which is smaller than JSON.
This is code structure only: it asserts nothing about vulnerabilities,
and every reach or impact count is a floor (unresolved calls are
disclosed in the honesty header). For the security view of a change,
use scan_diff.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the project root. |
mode | "map" | "impact" | "callers" | "callees" | "search" | "body" | no | Query type. Default map. |
symbol | string | for impact, callers, callees and body | Target symbol, as file::name or a bare name. A bare method name also finds Class.method. |
query | string | for search | The name to find (default: the name part of symbol). Matched as a whole identifier when it is one. |
substring | boolean | no | search: match the query anywhere, not only as a whole identifier. |
context | integer | no | search: lines of context around each hit. Default 1, at most 5. |
offset | integer | no | search: the first hit to return, from the previous page's page.next_offset. |
max_symbols | integer | no | Maximum rows to return (in search, hits per page). Default 40; the output reports the true total and how to get the rest. |
max_tokens | integer | no | Hard token ceiling; the lowest-ranked rows are trimmed to fit. |
format | "json" | "toon" | no | json (default) or toon, the same content as compact text. |
from_trace
from_trace({path, trace}). Pro: code-structure.
Map a production stack trace onto the code graph for crash triage. Each
file:line frame is resolved to its enclosing symbol, with that symbol's
findings and blast radius, so the agent knows which frame to start at.
Fuses the code graph with a scan; read-only.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the project root. |
trace | string | yes | The stack trace text, in any common language format; file:line frames are extracted. |
evidence_graph
evidence_graph({path, repo_name?}). Pro: evidence-formats.
Scan a project root and return its evidence-graph/1.0 document: the same
shape vulkro scan <repo> --format evidence-graph
emits. This is a stable, versioned, model-free view of the repo's attack
surface: endpoints with route shape and auth tier, outbound calls, taint
flows, findings with reachability verdicts, and dependency evidence. Feed
the output to aggregate for cross-repo linking.
| Field | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to the project root to scan. |
repo_name | string | no | Label for this repo in the graph. Defaults to the detected project name. Set it when you plan to aggregate several repos and want stable, human-readable node names. |
aggregate
aggregate({graphs}). Pro: portfolio.
Link two or more evidence-graph documents to surface cross-repo edges (a
caller in one repo hitting an endpoint in another, especially
unauthenticated targets). Same linker as vulkro aggregate.
Pass the raw evidence-graph JSON objects inline (one per repo), each
produced by the evidence_graph tool.
| Field | Type | Required | Description |
|---|---|---|---|
graphs | array of objects | yes | Two or more evidence-graph/1.0 JSON documents, one per repo, each as returned by the evidence_graph tool. |
Resources: the local AI memory
Besides tools, the server exposes the advisory AI memory store (the
records under .vulkro/ai/ in the nearest ancestor of the server's
working directory that has one) as read-only MCP resources, so a host can
read prior triage, fix and hunt interactions. resources/list returns up
to 200 records, newest first, as vulkro-ai-memory:///<id> URIs, and
resources/read returns one record. Listing never creates the store, and
nothing in it can change a finding, a severity or an exit code.
Workflows
Verify a change before you commit it
scan_project({path, format: "compact"})once, to get the findings and ascan_id.- After the edit,
scan_diff({path, base_ref})(Pro) to see only what the change introduced. prove({scan_id, finding_id})on anything reported, to read the evidence before acting on it.suggest_fixes({scan_id}), thenverify_fix({diff, scan_id, finding_id})on the proposed patch, before the agent applies it.
AI-usable evidence-graph workflow
evidence_graph and aggregate (both Pro) compose into a deterministic,
model-free pipeline an agent can drive end to end without leaving MCP:
- Call
evidence_graph({path})once per repo. Each call returns a versionedevidence-graph/1.0document (endpoints, outbound calls, taint flows, findings, dependencies). - Collect those documents and pass them to
aggregate({graphs}). aggregatereturns candidate cross-repo links (route-shape and method matches, deterministic) with the callee auth posture attached.
Vulkro supplies the evidence; the agent brings its own reasoning about whether a linked flow is a real risk. Nothing in this path calls a model, so the inputs an agent reasons over are reproducible.
Set up in Claude Code, Cursor and Codex
Claude Code
Register the server once:
claude mcp add vulkro -- vulkro mcp serve
Optionally add the Vulkro skill too, which teaches Claude Code when and how to call the CLI (see Claude Code skill):
curl -fsSL https://dist.vulkro.com/skill-install.sh | bash -s -- --agent claude-code
Cursor
Add the server to ~/.cursor/mcp.json:
{
"mcpServers": {
"vulkro": {
"command": "vulkro",
"args": ["mcp", "serve"]
}
}
}
Restart Cursor. The skill installer's --agent cursor option also writes
a Vulkro rules file to ~/.cursor/rules/vulkro.md.
Codex
Codex uses the Vulkro skill rather than the MCP server: the installer
appends the Vulkro instructions to ~/.codex/AGENTS.md (it never
overwrites the file), and Codex then runs the vulkro CLI directly.
curl -fsSL https://dist.vulkro.com/skill-install.sh | bash -s -- --agent codex
VS Code agent mode
The VS Code extension registers this
server for the editor's agent automatically, launched with
VULKRO_OFFLINE=1, and adds its own language-model tools. No manual
config is needed.
Other MCP hosts
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS), ~/.config/Claude/claude_desktop_config.json (Linux), or
%APPDATA%\Claude\claude_desktop_config.json (Windows) and add a
mcpServers.vulkro entry:
{
"mcpServers": {
"vulkro": {
"command": "vulkro",
"args": ["mcp", "serve"]
}
}
}
Restart Claude Desktop. The tool picker should now list the Vulkro
tools (scan_project, scan_file, prove, and the rest). Ask the
assistant "scan this project" with a project directory open and the
model will call scan_project with the path.
Windsurf
Edit ~/.windsurf/mcp.json (or ~/.codeium/windsurf/mcp_config.json
on older versions):
{
"mcpServers": {
"vulkro": {
"command": "vulkro",
"args": ["mcp", "serve"]
}
}
}
Restart Windsurf.
Continue (VS Code)
Continue reads MCP server config from ~/.continue/mcp.json. The
shape is identical:
{
"mcpServers": {
"vulkro": {
"command": "vulkro",
"args": ["mcp", "serve"]
}
}
}
Reload the Continue extension.
VS Code MCP client
The VS Code MCP client reads from ~/.vscode/mcp_settings.json (or
~/.vscode-server/mcp_settings.json in Remote setups). Same shape.
Transport details
Stdio is the default because every major MCP host launches its
servers as child processes and talks JSON-RPC over the standard
streams. The server reads one JSON-RPC request per line on stdin
and writes one response per line on stdout; stderr is reserved for
the optional VULKRO_MCP_LOG=1 trace stream.
The SSE transport (opt-in via --port) binds an HTTP listener on
127.0.0.1:<port> only (never 0.0.0.0). Clients POST JSON-RPC
requests to /messages and read responses from a long-lived
GET /sse stream. The server emits an event: endpoint SSE message
on connection with the POST endpoint path; subsequent responses
arrive as event: message payloads.
Environment variables
| Name | Effect |
|---|---|
VULKRO_MCP_LOG=1 | Emit per-request trace lines on stderr. Off by default so the stdio JSON-RPC stream stays clean of side-band output. |
VULKRO_OFFLINE=1 | Disable any outbound HTTP call during scans triggered through the MCP surface. Same semantics as the top-level scan subcommand. |
Other VULKRO_* variables honoured by vulkro scan (e.g.
VULKRO_QUIET_PRESET_HINT, VULKRO_GIT_BLAME,
VULKRO_AUTH_MIDDLEWARE_NAMES) apply transparently to scans
triggered through the MCP surface.
Read-only by design
There is no apply_fix, no write_file, no tool that mutates the
user's repo. suggest_fixes and verify_fix work on temporary copies
and hand the diff back; the agent or the user applies it. vulkro fix
already prints diffs to stdout for the user to apply by hand, and the
MCP surface holds that line.
Related
vulkro mcp: the command group.vulkro-sf mcp serve: the Salesforce MCP server, with the Vulkro Cloud workspace tools.- Desktop console: the same scanner exposed through a local web app instead.
vulkro mcp-audit: audit MCP host configs for supply-chain and credential-handling risks (the inverse direction: scan the configs that launch MCP servers, rather than be one).vulkro explain: the per-rule explainer that backs the MCPexplaintool.