Maybe the headless Ghidra MCP you are looking for.
Overview
Ryuumonbuchi turns Ghidra into a 217-tool Model Context Protocol server. An LLM agent opens a binary and drives analysis through typed tool calls: decompilation, disassembly, type reconstruction, patching, symbol and memory edits, and project export. No GUI automation, no hand-written analyzeHeadless scripts.
The server builds on the low-level MCP SDK (mcp.server.lowlevel) with a registry generated from one authoritative catalog. The catalog declares 217 dotted tool names; 212 map one-to-one onto methods of a persistent PyGhidra backend. health.ping and mcp.response_format are server-native, headless.run and headless.start are the native launcher paths, and operation.batch is the batching dispatcher.
Behind the transport, one persistent worker child holds a live PyGhidra/JVM session for the whole MCP lifespan. Repeated calls reuse the warmed backend instead of paying JVM startup per request.
Architecture
MCP client ── stdio or streamable HTTP ──► Ryuumonbuchi server
(low-level SDK, schema/policy dispatch)
│
│ protocol-v2, 8-byte length-prefixed socket IPC
▼
persistent worker child
(lazy PyGhidra/JVM, multiple program sessions)
│
┌──────────────────┴───────────────────┐
▼ ▼
backend (PyGhidra) headless.run ── analyzeHeadless
program sessions, (separate process group)
decompiler, analysis
Three dispatch paths:
- Worker tools (212): validated against the catalog schema, sent to the persistent child over protocol-v2 IPC, run against a live program session.
headless.run: spawnssupport/analyzeHeadlessdirectly with the caller's argv in its own process group. No shell, no rewriting of arguments.operation.batch: 1 to 32 worker tools in one call; read-only batches run without a transaction, mutating batches wrap in one undo transaction with rollback on error.
Prerequisites
- Ghidra 12.0 or newer. Its bundled JDK supplies Java, so no separate Java install is needed.
- Python 3.10 through 3.13. Python 3.14 is not yet supported:
pyghidrapinsJpype1==1.5.2, which ships no Python 3.14 wheel. - uv to run the documented workflow.
Install
Run the published package from PyPI:
uvx ryuumonbuchi
uvx builds an isolated environment on first run and reuses it after.
To run the current main branch instead:
uvx --from git+https://github.com/elliottophellia/Ryuumonbuchi@main ryuumonbuchi
For a local checkout:
git clone https://github.com/elliottophellia/Ryuumonbuchi.git
cd ryuumonbuchi
uv sync --locked --all-groups
uv run ryuumonbuchi
Configuration
Precedence is CLI flag over environment variable over built-in default. Classpath, class-file, and VM-argument values from CLI and environment are combined rather than overridden.
| CLI flag | Environment variable | Default | Description |
|---|---|---|---|
--ghidra-install-dir PATH |
GHIDRA_INSTALL_DIR |
/usr/share/ghidra (/opt/homebrew/share/ghidra on macOS) |
Ghidra installation root |
--max-heap-mb MIB |
RYUUMONBUCHI_MAX_HEAP_MB |
1024 |
Worker JVM max heap, 256 to 8192 |
--max-cpu COUNT |
RYUUMONBUCHI_MAX_CPU |
2 |
Worker CPU affinity count, 1 to os.cpu_count() |
--operation-timeout-seconds SECONDS |
RYUUMONBUCHI_OPERATION_TIMEOUT_SECONDS |
900 |
Per-operation wall-clock deadline, 30 to 86400 |
--max-import-bytes BYTES |
RYUUMONBUCHI_MAX_IMPORT_BYTES |
67108864 |
Cap on program.open_bytes payloads |
--max-response-bytes BYTES |
RYUUMONBUCHI_MAX_RESPONSE_BYTES |
4194304 |
Inline response cap before spill-to-file |
--max-log-tail-bytes BYTES |
RYUUMONBUCHI_MAX_LOG_TAIL_BYTES |
65536 |
Worker log tail returned on failure |
--classpath PATH (repeatable) |
RYUUMONBUCHI_CLASSPATH (path-separated) |
empty | Extra Java classpath entries |
--class-file PATH (repeatable) |
RYUUMONBUCHI_CLASS_FILES (path-separated) |
empty | Extra Java class files to load |
--vmarg ARG (repeatable) |
RYUUMONBUCHI_VMARGS (shlex) |
empty | Extra JVM arguments |
--allow-export |
RYUUMONBUCHI_ALLOW_EXPORT |
disabled | Enable export and save tools |
--allow-import-bytes |
RYUUMONBUCHI_ALLOW_IMPORT_BYTES |
disabled | Enable program.open_bytes |
--transport {stdio,http} |
RYUUMONBUCHI_TRANSPORT |
stdio |
MCP transport to serve |
--http-host HOST |
RYUUMONBUCHI_HTTP_HOST |
127.0.0.1 |
Bind address for --transport http |
--http-port PORT |
RYUUMONBUCHI_HTTP_PORT |
8765 |
Bind port for --transport http, 1 to 65535 |
--http-path PATH |
RYUUMONBUCHI_HTTP_PATH |
/mcp |
Streamable HTTP mount path |
[!IMPORTANT]
program.export_binary,program.export_packed,program.save,program.save_as, andproject.exportrequireRYUUMONBUCHI_ALLOW_EXPORT=1or--allow-export.program.open_bytesrequiresRYUUMONBUCHI_ALLOW_IMPORT_BYTES=1or--allow-import-bytes, and obeys the byte cap. Both gates default to deny.
Tool surface
All 217 tools use dotted names and take a JSON object. Backend tools generally accept a session_id (returned by program.open or program.open_bytes) and are batch-eligible. A representative slice, drawn from the catalog:
| Category | Example tools |
|---|---|
| Program & session | program.open, program.open_bytes, program.close, program.summary, program.report, program.mode.get/set, program.image_base.set, program.save, program.save_as, program.export_binary, program.export_packed |
| Analysis & tasks | analysis.update, analysis.update_and_wait, analysis.status, analysis.options.*, analysis.analyzers.*, analysis.clear_cache, task.analysis_update, task.status, task.result, task.cancel |
| Listing, decompilation, p-code | listing.disassemble.*, listing.code_units.list, listing.data.*, listing.clear, decomp.function, decomp.tokens, decomp.ast, decomp.writeback.*, pcode.function, pcode.block, pcode.op.at |
| Functions, symbols, types, layouts | function.*, symbol.*, namespace.create, class.create, type.*, layout.struct.*, layout.enum.*, layout.union.*, variable.*, parameter.* |
| References, search, graphs | reference.*, search.*, graph.basic_blocks, graph.cfg.edges, graph.call_paths |
| Memory | memory.blocks.list, memory.read, memory.write, memory.block.* |
| Comments, bookmarks, tags | comment.*, bookmark.*, tag.* |
| Transactions & patches | transaction.*, patch.assemble, patch.nop, patch.branch_invert |
| Projects & metadata | project.*, metadata.query, metadata.store |
| External, source, relocations | external.*, source.file.*, source.map.*, relocation.*, equate.* |
| Open world | ghidra.call, ghidra.eval, ghidra.script |
| Server-native | health.ping, mcp.response_format, headless.run, headless.start, operation.batch |
Usage
A first-analysis sequence:
health.ping: confirm the server responds; this never starts the JVM.program.openwithpath,read_only: true,update_analysis: false. Bothread_onlyandupdate_analysisdefault totruewhen omitted, so setupdate_analysis: falsewhen analysis options must be changed first.analysis.update_and_waitto run auto-analysis to completion.function.listto enumerate recovered functions.decomp.functionon a function start address.program.close.
Function addresses and decompiler views
Function tools accept exact function entries and addresses contained within a function. An unresolved address remains an error rather than selecting a nearby function; the error reports the normalized address plus the nearest previous and next function entries.
decomp.function defaults to view: "raw", the complete Ghidra C output. Use view: "compact" only for initial triage of declaration-heavy functions:
{
"session_id": "<session_id>",
"function_start": 1053104,
"view": "compact"
}
Compact output conservatively elides Ghidra-generated local declarations, is not compilable, and reports the omission count. Return to raw C, decomp.tokens, decomp.ast, or p-code when exact structure matters.
Typed tools come first. Sessions open read-only by default; switch program.mode.set to read_only: false only before intended mutations. Use operation.batch for 1 to 32 atomic program-bound calls. Treat ghidra.call, ghidra.eval, ghidra.script, headless.run, and headless.start as open-world execution. Inspect the second TextContent block for the full JSON result; the first is a compact summary. Close sessions with program.close when done.
Long operations
analysis.update_and_wait and headless.run block until they finish. When the client attaches a progress token to the call, both stream notifications/progress while they run:
analysis.update_and_waitruns as a worker task and polls it every 2 seconds.progressis elapsed seconds,totalisoperation_timeout_seconds, andmessageis Ghidra's current analyzer phase (for exampleDisassembled 14 K). Ghidra's task monitor rescopes its own counters per analyzer, so elapsed time is the only monotonic series available. Without a progress token the call keeps its original blocking behavior.headless.runreports elapsed seconds against itstimeout_secondsonce per second.
headless.start takes the same arguments as headless.run minus terminal, returns immediately with {"task_id": "native-<hex>", "status": "running"}, and is polled with the regular task.status, task.result, and task.cancel tools. task.cancel terminates the launcher's whole process group. Background runs have no PTY, so terminal capture stays exclusive to headless.run.
Claude Code
Add the server under mcpServers in .mcp.json or ~/.claude.json:
{
"mcpServers": {
"ryuumonbuchi": {
"type": "stdio",
"command": "uvx",
"args": [
"ryuumonbuchi",
"--ghidra-install-dir",
"/usr/share/ghidra"
],
"env": {
"RYUUMONBUCHI_MAX_CPU": "4",
"RYUUMONBUCHI_MAX_HEAP_MB": "2048"
}
}
}
}
Codex
Add the server to ~/.codex/config.toml:
[mcp_servers.ryuumonbuchi]
command = "uvx"
args = ["ryuumonbuchi", "--ghidra-install-dir", "/usr/share/ghidra"]
[mcp_servers.ryuumonbuchi.env]
RYUUMONBUCHI_MAX_CPU = "4"
RYUUMONBUCHI_MAX_HEAP_MB = "2048"
Streamable HTTP
The default transport is stdio. --transport http serves the same tool surface over MCP streamable HTTP:
uv run ryuumonbuchi --transport http --http-port 8765
Clients connect to http://127.0.0.1:8765/mcp:
{
"mcpServers": {
"ryuumonbuchi": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp"
}
}
}
One process serves one persistent worker and one JVM. Concurrent HTTP sessions share it and serialize on the worker lock.
[!WARNING] The HTTP transport is unauthenticated. It binds
127.0.0.1by default, where DNS-rebinding protection restrictsHostandOriginto loopback. Binding beyond loopback (--http-host 0.0.0.0) exposes every tool, includingheadless.run,ghidra.eval, andghidra.script, to anyone who can reach the port. Put it behind an authenticating reverse proxy or a private network boundary.
Development
uv sync --locked --all-groups # install runtime and dev dependencies
uv run ryuumonbuchi --version # print version and exit
uv run ryuumonbuchi --help # list every CLI flag
Test tiers:
uv run pytest tests/test_mcp_client_smoke.py tests/test_worker_lifecycle.py -q
uv run pytest -m "not live and not live_server" --cov=ryuumonbuchi --cov-branch --cov-report=term-missing --cov-fail-under=100
The live matrix requires a real Ghidra install plus Java 21 and a C compiler, and is skipped by default:
RYUUMONBUCHI_REQUIRE_LIVE=1 GHIDRA_INSTALL_DIR=/usr/share/ghidra uv run pytest -m live tests/test_live_workflow.py -q
Static checks run with ruff check, ruff format --check, and strict pyright.
Comments