Drive Dolphin's debugger from Claude to automate the function-discovery loop: set breakpoint/watchpoint → run game → read r3/LR/PC → resolve to a function → name it.
It talks to Dolphin's built-in GDB stub over the GDB Remote Serial Protocol
(TCP). No Dolphin plugin, no gdb binary, no pip installs — pure stdlib Python.
Built for SFA-Decomp, a matching
decompilation of Star Fox Adventures: because the build byte-matches retail, the
addresses in the symbol map are the live RAM addresses while the game runs, so a
halted PC or an r3 pointer resolves straight back to unit/file.c::Function +0xNN.
There's a write-up of how it came together here.
In Dolphin.ini (on macOS, ~/Library/Application Support/Dolphin/Config/Dolphin.ini):
[General]
GDBPort = 2159The key really does live under [General], not [Core] — internally it's
{System::Main, "General", "GDBPort"}, and a value under [Core] is silently
ignored. Edit Dolphin.ini only while Dolphin is fully quit — it rewrites
the file on exit and will clobber a live edit.
([Interface] DebugModeEnabled = True is also handy for the GUI debugger.)
Then restart Dolphin and boot SFA — the port must be set before boot.
The server resolves addresses using config/GSAE01/{symbols.txt,splits.txt}
from an SFA-Decomp checkout. Tell it where that lives:
export DOLPHIN_REPO_ROOT=/path/to/SFA-Decomp(If you vendor this folder under <repo>/tools/dolphin_mcp/ it auto-detects the
checkout and you can skip this.)
python3 smoketest.pyExpect it to connect, halt, and print PC/LR/r3 resolved to symbol names. If it says "could not connect", the stub isn't listening (check the ini / rebuild / that the game is booted).
claude mcp add dolphin \
-e DOLPHIN_REPO_ROOT=/path/to/SFA-Decomp \
-- python3 /path/to/dolphin-debugger-mcp/server.pyOptional env: DOLPHIN_GDB_HOST (default 127.0.0.1), DOLPHIN_GDB_PORT (2159),
DOLPHIN_GAMEID (GSAE01), DOLPHIN_REPO_ROOT (the SFA-Decomp checkout).
| Tool | Purpose |
|---|---|
connect / status |
Connect to the stub / show state + active breakpoints |
whereami |
Resolve current PC + LR → function+offset and unit |
read_registers |
All GPRs + pc/lr/ctr/cr/xer/msr; resolves pc/lr/r3 |
read_register / write_register |
One register (r0-31, f0-31, pc, lr, ...) |
read_memory / write_memory |
Memory r/w (hex + big-endian u32 words) |
set_breakpoint |
Execution breakpoint at a symbol or address |
watch_memory |
Hardware watchpoint (write / read / access) |
clear_breakpoint / clear_all_breakpoints |
Remove breakpoints/watches |
continue |
Resume; blocks until a breakpoint hits or timeout (then force-halts) |
resume |
Free-run without blocking (for booting / free play) |
wait_stop |
Wait for a breakpoint/watch to fire; never force-halts on timeout |
step |
Single-step one instruction |
halt |
Break the running emulator |
toggle_loop / stop_loop |
A/B a memory value visually on a timer (see below) |
trace_breakpoint |
Auto-continue a hot breakpoint, logging distinct hits deduped by (regs, caller) with counts; stops after N idle seconds with no new hit; great for "what is GameBit_Set called with as I walk around" |
lookup |
Symbol ↔ address resolution (no Dolphin needed) |
Addresses accept hex (0x801ee668), decimal, or a symbol name.
Boot the game, get to the cloudrunner level, then watch the steer field for writes and tell me which function writes it.
Two execution models:
- Free play (recommended for getting in-game):
resumeto run freely while you play; when you've set a breakpoint/watch,resumethenwait_stopto catch the hit.wait_stopleaves the game running on timeout (call it again), so long stretches of play are fine.haltto break in manually. - Blocking:
set_breakpoint/watch_memorythencontinue— blocks until the hit, or force-halts attimeout(use when you expect the hit soon).
On any stop, the report includes pc, lr, and r3 already resolved to
function+offset — exactly what you need to name the function.
While the game is running (after resume), register/memory reads are
rejected with a clear error — the stub only answers reads when halted. Call
wait_stop or halt first.
Reading code tells you what a value should do; flipping it live tells you what
it actually does on screen. toggle_loop alternates a memory location between
two values every period seconds in a background thread, so the game free-runs
between switches (each switch is a brief halt → write → resume blip):
Alternate
damagePhasebetween 0 and 5 every five seconds while I watch.
toggle_loop(addr="0x80…", value_a="5", value_b="0", size=1, period=5)
cycles=0 (the default) runs until you call stop_loop, which leaves the game
running on the last-written value. size is the write width in bytes
(big-endian). Only one loop runs at a time.
rsp.py— RSP client: packet framing/checksums,p/Pregisters,m/Mmemory,Z/zbreakpoints+watchpoints,c/s+ Ctrl-C halt. Register numbering matches Dolphin'sGDBStub.cpp(GPR 0-31, FPR 32-63, pc 64, msr 65, cr 66, lr 67, ctr 68, xer 69, fpscr 70).symbols.py— loadsconfig/GSAE01/{symbols.txt,splits.txt}fromDOLPHIN_REPO_ROOT; bisect lookup turns an address intounit::function+offset. The build byte-matches retail, so symbols.txt addresses are the live RAM addresses.server.py— MCP stdio transport (newline-delimited JSON-RPC 2.0) + tools.
- The stub freezes the CPU at boot (
__start) waiting for a client — a black screen on boot is expected. Connect, thencontinueto run the game. - One client per boot, and the stub tears down on disconnect. Dolphin's stub
accepts a single GDB connection for the lifetime of that emulation boot; when
the client disconnects the port closes. So: connect once and hold it for the
whole session. Any reconnect (new Claude session, or running the one-shot
smoketest.py/live_proof.py, which close when done) needs a game reboot (Stop → Play) to get a fresh stub. - Never probe the port with
nc/telnet. A connect-then-drop consumes the stub's single client slot and tears it down. Only connect with the real RSP client (the MCP server, which holds the socket). - The stub halts the whole emulator on break (fine for discovery).
continueblocks up to itstimeout(default 30s) waiting for a hit; trigger the in-game action during that window. On timeout it force-halts and says so.- Watchpoints are hardware-backed and historically rough on some builds — if a
watch never fires, fall back to an execution
set_breakpoint.
Copyright © 2026 Jack Price-Burns.
Released under the GNU Affero General Public License v3.0 — see LICENSE. It's strong copyleft: if you use, modify, or run this (including offering it as a network service), you must make your full corresponding source available under the same license.