mgba_run_lua
Execute custom Lua scripts in the mGBA emulator to automate testing, analyze game data, read/write memory, capture screenshots, and generate structured output for Game Boy, GBC, and GBA ROMs.
Instructions
Run a custom Lua script in the emulator. The script can use emu:read8(), emu:write8(), emu:screenshot(), callbacks:add(), etc.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| rom_path | Yes | Path to the ROM file | |
| script | Yes | Lua script to execute. Use emu:quit() to exit. Write JSON to 'output.json' for structured data. | |
| savestate_path | No | Optional savestate to load | |
| timeout | No | Timeout in seconds (default: 30) |
Implementation Reference
- src/mgba_mcp/server.py:340-365 (handler)MCP server tool handler for 'mgba_run_lua': invokes emulator.run_lua_script with arguments and formats the result (text output from data/stdout/error, optional screenshot) into MCP content.elif name == "mgba_run_lua": result = emu.run_lua_script( rom_path=arguments["rom_path"], script=arguments["script"], savestate_path=arguments.get("savestate_path"), timeout=arguments.get("timeout", 30), ) lines = [] if result.success: lines.append("Lua script executed successfully") if result.data: lines.append(f"Output data: {json.dumps(result.data, indent=2)}") if result.output: lines.append(f"Stdout: {result.output}") else: lines.append(f"Error: {result.error}") 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", ))
- src/mgba_mcp/server.py:177-203 (registration)Registers the 'mgba_run_lua' tool with the MCP server in list_tools(), including full input schema definition.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:432-440 (helper)Emulator method directly called by MCP handler; delegates to internal _run_with_lua for execution.def run_lua_script( self, rom_path: str, script: str, savestate_path: Optional[str] = None, timeout: int = 30, ) -> EmulatorResult: """Run a custom Lua script.""" return self._run_with_lua(rom_path, script, savestate_path, timeout)
- src/mgba_mcp/emulator.py:126-253 (helper)Core implementation: launches mGBA headless with custom Lua script via subprocess (xvfb-run mgba-qt --script), uses DONE marker file for completion detection, captures screenshot.png and output.json, validates/normalizes PNG, handles timeout/process 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), )