The Zorya MCP server (zorya-mcp) exposes the concolic execution engine as a set of tools that an LLM agent can call directly inside Cursor, Claude Code, or GitHub Copilot. Instead of running shell commands manually, you describe what you want to find and the agent drives the full analysis — loading the binary, picking functions, running campaigns, and surfacing vulnerability-triggering inputs.
The agent works through three phases:
- Recon — load the binary, list functions, resolve argument types.
- Execution — submit a concolic campaign, poll for completion.
- Results — read SAT witnesses (concrete inputs that trigger a bug), inspect the execution trace.
Before using the MCP server, make sure the full Zorya toolchain is installed and working on your machine. The MCP server is a thin wrapper around the same pipeline used by the CLI — it won't work without the underlying engine.
If you haven't installed Zorya yet, follow one of these options from the repo root:
# Option A — Docker (recommended, no native dependencies)
docker build -t zorya:latest .
# Option B — Native
make ghidra-config && make allVerify your installation by running a quick test:
zorya tests/programs/crashme/crashmeOnce Zorya runs correctly, proceed below.
From the zorya workspace root, build all release binaries at once. The MCP server needs both zorya-mcp (the server itself) and zorya-fuzzer (used internally by run_campaign):
cargo build --releaseThis places the binaries at:
target/release/zorya-mcp— the MCP servertarget/release/zorya-fuzzer— the campaign runner (called internally, do not invoke directly)
If you only build
--bin zorya-mcp,run_campaignwill fail at runtime with azorya-fuzzer binary not founderror.
Pick the section that matches your editor. Replace /absolute/path/to/zorya with the actual path to the cloned repository on your machine.
Create or edit .cursor/mcp.json at your project root (or ~/.cursor/mcp.json for a global installation):
{
"mcpServers": {
"zorya": {
"command": "/absolute/path/to/zorya/target/release/zorya-mcp",
"env": {
"ZORYA_DIR": "/absolute/path/to/zorya"
}
}
}
}Restart Cursor. Go to Settings → MCP to confirm the zorya server appears with a green status indicator.
Register the server once from the terminal:
claude mcp add zorya /absolute/path/to/zorya/target/release/zorya-mcp \
--env ZORYA_DIR=/absolute/path/to/zoryaVerify it was added:
claude mcp listThe zorya server will be available in every Claude Code session on that machine from now on.
Create .vscode/mcp.json at your workspace root:
{
"servers": {
"zorya": {
"type": "stdio",
"command": "/absolute/path/to/zorya/target/release/zorya-mcp",
"env": {
"ZORYA_DIR": "/absolute/path/to/zorya"
}
}
}
}Also make sure MCP is enabled in your VS Code user settings:
{
"chat.mcp.enabled": true
}Reload VS Code. Copilot Chat will discover the zorya tools automatically.
ZORYA_DIR tells the server where the zorya workspace root is (the directory that contains Cargo.toml). If you omit it, the server tries to auto-detect the location by walking up from its own binary path — this usually works when the binary lives inside the repo (target/release/zorya-mcp), but setting it explicitly is safer and avoids ambiguity.
Once the server is connected, open a chat session in your editor and give the agent a target binary. A minimal prompt to get started:
"Load
/absolute/path/to/mybinary, list the functions in themainpackage, pick the most interesting ones for concolic analysis, run a campaign, and tell me if any SAT states were found."
The agent will call the tools in the correct order. You can also drive it step by step as described below.
The numbered steps match the arrows in the diagram above.
load_binary(
binary_path = "/absolute/path/to/mybinary",
language = "go", # go | c | c++
compiler = "gc", # gc | tinygo (Go) / gcc | clang (C/C++)
thread_scheduling = "main-only" # optional, Go gc only
)
The server parses the ELF, extracts the entry point and section list, and stores binary_path, language, compiler, and thread_scheduling in session state. Every subsequent tool call reuses these values — you do not need to repeat them.
Build your binary with debug symbols for best results:
- Go:
go build -gcflags=all="-N -l" .- TinyGo:
tinygo build -gc=conservative -opt=0 .
list_functions(filter="main.", limit=50)
Returns a paginated table of function names, addresses, and sizes sorted by address. Use filter to narrow to a package or prefix, and offset/limit to page through large symbol tables.
get_function_signature(name_or_address="main.processInput")
# or by address:
get_function_signature(name_or_address="0x4bec40")
The server looks up the function in results/function_signatures_go.json (produced by a previous Zorya run), then falls back to Ghidra signatures, then to the raw ELF symbol table. The result tells the agent what argument types to pass in the campaign.
Detailed argument types are only available when the binary has been analysed at least once before (the signature file is generated during pcode extraction). On a first run, the ELF fallback still returns the function address and size.
run_campaign takes only a tests list. The global config (language, compiler, binary path, thread scheduling, negate-path) is taken automatically from the session state set by load_binary.
run_campaign(tests=[
{
"id": "main.processInput-sweep",
"mode": "function", # start | main | function
"start_address": "0x4bec40",
"args": "5", # space-separated concrete seeds, or "none"
"timeout_seconds": 120
}
])
Returns a job_id (integer). The analysis runs in the background; the call returns immediately. The campaign config is written to mcp_campaign_config.json in the workspace root for inspection.
For a single function without running a full campaign, use run_analysis instead:
run_analysis(
mode = "function",
start_address = "0x4bec40",
args = "5",
negate_path = true
)
get_job_status(job_id=42)
Returns:
state:running|completed|failedelapsed_seconds: time since submissionexit_code: process exit code (once finished)sat_found:trueif aFOUND_SAT_STATE.txtfile was written (completed jobs only)
Poll every ~10 seconds until state is no longer running.
For a campaign, use the test id you set in step 4:
get_sat_states(test_id="main.processInput-sweep")
For a single run_analysis run, omit test_id to read from the default results/ directory:
get_sat_states()
Returns the raw content of FOUND_SAT_STATE.txt — concrete input values that triggered a vulnerability (OOB index, integer overflow, nil dereference, panic, etc.). When FOUND_SAT_STATE.json sits next to it, the parsed report is also returned in report, and its path in json_path; see JSON reports for the schema.
To see a summary across all tests in a campaign:
get_campaign_summary()
Reads fuzzer_results/fuzzer_summary.txt and returns a pass/fail/timeout/SAT table for every test.
get_execution_trace(test_id="main.processInput-sweep", limit=100)
# or for a single run:
get_execution_trace(limit=100)
Returns a paginated function call trace with argument values, useful for understanding the execution path that led to the bug. Use offset + limit to page through large traces.
Based on findings, the agent refines the target list, adjusts arguments, or increases timeouts and submits a new campaign. Use cancel_job(job_id=42) to kill a stuck job. Use list_result_files(test_id="main.processInput-sweep") to enumerate all artifact files for a test.
| Group | Tool | Parameters | Description |
|---|---|---|---|
| Recon | load_binary |
binary_path, language, compiler?, thread_scheduling? |
Parse an ELF binary and store language/compiler in session state. Must be called first. |
| Recon | list_functions |
filter?, offset?, limit? |
Paginated function listing from the ELF symbol table. |
| Recon | get_function_signature |
name_or_address |
Resolve a function's argument types from signatures JSON, Ghidra, or ELF fallback. |
| Recon | list_strings |
filter?, min_length?, offset?, limit? |
List printable strings from the binary (requires strings on PATH). |
| Execution | run_analysis |
mode, start_address, args?, negate_path? |
Run concolic analysis on a single function. Returns job_id. |
| Execution | run_campaign |
tests |
Run a multi-test campaign via zorya-fuzzer. Returns job_id. |
| Execution | get_job_status |
job_id |
Poll a job: state, elapsed_seconds, sat_found, exit_code. |
| Execution | cancel_job |
job_id |
Kill a running job by ID. |
| Results | get_sat_states |
test_id? |
Read FOUND_SAT_STATE.txt for a campaign test or the latest single run. |
| Results | get_execution_trace |
test_id?, offset?, limit? |
Read the function call trace (paginated). |
| Results | get_campaign_summary |
(none) | Read fuzzer_results/fuzzer_summary.txt — pass/fail/SAT table for all tests. |
| Results | list_result_files |
test_id? |
List artifact files for a test or the main results/ directory. |
| File | Role |
|---|---|
main.rs |
Binary entry point, ZORYA_DIR detection, stdio server startup |
server.rs |
ZoryaMcp struct with all tool implementations (rmcp #[tool] macros) |
types.rs |
Parameter types with JSON Schema descriptions for LLM tool discovery |
jobs.rs |
Async job manager tracking spawned child processes |
