Ryuumonbuchi # Ryuumonbuchi **Maybe the headless Ghidra MCP you are looking for.** [![Python](https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776AB?style=flat-square&logo=python&logoColor=white)](https://www.python.org) [![License](https://img.shields.io/badge/License-GPL--2.0-blue?style=flat-square)](LICENSE) [![Ghidra](https://img.shields.io/badge/Ghidra-headless-FF6600?style=flat-square)](https://ghidra-sre.org) [![MCP](https://img.shields.io/badge/Protocol-MCP-6E4AF0?style=flat-square)](https://modelcontextprotocol.io)
## Overview Ryuumonbuchi turns Ghidra into a 217-tool [Model Context Protocol](https://modelcontextprotocol.io) 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: 1. Worker tools (212): validated against the catalog schema, sent to the persistent child over protocol-v2 IPC, run against a live program session. 2. `headless.run`: spawns `support/analyzeHeadless` directly with the caller's argv in its own process group. No shell, no rewriting of arguments. 3. `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: `pyghidra` pins `Jpype1==1.5.2`, which ships no Python 3.14 wheel. - [uv](https://docs.astral.sh/uv/) to run the documented workflow. ## Install Run the published package from PyPI: ```bash uvx ryuumonbuchi ``` `uvx` builds an isolated environment on first run and reuses it after. To run the current main branch instead: ```bash uvx --from git+https://github.com/elliottophellia/Ryuumonbuchi@main ryuumonbuchi ``` For a local checkout: ```bash 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`, 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. ## 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: 1. `health.ping`: confirm the server responds; this never starts the JVM. 2. `program.open` with `path`, `read_only: true`, `update_analysis: false`. Both `read_only` and `update_analysis` default to `true` when omitted, so set `update_analysis: false` when analysis options must be changed first. 3. `analysis.update_and_wait` to run auto-analysis to completion. 4. `function.list` to enumerate recovered functions. 5. `decomp.function` on a function start address. 6. `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: ```json { "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_wait` runs as a worker task and polls it every 2 seconds. `progress` is elapsed seconds, `total` is `operation_timeout_seconds`, and `message` is Ghidra's current analyzer phase (for example `Disassembled 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.run` reports elapsed seconds against its `timeout_seconds` once per second. `headless.start` takes the same arguments as `headless.run` minus `terminal`, returns immediately with `{"task_id": "native-", "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`: ```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`: ```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: ```bash uv run ryuumonbuchi --transport http --http-port 8765 ``` Clients connect to `http://127.0.0.1:8765/mcp`: ```json { "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. ## Development ```bash 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: ```bash 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: ```bash 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`.