Metadata-Version: 2.4
Name: ryuumonbuchi
Version: 1.1.0
Summary: maybe the headless ghidra mcp you are looking for
Author: Ryuumonbuchi contributors
License-Expression: GPL-2.0-only
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: Software Development :: Disassemblers
Requires-Dist: mcp>=2,<3
Requires-Dist: pyghidra>=3.0.2,<4
Requires-Dist: anyio>=4.9,<5
Requires-Dist: jsonschema>=4.20,<5
Requires-Dist: uvicorn>=0.31.1,<1
Requires-Python: >=3.10, <3.14
Description-Content-Type: text/markdown

<div align="center">

<img src="https://cdn.rei.my.id/images/Ryuumonbuchi.png?v=1.1.0" alt="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)

</div>

## 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": "<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-<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`:

```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`.