Skip to main content
Glama

mgba_read_memory

Read memory values from specific addresses in Game Boy, Game Boy Color, or Game Boy Advance ROMs after running for a set number of frames, enabling game analysis and automated testing.

Instructions

Read memory at specified addresses after running for some frames

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
rom_pathYesPath to the ROM file
addressesYesList of memory addresses to read (as integers, e.g., [0xC200, 0xFFBF])
savestate_pathNoOptional savestate to load
framesNoFrames to run before reading (default: 60)

Implementation Reference

  • Core handler implementation: generates Lua script to run emulator for frames_before_read frames, then reads memory at each address using emu:read8(), writes JSON output, takes screenshot, signals completion.
        def read_memory(
            self,
            rom_path: str,
            addresses: list[int],
            savestate_path: Optional[str] = None,
            frames_before_read: int = 60,
        ) -> EmulatorResult:
            """Read memory at specified addresses."""
            addr_list = ", ".join(f"0x{a:04X}" for a in addresses)
            lua_script = f"""
    local frame = 0
    local addresses = {{{addr_list}}}
    
    callbacks:add("frame", function()
        frame = frame + 1
        if frame >= {frames_before_read} then
            local f = io.open("output.json", "w")
            if f then
                f:write('{{')
                for i, addr in ipairs(addresses) do
                    if i > 1 then f:write(',') end
                    f:write(string.format('"0x%04X":%d', addr, emu:read8(addr)))
                end
                f:write('}}')
                f:close()
            end
            emu:screenshot("screenshot.png")
            -- Write DONE marker
            local done = io.open("DONE", "w")
            if done then done:write("OK"); done:close() end
        end
    end)
    """
            return self._run_with_lua(rom_path, lua_script, savestate_path)
  • MCP server tool dispatch handler: calls MGBAEmulator.read_memory() with parameters, formats results as hex dump text and includes screenshot if available.
    elif name == "mgba_read_memory":
        result = emu.read_memory(
            rom_path=arguments["rom_path"],
            addresses=arguments["addresses"],
            savestate_path=arguments.get("savestate_path"),
            frames_before_read=arguments.get("frames", 60),
        )
    
        if result.success and result.data:
            # Format memory as hex dump
            lines = ["Memory dump:"]
            for addr_str, value in result.data.items():
                lines.append(f"  {addr_str}: 0x{value:02X} ({value})")
            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",
                ))
        else:
            result_content.append(TextContent(type="text", text=f"Error: {result.error}"))
  • Tool schema definition including input parameters: rom_path (required), addresses (required list of integers), optional savestate_path and frames.
    Tool(
        name="mgba_read_memory",
        description="Read memory at specified addresses after running for some frames",
        inputSchema={
            "type": "object",
            "properties": {
                "rom_path": {
                    "type": "string",
                    "description": "Path to the ROM file",
                },
                "addresses": {
                    "type": "array",
                    "items": {"type": "integer"},
                    "description": "List of memory addresses to read (as integers, e.g., [0xC200, 0xFFBF])",
                },
                "savestate_path": {
                    "type": "string",
                    "description": "Optional savestate to load",
                },
                "frames": {
                    "type": "integer",
                    "description": "Frames to run before reading (default: 60)",
                    "default": 60,
                },
            },
            "required": ["rom_path", "addresses"],
        },
    ),
  • Tool registration via @server.list_tools() decorator, includes mgba_read_memory in the returned list of available tools.
    @server.list_tools()
    async def list_tools() -> list[Tool]:
        """List available MCP tools."""
        return [
            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"],
                },
            ),
            Tool(
                name="mgba_read_memory",
                description="Read memory at specified addresses after running for some frames",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "rom_path": {
                            "type": "string",
                            "description": "Path to the ROM file",
                        },
                        "addresses": {
                            "type": "array",
                            "items": {"type": "integer"},
                            "description": "List of memory addresses to read (as integers, e.g., [0xC200, 0xFFBF])",
                        },
                        "savestate_path": {
                            "type": "string",
                            "description": "Optional savestate to load",
                        },
                        "frames": {
                            "type": "integer",
                            "description": "Frames to run before reading (default: 60)",
                            "default": 60,
                        },
                    },
                    "required": ["rom_path", "addresses"],
                },
            ),
            Tool(
                name="mgba_read_range",
                description="Read a contiguous range of memory addresses",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "rom_path": {
                            "type": "string",
                            "description": "Path to the ROM file",
                        },
                        "start_address": {
                            "type": "integer",
                            "description": "Starting memory address",
                        },
                        "length": {
                            "type": "integer",
                            "description": "Number of bytes to read",
                        },
                        "savestate_path": {
                            "type": "string",
                            "description": "Optional savestate to load",
                        },
                        "frames": {
                            "type": "integer",
                            "description": "Frames to run before reading (default: 60)",
                            "default": 60,
                        },
                    },
                    "required": ["rom_path", "start_address", "length"],
                },
            ),
            Tool(
                name="mgba_dump_oam",
                description="Dump OAM (Object Attribute Memory) sprite data - shows all 40 sprites with position, tile, flags, and palette",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "rom_path": {
                            "type": "string",
                            "description": "Path to the ROM file",
                        },
                        "savestate_path": {
                            "type": "string",
                            "description": "Optional savestate to load",
                        },
                        "frames": {
                            "type": "integer",
                            "description": "Frames to run before dumping (default: 60)",
                            "default": 60,
                        },
                    },
                    "required": ["rom_path"],
                },
            ),
            Tool(
                name="mgba_dump_entities",
                description="Dump entity/actor data from WRAM - useful for analyzing game objects",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "rom_path": {
                            "type": "string",
                            "description": "Path to the ROM file",
                        },
                        "entity_base": {
                            "type": "integer",
                            "description": "Base address of entity array (default: 0xC200)",
                            "default": 49664,
                        },
                        "entity_size": {
                            "type": "integer",
                            "description": "Size of each entity in bytes (default: 24)",
                            "default": 24,
                        },
                        "entity_count": {
                            "type": "integer",
                            "description": "Number of entities to dump (default: 10)",
                            "default": 10,
                        },
                        "savestate_path": {
                            "type": "string",
                            "description": "Optional savestate to load",
                        },
                        "frames": {
                            "type": "integer",
                            "description": "Frames to run before dumping (default: 60)",
                            "default": 60,
                        },
                    },
                    "required": ["rom_path"],
                },
            ),
            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"],
                },
            ),
        ]
  • Core helper method that executes all Lua-based emulator operations: launches mGBA subprocess with xvfb, polls for DONE marker, collects output.json and screenshot.png, validates PNG, handles cleanup/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