opalemu-mcp
Health Warn
- License — License: AGPL-3.0
- No description — Repository has no description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Opal Emu MCP
Let LLM agents play retro games.
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.
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.tsinjected into it) is where the game actually runs. It driveswindow.EJS_emulatordirectly 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 throughApp.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. A button press resumes it briefly; skip_frames runs until at least the requested number of core frames have advanced and reports the measured count. Frame polling can overshoot the target. The emulator pauses again before returning a screenshot, so it does not keep running while an agent is thinking.
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 last captured frame as a PNG, without advancing the game. |
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 until at least the requested number of core frames has elapsed, then reports the actual count. |
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 ci
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.
Local ROM files larger than 128 MiB are rejected before loading. Direct romBase64 input is limited to 6 MiB decoded so its Base64 and JSON-RPC message fit below the MCP SDK's default 10 MiB stdio limit. Symlinks in the ROM directory are not listed or loaded. Large disc images may need a future streaming path; this release keeps ROM transfer bounded in memory.
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>, --playwright-profile-dir <path>, --http-port <port> (default 4173), --ws-port <port> (default 4174), --headless. The browser profile defaults to .playwright-profile/ in this checkout.
The HTTP and WebSocket listeners bind to 127.0.0.1. The Playwright browser receives a fresh bridge token at startup. The token is not served by the HTTP page and is not intended for other clients. Do not expose these ports through a proxy or port forward.
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
Originally tested against OpalEmu commit 776874a, whose package version was 1.1.0. The published v1.1.0 tag is the following commit, 800755a. The v1.1.0 hardening tests also passed with a clean local OpalEmu v1.2.3 checkout at 3df680cd11b40f4590fb64c879e14ea253d5a0c1.
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.tsinjectsagent.jswith a literal</body>string-replace. Breaks if OpalEmu's build output changes shape.- The
drop-event loading contract.load_romworks by dispatching a synthetic drag-drop event that OpalEmu'suseDragDrop.tslistens for onwindow. 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 small local discovery list determines whatlist_romsandload_rom({name})can find. OpalEmu's in-page detection still chooses the actual system. When OpalEmu adds formats, local discovery must be reviewed too. - 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
npm run test:security # network, bridge, and ROM safety regression tests
npm run test:browser # boots the local OpalEmu build and checks the authenticated browser bridge
test:e2e and test:mcp need a ROM in roms/; they use whichever one they find first. test:mcp-app checks the UI resource without a ROM. Build first with npm run build before running test:mcp-app, since it spawns the compiled server. test:security does not need a ROM or browser. test:browser needs a built OpalEmu checkout and Playwright Chromium, but no ROM.
The emulator and MCP smoke scripts accept the server's --http-port and --ws-port flags. The two MCP smoke tests default to 4193/4194 so they do not 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, or.chdfiles 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, soload_romwill 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.orgon first use per system;load_romaccounts for this with a generous timeout, and a persistent browser profile (.playwright-profile/) means it only happens once. - Stale browser profile data. Cached assets in an older profile may prevent a game from booting after an OpalEmu or EmulatorJS update. Use
--playwright-profile-dir <new-path>to try a fresh profile without deleting the old one. - MCP client deadlines. A first
load_rommay need up to 100 seconds, while some MCP clients default to a shorter tool-call deadline. Increase the client's timeout for cold core downloads if it supports that setting. - One browser tab, one game at a time. A second
load_romcall replaces the current session; there's no multi-instance support. - Frame targets are approximate.
skip_framesmeasures core frames after each display callback and may advance beyond the requested count. Its result states the actual count; it fails if the target is not reached before timeout. - EmulatorJS assets are controlled by OpalEmu. The separate OpalEmu checkout currently references the floating
stable/dataCDN path. This project cannot pin its loader and cores without changing OpalEmu or rewriting its built assets. Review that upstream dependency before deployments that require reproducible emulator assets.
Security
See SECURITY.md for private vulnerability reporting guidance. The local HTTP and WebSocket ports are intended only for the Playwright browser started by this process.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found