mgba_run
Execute Game Boy, GBC, or GBA ROMs for a set number of frames and capture screenshots for automated testing and game analysis.
Instructions
Run a GB/GBC/GBA ROM for a specified number of frames and capture a screenshot
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rom_path | Yes | Path to the ROM file (.gb, .gbc, .gba) | |
| frames | No | Number of frames to run (default: 60) | |
| savestate_path | No | Optional path to a savestate file to load |
Implementation Reference
- src/mgba_mcp/server.py:213-231 (handler)Handler logic for the 'mgba_run' tool within the call_tool dispatcher. Calls MGBAEmulator.run_frames with provided arguments, handles the result, adds success/error text content, and embeds the screenshot as base64 ImageContent.if name == "mgba_run": result = emu.run_frames( rom_path=arguments["rom_path"], frames=arguments.get("frames", 60), savestate_path=arguments.get("savestate_path"), screenshot=True, ) if result.success: result_content.append(TextContent(type="text", text="Emulator ran successfully")) 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:37-55 (schema)Input schema definition for the 'mgba_run' tool, specifying rom_path as required, optional frames and savestate_path.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"], },
- src/mgba_mcp/server.py:34-56 (registration)Registration of the 'mgba_run' tool in the list_tools() function, including name, description, and input schema.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"], }, ),
- src/mgba_mcp/emulator.py:254-280 (handler)Core handler implementation in MGBAEmulator class: generates Lua script to run specified frames using frame callbacks, takes screenshot if requested, writes DONE marker, and delegates to _run_with_lua which manages subprocess execution, polling, and output collection.def run_frames( self, rom_path: str, frames: int = 60, savestate_path: Optional[str] = None, screenshot: bool = True, ) -> EmulatorResult: """Run emulator for specified number of frames.""" lua_script = f""" local frame = 0 local target_frames = {frames} local take_screenshot = {'true' if screenshot else 'false'} callbacks:add("frame", function() frame = frame + 1 if frame >= target_frames then if take_screenshot then emu:screenshot("screenshot.png") end -- Write DONE marker (emu:quit() is unreliable) local f = io.open("DONE", "w") if f then f:write("OK"); f:close() end end end) """ return self._run_with_lua(rom_path, lua_script, savestate_path)
- src/mgba_mcp/emulator.py:126-253 (helper)Core helper method that launches mGBA subprocess with xvfb (headless), runs Lua script, polls for completion marker, kills process, collects screenshot and JSON output, validates/normalizes PNG, returns structured EmulatorResult.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), )