Skip to main content
Glama

An MCP server that lets LLM agents (Claude, GPT, etc.) autonomously play retro games, by wrapping OpalEmu, a browser-based, EmulatorJS-powered retro emulator supporting 18 systems, with a Node.js bridge.

This project does not modify OpalEmu. It drives a real, unmodified OpalEmu page in a Playwright-controlled browser and exposes it to agents as 6 MCP tools, plus (if the client supports it) a live, interactive MCP Apps viewer.

Credit

All emulation is OpalEmu (source) running EmulatorJS cores. This project only adds the MCP bridge around it; it contributes no emulation code of its own.

Related MCP server: MCP GameBoy Server

License

AGPL-3.0, the same license as OpalEmu (see LICENSE).

Architecture

LLM Agent (Claude, GPT, ...)
    │ MCP protocol (stdio)
    ▼
Node.js MCP server  (this project)
    │                              │
    │ WebSocket                    │ HTML resource (MCP Apps, optional)
    ▼                              ▼
Playwright-controlled Chromium     MCP client's own sandboxed iframe
    │ runs the real OpalEmu page   (client/mcp-app.ts, a *separate*
    │ (served by this project's    browser context; talks back to this
    │ own Express server)          server's tools via the App Bridge,
    ▼                              never touches the emulator directly)
window.EJS_emulator
(EmulatorJS instance, the game
actually runs here)

Two independent browser contexts are involved, and it's worth being explicit about why:

  • The Playwright tab (client/agent.ts injected into it) is where the game actually runs. It drives window.EJS_emulator directly and is the only place OpalEmu's real, stateful emulator instance exists.

  • The MCP Apps viewer (client/mcp-app.ts), if the connecting MCP client supports it, renders in the client's own sandboxed iframe, a completely different, unrelated browser context. It never touches the emulator directly; every button press or screenshot request goes through App.callServerTool(), which the host proxies to this server's real tools, the same tools the LLM calls. It's a control surface, not a second embed of the OpalEmu page (embedding the raw page there would boot a second, disconnected, unloaded emulator instance).

Auto-pause

The emulator is paused whenever no tool call is in flight. Each tool resumes play for exactly as long as it needs (a couple of frames for a button press, an exact frame count for skip_frames, however long a fresh core download takes for load_rom), then pauses again before returning a screenshot. This is what lets an agent "think" between moves without the game running unattended and missing its own inputs.

Tools

Tool

Description

list_roms

Lists ROM files available on the server (from --roms-dir).

load_rom

Loads a ROM by name (from list_roms) or by romBase64 + fileName. Returns the first screenshot once booted.

reset_emulator

Hard-resets the current game.

get_current_screen

Returns the current frame as a PNG, without changing emulator state.

control_emulator

Presses or releases a button (a, b, up, start, l2, etc.). Hold a direction across multiple skip_frames calls by sending state: "down" once and state: "up" later.

skip_frames

Advances by an exact number of core-internal frames (not wall-clock), for letting animations or held inputs play out.

All tools except list_roms are also registered as MCP Apps app tools, so a supporting client can render the live viewer regardless of which one is called first.

Setup

Prerequisites:

  • Node.js 20+

  • A built checkout of OpalEmu. By default this project looks for it as a sibling directory (../OpalEmu/dist); use --opalemu-dist <path> for any other layout. It only ever reads from there, never writes.

# 1. Build OpalEmu itself (the emulator this wraps)
git clone https://github.com/thevalmarch/opalemu ../OpalEmu
cd ../OpalEmu && npm install && npm run build && cd -

# 2. Build this project
npm install
npx playwright install chromium
npm run build

If OpalEmu's build isn't found, the server says so explicitly at startup, including the path it looked in.

ROMs

No ROMs are included, and none are downloaded. You supply your own game files, which you should already legally own. Drop them into roms/ (or point elsewhere with --roms-dir <path>) and list_roms will pick up anything with a recognized extension. Nothing in roms/ is committed to git.

Run standalone

npm start                    # headed browser by default, so you can watch it play
npm start -- --headless      # for CI / headless environments

CLI flags (all optional): --opalemu-dist <path>, --roms-dir <path>, --http-port <port> (default 4173), --ws-port <port> (default 4174), --headless.

Connect to Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "opalemu": {
      "command": "node",
      "args": ["/absolute/path/to/opalemu-mcp/dist/index.js"]
    }
  }
}

Use absolute paths: Claude Desktop spawns MCP servers without a working directory set.

Compatibility with OpalEmu

Tested against OpalEmu v1.1.0 (776874a).

OpalEmu and this project are separate repos with independent versions on purpose: different concerns, different release cadences. In practice, most of what this project depends on isn't OpalEmu-specific at all: button indices, frame counting, screenshot capture, and gameManager.restart() all come from EmulatorJS's stable CDN bundle, a third-party dependency OpalEmu itself just configures. OpalEmu releases mostly don't touch any of that.

The coupling that is real, and worth knowing about before bumping the sibling checkout:

  • dist/index.html's structure. src/http/static.ts injects agent.js with a literal </body> string-replace. Breaks if OpalEmu's build output changes shape.

  • The drop-event loading contract. load_rom works by dispatching a synthetic drag-drop event that OpalEmu's useDragDrop.ts listens for on window. Breaks if OpalEmu changes how files get loaded (e.g. drops the drag-drop path in favor of file-input-only).

  • The extension→system list in src/roms/store.ts. A ported copy of OpalEmu's own list, used only for list_roms's display labels, never for actually loading a ROM (OpalEmu's own in-page detection is authoritative there). Drifts silently, not a breakage, if OpalEmu adds systems.

  • COOP/COEP headers. Mirrored from OpalEmu's own vite/vercel config. Breaks threaded cores (n64, psx, etc.) if OpalEmu's requirements change.

When you update the OpalEmu checkout: rerun npm run test:e2e and npm run test:mcp against it (they boot a real OpalEmu build and exercise the full tool path), then bump the version line above if they pass.

Development

npm run dev            # run from source via tsx, no build step
npm run test:e2e       # scripted check against a real ROM, no LLM/MCP client needed
npm run test:mcp       # spawns the real server and talks real MCP stdio JSON-RPC to it
npm run test:mcp-app   # verifies the MCP Apps ui:// resource is registered and well-formed

All three need a ROM in roms/; they use whichever one they find first, so any test file works. test:mcp and test:mcp-app build first (npm run build) since they spawn the compiled server.

Every script takes the same --http-port / --ws-port flags as the server. The two smoke tests already default to 4193/4194 so they don't collide with a running instance; pass the flags explicitly if you need something else:

npm run test:mcp -- --http-port 5000 --ws-port 5001

Known limitations

  • Ambiguous disc formats. .bin/.iso files that OpalEmu can't confidently identify (e.g. it can't tell PSX from Sega CD) normally prompt the user with a picker dialog. There's no automated path through that dialog here, so load_rom will time out on such a file. Unambiguous formats (cartridge-based systems, clearly-identified discs) are unaffected.

  • First load per system downloads a core. EmulatorJS cores (5-30MB) download from cdn.emulatorjs.org on first use per system; load_rom accounts for this with a generous timeout, and a persistent browser profile (.playwright-profile/) means it only happens once.

  • One browser tab, one game at a time. A second load_rom call replaces the current session; there's no multi-instance support.

Install Server
A
license - permissive license
A
quality
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

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/thevalmarch/opalemu-mcp'

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