mcp-mgba
       
An MCP server that exposes the mGBA Game Boy Advance emulator to any MCP-compatible client (Claude Desktop, Claude Code, etc.).
Lets your model read and write GBA memory, inject button presses, take screenshots, and step the emulator β all through a clean tool interface.
!demo
Claude driving an in-development homebrew side-scroller through mgba_press_buttons β Start to begin, A to confirm New Game, then Right to walk and A to jump. Each frame is captured via mgba_screenshot.
How it works
+------------------+ stdio +------------------+ TCP :8765 +------------------+
| MCP client | JSON-RPC | mcp-mgba | newline JSON | mGBA emulator |
| (Claude / etc.) | ===========> | (Node.js) | ============> | bridge.lua |
+------------------+ +------------------+ +------------------+
Two pieces:
lua/bridge.luaβ runs inside mGBA's scripting engine, opens a loopback TCP server on port 8765dist/index.jsβ Node.js MCP server, talks to the Lua bridge over TCP, exposes tools over stdio
Requirements
- mGBA 0.10 or newer (with Lua scripting)
- Node.js 22+ (for the MCP server)
Install
Option A β install from npm (recommended)
npm install -g mcp-mgba
Puts mcp-mgba on your PATH. Verify with mcp-mgba --help (it'll print a startup line and wait for stdio β Ctrl+C to exit).
Option B β npx (no install)
npx -y mcp-mgba
Run on demand. Good for trying it out without committing to a global install.
Option C β clone and develop
git clone https://github.com/dmang-dev/mcp-mgba
cd mcp-mgba
npm install # also runs the build via the `prepare` hook
Then reference the absolute path to dist/index.js when registering, or npm install -g . to symlink the bin globally.
Set up the mGBA bridge
- Launch mGBA and load any GBA ROM.
- Open Tools > Scriptingβ¦
- Click File > Load script and select
lua/bridge.luafrom this repo.
You should see in the scripting console: `` [mcp-mgba] bridge listening on 127.0.0.1:8765 [mcp-mgba] frame callback registered β bridge is active ``
If you see a bind failed error, the previous instance's socket is still held β quit and relaunch mGBA.
Register with your MCP client
Claude Code (CLI)
claude mcp add mgba --scope user mcp-mgba
(if you used Option B without global install, replace mcp-mgba with node /absolute/path/to/dist/index.js)
Verify: ```bash claude mcp list
mgba: mcp-mgba - β Connected
### Claude Desktop
Edit `claude_desktop_config.json`:
| Platform | Path |
|---|---|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
Add (assuming Option A β globally installed):
{ "mcpServers": { "mgba": { "command": "mcp-mgba" } } } ```
Or with explicit Node + path (Option B): ``json { "mcpServers": { "mgba": { "command": "node", "args": ["/absolute/path/to/mcp-mgba/dist/index.js"] } } } ``
Restart Claude Desktop after editing.
Other MCP clients
The server speaks standard MCP over stdio. Run mcp-mgba (or node dist/index.js) and connect any MCP client to its stdio.
Configuration
| Env var | Default | Purpose | |-------------|---------------|------------------------| | MGBA_HOST | 127.0.0.1 | Bridge host to dial | | MGBA_PORT | 8765 | Bridge port to dial |
Tools
| Tool | Description | |------|-------------| | mgba_ping | Verify bridge connectivity (returns pong) | | mgba_get_info | Game title, code, frame count | | mgba_read8 / mgba_read16 / mgba_read32 | Read memory at an address | | mgba_write8 / mgba_write16 / mgba_write32 | Write to RAM | | mgba_read_range | Read up to 4096 bytes as a byte array | | mgba_write_range | Write up to 4096 bytes from a byte array | | mgba_press_buttons | Queue a button press (FIFO; consecutive calls produce distinct events) | | mgba_advance_frames | Step the emulator N frames | | mgba_pause / mgba_unpause | Pause / resume emulation | | mgba_reset | Reset the loaded ROM | | mgba_screenshot | Save a PNG of the current display | | mgba_save_state / mgba_load_state | Save/load emulator state to a slot or path |
See docs/RECIPES.md for end-to-end examples (RAM hunting, snapshot-experiment-restore, side-scroller automation, etc.).
GBA button names
A, B, Select, Start, Right, Left, Up, Down, R, L
GBA address space (cheat sheet)
| Range | Region | |----------------|-------------------------------| | 0x02000000 | EWRAM (256 KiB, general) | | 0x03000000 | IWRAM (32 KiB, fast) | | 0x04000000 | I/O registers | | 0x05000000 | Palette RAM | | 0x06000000 | VRAM | | 0x07000000 | OAM | | 0x08000000 | ROM (read-only) |
Troubleshooting
| Symptom | Cause / Fix | |---|---| | Cannot reach mGBA bridge at 127.0.0.1:8765 | mGBA isn't running, or bridge.lua isn't loaded β open Tools > Scripting and load it | | bind failed β port 8765 may already be in use | A previous mGBA instance still holds the socket; quit and relaunch mGBA | | Tool calls hang | The bridge script may have errored out silently after a hot-reload β check the mGBA scripting console | | Tools missing in Claude after install | Restart your MCP client; Claude only enumerates servers on startup | | Tool calls return data shaped like an old version after editing bridge.lua and choosing Load Script again | mGBA doesn't fully tear down a previous script when you reload. The new script's bind() may succeed but the old frame callback keeps serving requests. Fix: quit mGBA entirely, relaunch, load the ROM, then load bridge.lua once. Check the console for the frame callback registered line β there should be exactly one. | | attempt to index a nil value (global 'emu') at script load | mGBA's emu global only exists once a ROM is loaded. Load any ROM first, then load bridge.lua. (Or load the script first; capability detection will defer until a ROM is loaded.) | | emu:foo not available on this mGBA build for pause, unpause, frameAdvance, etc. | This particular build of mGBA doesn't expose that method. The bridge feature-detects on the first frame; check mgba_get_info for the full capabilities map. For frameAdvance, the bridge falls back to runFrame then step automatically. | | read8/16/32 returns "invoking failed" intermittently | Known mGBA Lua quirk β the typed read methods are flaky via pcall from the frame callback. The bridge already routes read8/16/32 through the more reliable readRange internally; if you still see this on a write, the retry loop usually clears it within a few attempts. | | Multiple press_buttons calls don't seem to register as distinct events | Older mgba_press_buttons (β€0.1.0) had this bug; v0.2.0+ uses a FIFO queue. Make sure you've upgraded with npm install -g mcp-mgba and restarted your MCP client. |
Development
npm install
npm run dev # tsc --watch β autobuilds on src/ changes
The Lua side (lua/bridge.lua and lua/json.lua) needs no build step. Edit and reload via mGBA's File > Load script.
Debugging with the MCP Inspector
Browse and call this server's tools interactively with the MCP Inspector:
npm run inspector
Build first if you've edited src/ since your last npm install (npm run build, or keep npm run dev running). Override the bridge address with MGBA_HOST / MGBA_PORT (default 127.0.0.1:8765). tools/list works even without mGBA connected; calling a tool needs mGBA open with lua/bridge.lua loaded.











