mcp-mgba recipes
May 10, 2026 · View on GitHub
Practical examples of driving an mGBA emulator through Claude (or any MCP client). Each recipe is self-contained — copy-paste the prompt at the top of a Claude conversation with mcp-mgba registered, and Claude will work through the tool calls.
Prerequisites: mGBA running, ROM loaded,
bridge.lualoaded via Tools > Scripting. Test withmgba_pingfirst.
1. Find the address of a counter you can see on screen
"I'm running my homebrew RPG. The HP counter on screen reads 100. I want to find which IWRAM address holds it. Walk into a fight, take some damage so HP is now 87, and find the address."
The pattern Claude follows: read a window of IWRAM (e.g. 4096 bytes at 0x03000000) before and after a known change, then diff for u16/u32 values that went 100 → 87. With mgba_read_range returning byte arrays directly, this fits in 2 tool calls and a quick byte-by-byte scan.
1. mgba_read_range(address=0x03000000, length=4096) # snapshot A (HP=100)
2. <user takes damage>
3. mgba_read_range(address=0x03000000, length=4096) # snapshot B (HP=87)
4. <Claude diffs A vs B for any u16/u32 that changed 100 → 87>
For values outside IWRAM, broaden to 0x02000000 (EWRAM, 256 KiB — needs 64 read_range calls). Game Boy ROMs: scan 0xC000 (WRAM, 8 KiB) and 0xA000 (SRAM, 8 KiB).
2. Inject a value into a known address
"The clock variable in my totp-gba ROM is at IWRAM 0x03000148. Set it to epoch 4070908801 (year 2099) so I can see how the TOTP code rolls."
mgba_write32(address=0x03000148, value=4070908801)
For Game Boy MBC-cartridge SRAM, don't use mgba_write* to seed save data — those bypass the bus model and won't trigger MBC RAM-enable. Use mgba_load_state with a pre-prepared state file instead (see recipe #6).
3. Verify a memory write actually landed
"I just wrote 0xDEADBEEF to address X. Read it back and confirm."
mgba_write32(address=X, value=3735928559)
mgba_read32(address=X)
mgba_read_range is more reliable than typed reads (read32 is intermittently flaky via pcall — see CHANGELOG). For high-confidence verification, use:
mgba_read_range(address=X, length=4)
and decode little-endian from the bytes.
4. Drive a side-scroller's character
"Press Right for half a second, jump (A), then press Right again for half a second."
mgba_press_buttons queues each press independently so they fire as distinct edge events (not one continuous hold). At 60 fps, 30 frames ≈ half a second:
mgba_press_buttons(buttons=["Right"], frames=30)
mgba_press_buttons(buttons=["A"], frames=4)
mgba_press_buttons(buttons=["Right"], frames=30)
Each call returns immediately with the new queue size. Subsequent screenshots will show the result of the queued sequence.
5. Take a series of screenshots over time
"Take 6 screenshots, one every second, of the current scene."
for i in 1..6:
mgba_screenshot(path="/tmp/frame_${i}.png")
<wait 1 second of real time before next call>
(MCP doesn't give Claude a sleep tool directly, but the model can intersperse other tool calls or just describe the timing for you to execute. A future recipe will use mgba_advance_frames to make this deterministic.)
6. Snapshot, scribble, restore
"Save the current state to slot 9, then experiment freely (write to memory, press buttons), then restore."
mgba_save_state(slot=9)
# experiment freely — read/write/press anything
mgba_load_state(slot=9) # back to the snapshot
This is the cleanest pattern for "non-destructive exploration" of game state.
7. Discover what mGBA build supports
"What can this build of mGBA actually do?"
mgba_get_info
Returns capabilities — a map of {methodName: bool} for every optional emu method. Some builds lack pause/unpause/frameAdvance; the bridge falls back to runFrame or step automatically for frame-advance, but the other tools error explicitly with a clear "not available on this mGBA build" message.
8. Quick sanity-check the bridge before doing anything
mgba_ping # should return "pong"
mgba_get_info # confirms ROM loaded, shows capabilities
If ping fails: emulator's not running, bridge.lua isn't loaded, or the bridge crashed.
If ping works but get_info.rom_loaded is false: load a ROM in mGBA.
Tips for Claude (or other LLMs) using these tools
- Always start with
mgba_get_info. The capabilities map tells you which tools will work, and you can pick fallback strategies. - Use
mgba_read_rangefor typed reads when reliability matters. The single-valueread8/16/32are intermittently flaky on some mGBA builds (see CHANGELOG);read_rangeis rock-solid. - Queue button presses, don't hold then release manually. The press queue handles edge events correctly so each call = one logical button press.
- For RAM hunting, two snapshots + diff beats incremental reads. It's easier to spot the 1 changed
u16in 4096 bytes than to ask "did this address change" 4096 times.