mgba_read_memory
Read memory values from specific addresses in Game Boy, Game Boy Color, or Game Boy Advance ROMs after running for a set number of frames, enabling game analysis and automated testing.
Instructions
Read memory at specified addresses after running for some frames
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rom_path | Yes | Path to the ROM file | |
| addresses | Yes | List of memory addresses to read (as integers, e.g., [0xC200, 0xFFBF]) | |
| savestate_path | No | Optional savestate to load | |
| frames | No | Frames to run before reading (default: 60) |
Implementation Reference
- src/mgba_mcp/emulator.py:281-315 (handler)Core handler implementation: generates Lua script to run emulator for frames_before_read frames, then reads memory at each address using emu:read8(), writes JSON output, takes screenshot, signals completion.def read_memory( self, rom_path: str, addresses: list[int], savestate_path: Optional[str] = None, frames_before_read: int = 60, ) -> EmulatorResult: """Read memory at specified addresses.""" addr_list = ", ".join(f"0x{a:04X}" for a in addresses) lua_script = f""" local frame = 0 local addresses = {{{addr_list}}} callbacks:add("frame", function() frame = frame + 1 if frame >= {frames_before_read} then local f = io.open("output.json", "w") if f then f:write('{{') for i, addr in ipairs(addresses) do if i > 1 then f:write(',') end f:write(string.format('"0x%04X":%d', addr, emu:read8(addr))) end f:write('}}') f:close() end emu:screenshot("screenshot.png") -- Write DONE marker local done = io.open("DONE", "w") if done then done:write("OK"); done:close() end end end) """ return self._run_with_lua(rom_path, lua_script, savestate_path)
- src/mgba_mcp/server.py:232-254 (handler)MCP server tool dispatch handler: calls MGBAEmulator.read_memory() with parameters, formats results as hex dump text and includes screenshot if available.elif name == "mgba_read_memory": result = emu.read_memory( rom_path=arguments["rom_path"], addresses=arguments["addresses"], savestate_path=arguments.get("savestate_path"), frames_before_read=arguments.get("frames", 60), ) if result.success and result.data: # Format memory as hex dump lines = ["Memory dump:"] for addr_str, value in result.data.items(): lines.append(f" {addr_str}: 0x{value:02X} ({value})") result_content.append(TextContent(type="text", text="\n".join(lines))) if result.screenshot: result_content.append(ImageContent( type="image", data=base64.b64encode(result.screenshot).decode(), mimeType="image/png", )) else: result_content.append(TextContent(type="text", text=f"Error: {result.error}"))
- src/mgba_mcp/server.py:57-84 (schema)Tool schema definition including input parameters: rom_path (required), addresses (required list of integers), optional savestate_path and frames.Tool( name="mgba_read_memory", description="Read memory at specified addresses after running for some frames", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file", }, "addresses": { "type": "array", "items": {"type": "integer"}, "description": "List of memory addresses to read (as integers, e.g., [0xC200, 0xFFBF])", }, "savestate_path": { "type": "string", "description": "Optional savestate to load", }, "frames": { "type": "integer", "description": "Frames to run before reading (default: 60)", "default": 60, }, }, "required": ["rom_path", "addresses"], }, ),
- src/mgba_mcp/server.py:30-204 (registration)Tool registration via @server.list_tools() decorator, includes mgba_read_memory in the returned list of available tools.@server.list_tools() async def list_tools() -> list[Tool]: """List available MCP tools.""" return [ Tool( name="mgba_run", description="Run a GB/GBC/GBA ROM for a specified number of frames and capture a screenshot", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file (.gb, .gbc, .gba)", }, "frames": { "type": "integer", "description": "Number of frames to run (default: 60)", "default": 60, }, "savestate_path": { "type": "string", "description": "Optional path to a savestate file to load", }, }, "required": ["rom_path"], }, ), Tool( name="mgba_read_memory", description="Read memory at specified addresses after running for some frames", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file", }, "addresses": { "type": "array", "items": {"type": "integer"}, "description": "List of memory addresses to read (as integers, e.g., [0xC200, 0xFFBF])", }, "savestate_path": { "type": "string", "description": "Optional savestate to load", }, "frames": { "type": "integer", "description": "Frames to run before reading (default: 60)", "default": 60, }, }, "required": ["rom_path", "addresses"], }, ), Tool( name="mgba_read_range", description="Read a contiguous range of memory addresses", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file", }, "start_address": { "type": "integer", "description": "Starting memory address", }, "length": { "type": "integer", "description": "Number of bytes to read", }, "savestate_path": { "type": "string", "description": "Optional savestate to load", }, "frames": { "type": "integer", "description": "Frames to run before reading (default: 60)", "default": 60, }, }, "required": ["rom_path", "start_address", "length"], }, ), Tool( name="mgba_dump_oam", description="Dump OAM (Object Attribute Memory) sprite data - shows all 40 sprites with position, tile, flags, and palette", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file", }, "savestate_path": { "type": "string", "description": "Optional savestate to load", }, "frames": { "type": "integer", "description": "Frames to run before dumping (default: 60)", "default": 60, }, }, "required": ["rom_path"], }, ), Tool( name="mgba_dump_entities", description="Dump entity/actor data from WRAM - useful for analyzing game objects", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file", }, "entity_base": { "type": "integer", "description": "Base address of entity array (default: 0xC200)", "default": 49664, }, "entity_size": { "type": "integer", "description": "Size of each entity in bytes (default: 24)", "default": 24, }, "entity_count": { "type": "integer", "description": "Number of entities to dump (default: 10)", "default": 10, }, "savestate_path": { "type": "string", "description": "Optional savestate to load", }, "frames": { "type": "integer", "description": "Frames to run before dumping (default: 60)", "default": 60, }, }, "required": ["rom_path"], }, ), Tool( name="mgba_run_lua", description="Run a custom Lua script in the emulator. The script can use emu:read8(), emu:write8(), emu:screenshot(), callbacks:add(), etc.", inputSchema={ "type": "object", "properties": { "rom_path": { "type": "string", "description": "Path to the ROM file", }, "script": { "type": "string", "description": "Lua script to execute. Use emu:quit() to exit. Write JSON to 'output.json' for structured data.", }, "savestate_path": { "type": "string", "description": "Optional savestate to load", }, "timeout": { "type": "integer", "description": "Timeout in seconds (default: 30)", "default": 30, }, }, "required": ["rom_path", "script"], }, ), ]
- src/mgba_mcp/emulator.py:126-253 (helper)Core helper method that executes all Lua-based emulator operations: launches mGBA subprocess with xvfb, polls for DONE marker, collects output.json and screenshot.png, validates PNG, handles cleanup/kill.def _run_with_lua( self, rom_path: str, lua_script: str, savestate_path: Optional[str] = None, timeout: int = 30, ) -> EmulatorResult: """Run mGBA with a Lua script and return results. Uses watchdog pattern: polls for DONE marker file, then kills process. """ # Convert paths to absolute (subprocess runs in temp_dir) rom_path = str(Path(rom_path).resolve()) if savestate_path: savestate_path = str(Path(savestate_path).resolve()) # Clean up any previous run done_file = self.temp_dir / "DONE" if done_file.exists(): done_file.unlink() for f in ["screenshot.png", "output.json"]: p = self.temp_dir / f if p.exists(): p.unlink() # Write Lua script to temp file lua_file = self.temp_dir / "script.lua" lua_file.write_text(lua_script) # Build command cmd = [] if self.use_xvfb: cmd.extend(["xvfb-run", "-a"]) cmd.extend([self.mgba_path, rom_path]) if savestate_path: cmd.extend(["-t", savestate_path]) cmd.extend(["--script", str(lua_file), "-l", "0"]) try: # Disable audio to prevent sound leakage in headless mode env = os.environ.copy() env["SDL_AUDIODRIVER"] = "dummy" # Start process in new process group for clean kill proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(self.temp_dir), start_new_session=True, env=env, ) # Poll for DONE file or timeout start_time = time.time() poll_interval = 0.1 # 100ms while time.time() - start_time < timeout: # Check if script wrote DONE marker if done_file.exists(): # Give script time to finish writing and flushing files # mGBA screenshot can be slow to flush time.sleep(0.5) # Double-check screenshot file is stable (not still being written) screenshot_path = self.temp_dir / "screenshot.png" if screenshot_path.exists(): size1 = screenshot_path.stat().st_size time.sleep(0.1) size2 = screenshot_path.stat().st_size if size1 != size2: # File still growing, wait more time.sleep(0.5) break # Check if process died if proc.poll() is not None: break time.sleep(poll_interval) # Kill the process (emu:quit() doesn't work reliably) self._kill_process_tree(proc) # Collect output files screenshot_path = self.temp_dir / "screenshot.png" output_path = self.temp_dir / "output.json" screenshot = None if screenshot_path.exists(): screenshot_data = screenshot_path.read_bytes() # Validate and normalize PNG for Claude API compatibility normalized_data, error_msg = validate_and_normalize_png(screenshot_data) if normalized_data: screenshot = normalized_data else: # Log validation failure for debugging import sys print(f"PNG validation failed: {error_msg} (size={len(screenshot_data)})", file=sys.stderr) output_data = None if output_path.exists(): try: output_data = json.loads(output_path.read_text()) except json.JSONDecodeError: pass # Success if we got expected output if screenshot or output_data or done_file.exists(): return EmulatorResult( success=True, screenshot=screenshot, data=output_data, ) return EmulatorResult( success=False, error=f"Emulator timed out after {timeout}s without producing output", ) except Exception as e: return EmulatorResult( success=False, error=str(e), )