Skip to main content
Glama
MarkMcCaskey

dolphin-decomp-mcp

by MarkMcCaskey
README.md
# dolphin-decomp-mcp

A small, general-purpose tool surface over Dolphin's [DAP debug
server](https://github.com/Jacoby6000/dolphin/tree/feature/dap-server) for
decompilation / reverse-engineering work on GameCube & Wii titles.

It exposes **12 primitives** — read/write memory and registers, disassemble,
run control, code & data breakpoints, a PPC assembler, code injection, detours,
and non-halting realtime watches/freezes — as an **MCP server** (for an agent
like Claude Code) and an equivalent **CLI** (for manual poking). Both front-ends
share the exact same handlers, so behaviour can't drift.

The tools are deliberately low-level. The agent composes them into whatever it
needs; the three headline workflows fall out of composition (see
[Recipes](#recipes)).

## Requirements

- A Dolphin built from the `feature/dap-server` branch with the DAP server
  enabled (see below). This project talks to that server; it does not embed one.
- Python ≥ 3.10.
- For the assembler tools: the `powerpc-eabi-*` binutils. This project reuses
  the ones the melee decomp tree ships under `build/binutils`. Point it
  elsewhere with `DOLPHIN_DECOMP_BINUTILS=/path/to/dir`.
- For the MCP server front-end only: `pip install mcp`. The CLI and the Python
  API need no third-party packages.

## Install

```bash
cd dolphin-decomp-mcp
python3 -m venv .venv && . .venv/bin/activate
pip install -e '.[mcp,dev]'      # drop [mcp] if you only want the CLI
pytest -q                         # 21 tests, no emulator needed
```

## Connecting to Dolphin

Start Dolphin's DAP server. **Prefer a Unix socket** — it is gated by
filesystem permissions. The TCP transport in this fork binds an *unauthenticated*
debug server that grants full guest-memory read/write and code injection; if you
use TCP, keep it on loopback (`127.0.0.1`) and off any untrusted network. (A
local patch pinning the fork's TCP bind to loopback lives in the sibling
`dolphin-dap` checkout.)

```bash
dolphin-emu-nogui \
  -C Dolphin.General.DAPSocket=/tmp/dolphin-decomp.sock \
  -C Dolphin.General.DAPStopOnEntry=true \
  -v Null --platform headless \
  --exec /path/to/GALE01.iso
# optional: --debug-elf /path/to/main.elf   (adds DWARF 1.1 line info / symbols)
```

Then point either front-end at it via `--socket` / `--port` or the env vars
`DOLPHIN_DAP_SOCKET`, `DOLPHIN_DAP_HOST`, `DOLPHIN_DAP_PORT`.

## MCP server (agent-facing)

Run it over stdio:

```bash
dolphin-decomp-mcp --socket /tmp/dolphin-decomp.sock
```

Register it with Claude Code (`.mcp.json` in your project, or `claude mcp add`):

```json
{
  "mcpServers": {
    "dolphin": {
      "command": "/abs/path/dolphin-decomp-mcp/.venv/bin/dolphin-decomp-mcp",
      "args": ["--socket", "/tmp/dolphin-decomp.sock"],
      "env": { "DOLPHIN_DECOMP_BINUTILS": "/abs/path/melee/build/binutils" }
    }
  }
}
```

The connection to Dolphin is lazy and reconnects if the socket drops, so the MCP
server can start before the emulator is up.

## CLI (manual)

Every tool is a subcommand (the `dolphin_` prefix is dropped). Scalars are
positional/flag args; object/array args are JSON strings. Output is JSON.

```bash
dolphin-decomp --socket /tmp/dolphin-decomp.sock session status
dolphin-decomp --socket /tmp/dolphin-decomp.sock read 0x80000000 16
dolphin-decomp --socket /tmp/dolphin-decomp.sock read 0x80003100 4 --format disasm
dolphin-decomp --socket /tmp/dolphin-decomp.sock control pause
dolphin-decomp --socket /tmp/dolphin-decomp.sock inject $'li r3, 1\nblr'
dolphin-decomp --socket /tmp/dolphin-decomp.sock breakpoints --data '[{"address":2147483648,"access":"write"}]'
dolphin-decomp assemble $'li r3, 0x1234\nblr'      # no emulator needed
```

## The tools

| Tool | What it does |
|------|--------------|
| `dolphin_session` | status / attach / launch / restart / terminate / disconnect |
| `dolphin_read` | read memory as hex, or as disassembly |
| `dolphin_write` | write raw bytes (hex) to memory |
| `dolphin_registers` | read all registers of the top frame; optionally write some first |
| `dolphin_eval` | evaluate a PPC debugger expression (`r3 + r4`, memory derefs) |
| `dolphin_stack` | backtrace the stopped core (who called / who wrote) |
| `dolphin_control` | continue / pause / step_over / step_in / step_out / goto — returns the resulting stop |
| `dolphin_breakpoints` | set code breakpoints and/or data watchpoints (authoritative) |
| `dolphin_assemble` | PPC (Gekko) asm text → machine-code bytes, no emulator needed |
| `dolphin_inject` | assemble (or take hex) and write code into a cave or a fixed address |
| `dolphin_detour` | install a transparent detour at an instruction (observe or replace) |
| `dolphin_watch` | non-halting realtime memory watch/poll, and freeze/unfreeze |

## Recipes

**Find the writer (reverse breakpoint).** What corrupts `0x8045C000`?

1. `dolphin_breakpoints data=[{address: 0x8045C000, access: "write"}]`
2. `dolphin_control continue` → stops with `reason: "data breakpoint"`, PC = the
   writing instruction.
3. `dolphin_stack` + `dolphin_registers` → the culprit and its inputs.

**Call a function / test a hypothesis.** Does `fn(0x10)` return what you think?

1. `dolphin_control goto <fn_entry>`; `dolphin_registers writes={r3: 0x10}`.
2. `dolphin_breakpoints code=[{address: <caller_return_or_blr>}]`.
3. `dolphin_control continue`; read the result from `r3` via `dolphin_registers`.

**Test a code change live.** Try a fix without rebuilding the game.

- Patch in place: `dolphin_inject code="<asm>" address=<site>` (read it first if
  you want to restore), or
- Observe/replace non-destructively: `dolphin_detour target=<fn> body="<asm>"`
  (end with `b`-to-trampoline to observe, or `blr` after setting `r3` to
  replace), then `dolphin_control continue` and verify.

## Layout

```
dolphin_decomp/
  dap.py       # framed DAP client core + event pump (no deps)
  session.py   # high-level Dolphin operations (no deps)
  ppc.py       # powerpc-eabi assembler wrapper (no deps)
  tools.py     # the 12-tool registry + Backend (shared by both front-ends)
  server.py    # MCP server (needs `mcp`)
  cli.py       # CLI (no deps)
tests/         # framing, session-over-mock, assembler; mcp_smoke.py is live-only
```

## License

MIT.