Opal Emu MCP
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Opal Emu MCPLoad the first available ROM and show me the screen."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.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. 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 |
| Lists ROM files available on the server (from |
| Loads a ROM by |
| Hard-resets the current game. |
| Returns the current frame as a PNG, without changing emulator state. |
| Presses or releases a button ( |
| 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 buildIf 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 environmentsCLI 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.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 ported copy of OpalEmu's own list, used only forlist_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-formedAll 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 5001Known limitations
Ambiguous disc formats.
.bin/.isofiles 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.One browser tab, one game at a time. A second
load_romcall replaces the current session; there's no multi-instance support.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Tools
- control_emulatorAApp
- get_current_screenAApp
- list_romsA
- load_romAApp
- reset_emulatorAApp
- skip_framesAApp
Related MCP Servers
- AlicenseDqualityDmaintenanceAn MCP server that enables LLMs to 'see' what's happening in browser-based games and applications through vectorized canvas visualization and debug information.Last updated1853MIT
- AlicenseAqualityDmaintenanceA Model Context Protocol server that enables LLMs to interact with a GameBoy emulator, providing tools for controlling the GameBoy, loading ROMs, and retrieving screen frames.Last updated1331MIT
- Alicense-qualityDmaintenanceA Model Context Protocol server that allows LLMs to interact with Game Boy games through PyBoy emulation, providing capabilities to load ROMs, control games, capture screens, save/load states, and maintain game knowledge.Last updated1MIT
- AlicenseAqualityCmaintenanceAn MCP server for the Pyxel retro game engine that enables AI models to autonomously run, verify, and iterate on retro game programs. It includes tools for visual verification through screenshots, sprite and layout analysis, and audio rendering.Last updated1424MIT
Related MCP Connectors
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
Cloud-hosted MCP server for durable AI memory
MCP server for AI dialogue using various LLM models via AceDataCloud
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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