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.
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.
- 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.
Run the published package from PyPI:
uvx ryuumonbuchiuvx 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 ryuumonbuchiFor a local checkout:
git clone https://github.com/elliottophellia/Ryuumonbuchi.git
cd ryuumonbuchi
uv sync --locked --all-groups
uv run ryuumonbuchiPrecedence 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, and project.export require RYUUMONBUCHI_ALLOW_EXPORT=1 or --allow-export. program.open_bytes requires RYUUMONBUCHI_ALLOW_IMPORT_BYTES=1 or --allow-import-bytes, and obeys the byte cap. Both gates default to deny.
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 |
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 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.
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.
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"
}
}
}
}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"The default transport is stdio. --transport http serves the same tool surface over MCP streamable HTTP:
uv run ryuumonbuchi --transport http --http-port 8765Clients 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.1 by default, where DNS-rebinding protection restricts Host and Origin to loopback. Binding beyond loopback (--http-host 0.0.0.0) exposes every tool, including headless.run, ghidra.eval, and ghidra.script, to anyone who can reach the port. Put it behind an authenticating reverse proxy or a private network boundary.
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 flagTest 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=100The 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 -qStatic checks run with ruff check, ruff format --check, and strict pyright.
