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),
            )
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses key behavioral traits: the tool executes Lua scripts with emulator APIs, can output JSON, and has a timeout. However, it misses details like error handling, side effects on emulator state, or performance implications, which are important for a scripting tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose and efficiently lists example Lua functions and output instructions in a single, compact sentence. Every element adds value without redundancy, making it highly concise and well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of running custom scripts in an emulator, no annotations, and no output schema, the description is moderately complete. It covers basic functionality and output method but lacks details on return values, error cases, or integration with sibling tools, leaving gaps for an AI agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents parameters like rom_path and script. The description adds minimal value beyond the schema, mentioning Lua functions and JSON output, but doesn't elaborate on parameter interactions or advanced usage, aligning with the baseline for high coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Run a custom Lua script') and the target ('in the emulator'), which is specific and actionable. It distinguishes itself from sibling tools like mgba_run by specifying Lua scripting capabilities, though it doesn't explicitly contrast with all siblings like mgba_read_memory.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage through examples of Lua functions (emu:read8(), etc.), suggesting when to use this tool for custom scripting versus simpler read operations. However, it lacks explicit guidance on when to choose this over alternatives like mgba_run or mgba_read_memory, leaving some ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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