Skip to main content
Glama

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
NameRequiredDescriptionDefault
rom_pathYesPath to the ROM file (.gb, .gbc, .gba)
framesNoNumber of frames to run (default: 60)
savestate_pathNoOptional path to a savestate file to load

Implementation Reference

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

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