Skip to main content
Glama

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
NameRequiredDescriptionDefault
rom_pathYesPath to the ROM file
scriptYesLua script to execute. Use emu:quit() to exit. Write JSON to 'output.json' for structured data.
savestate_pathNoOptional savestate to load
timeoutNoTimeout in seconds (default: 30)

Implementation Reference

  • 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",
            ))
  • 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"],
        },
    ),
  • 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)
  • 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),
            )

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/struktured-labs/mgba-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server