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.

Available Tools

6 tools
control_emulatorPress or release a buttonA

Presses or releases a controller button. The emulator briefly resumes just long enough for the input to register, then pauses again — so it never runs unattended while you're "thinking". To hold a direction across multiple skip_frames() calls, send state="down" once and state="up" later.

ParametersJSON Schema
NameRequiredDescriptionDefault
stateYes"down" to press, "up" to release.
buttonYesButton to press/release.
playerNoController port, 0-indexed. Defaults to player 1.

TDQS

A4.4/5.0
Behavior4/5

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

With no annotations, the description carries the transparency burden. It discloses the critical behavior that the emulator only runs briefly to register input and then pauses, which prevents unattended execution. It also explains how state='down'/'up' works for holding inputs.

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 two sentences, concise and front-loaded. The first sentence states the core action, and the second adds a crucial usage pattern. No wasted words.

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

Completeness4/5

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

For a simple input tool with no output schema, this description covers the action, the behavior of the emulator, and how to combine with skip_frames. It doesn't mention return values, but they are likely trivial. The only minor gap is not stating any prerequisites (e.g., ROM loaded) but siblings imply context.

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

Parameters4/5

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

Schema descriptions already cover each parameter, and the description adds value by illustrating the practical use of state for holding directions. This enriches the meaning of state beyond the basic enum description.

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

Purpose5/5

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

The description uses a specific verb ('presses or releases') and identifies the resource ('controller button'). It clearly distinguishes this from sibling tools like get_current_screen and skip_frames by focusing on input control.

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

Usage Guidelines4/5

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

The description explains the emulator's resume-pause behavior and gives explicit guidance for holding a direction across skip_frames() calls. It doesn't explicitly contrast with alternatives, but the context makes it the obvious choice for button input.

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

get_current_screenGet current screenA

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

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.2/5.0
Behavior4/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It usefully adds 'without changing emulator state,' indicating a non-mutating operation, and specifies the output format. However, it does not clarify whether the PNG is returned directly in the response or saved to a location, which would be more complete.

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 a single sentence of 15 words, directly front-loaded with the action and resource. Every word contributes meaning (current, emulator, frame, PNG, without changing state), with no filler or redundancy.

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

Completeness4/5

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

For a simple tool with no parameters, no output schema, and no annotations, the description is largely complete. It clearly conveys the core function and a key guarantee (no state change). However, it could be more explicit about the return mechanism (e.g., 'returns a PNG image'), which is relevant given the absence of an output schema.

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

Parameters4/5

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

The tool has zero parameters, so the baseline is 4. There are no parameter semantics to explain, and the schema is fully covered by an empty properties object. The description appropriately focuses on the action rather than parameters.

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

Purpose5/5

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

The description clearly states the tool captures the current emulator frame as a PNG image, using the specific verb 'Captures' and identifying both the resource ('current emulator frame') and the output format ('PNG image'). This distinguishes it from sibling tools like load_rom or reset_emulator, which handle different concerns.

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 context (obtaining a screenshot of the current emulator state) but does not explicitly state when to use this tool vs alternatives or when not to use it. No alternative tools are mentioned, so the agent must infer from the tool's name and siblings.

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

list_romsList ROMsA

Lists ROM files available on the server (from its --roms-dir). Use the returned "name" with load_rom.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.2/5.0
Behavior3/5

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

With no annotations, the description carries the full burden. It discloses the data source (--roms-dir) and implies a non-destructive read operation via 'Lists'. However, it does not explicitly state there are no side effects or that it is read-only, though this is strongly implied. Adequate but minimal.

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 two short sentences with no wasted words. The first sentence front-loads the action and source, and the second provides a direct usage pointer. Highly concise and well-structured.

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

Completeness4/5

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

For a simple tool with no parameters and no output schema, the description covers all essential info: what it lists, where from, and how to use the result. It could explicitly mention the output format (e.g., list of objects with a 'name' field), but this is implied by 'Use the returned "name"'. Sufficiently complete.

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

Parameters4/5

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

The tool has zero parameters, so the schema is empty and the baseline is 4. The description adds semantic context by explaining that the returned 'name' feeds into load_rom, which clarifies the purpose of the tool's output and indirectly helps with the sibling's parameter.

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

Purpose5/5

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

The description clearly states the action ('Lists') and the resource ('ROM files') with a specific source ('--roms-dir'). It also distinguishes the tool from siblings by linking its output to load_rom, making its role unambiguous.

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

Usage Guidelines4/5

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

The second sentence provides explicit usage context: the returned 'name' should be used with load_rom. This indicates when to use the tool (before loading a ROM). There are no alternatives to exclude, so this is clear and sufficient.

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

load_romLoad ROMA

Loads a ROM into the emulator and starts the game, paused, ready for the next tool call. Provide either "name" (from list_roms) or "romBase64" + "fileName" for a ROM not in the library. Returns the first screenshot once the game has booted — this can take a while the first time a system's core is downloaded.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNoA ROM name or fileName from list_roms().
fileNameNoRequired with romBase64 — needs a real extension (e.g. "game.nes") for system detection.
romBase64NoBase64-encoded ROM bytes, for a ROM not in the library.

TDQS

A4.5/5.0
Behavior4/5

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

With no annotations, the description carries the transparency burden. It discloses that the game starts paused, returns the first screenshot after boot, and may be slow on first core download. These behavioral traits go beyond a simple 'loads a ROM' statement, though it does not mention failure modes or side effects.

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 two sentences, front-loaded with the core action, then efficiently covers the two input modes and the return behavior. Every sentence contributes meaningful information without redundancy.

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

Completeness5/5

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

For a tool with 3 parameters, no output schema, and no annotations, this description covers the input options, the resulting state (paused), the return value (screenshot), and a timing caveat (core download). It provides an agent with enough context to invoke the tool correctly.

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

Parameters4/5

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

The schema already provides 100% parameter coverage, but the description adds crucial semantics: the mutually exclusive use of name vs. romBase64+fileName, and the requirement for fileName to have a real extension for system detection. This enriches the bare schema definitions.

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

Purpose5/5

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

The description clearly states the tool's function: 'Loads a ROM into the emulator and starts the game, paused.' It also distinguishes the tool from siblings like list_roms by referencing it and focusing on the loading action, making the purpose unambiguous.

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

Usage Guidelines4/5

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

The description provides explicit usage guidance: use 'name' from list_roms for library ROMs, or romBase64+fileName for external ROMs. It does not explicitly contrast with sibling tools like reset_emulator or control_emulator, but the pairing with list_roms gives sufficient context for when to invoke load_rom.

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

reset_emulatorReset emulatorA

Hard-resets the currently loaded game (equivalent to a console reset button). Returns a fresh screenshot.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.3/5.0
Behavior4/5

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

With no annotations, the description carries the full burden. It discloses the hard-reset behavior and the return value ('fresh screenshot'). However, it doesn't mention potential loss of unsaved progress or other side effects, which are important for a reset action.

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 two sentences, front-loaded with the core action, and every word is useful. It's concise without being vague.

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

Completeness5/5

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

For a simple tool with no parameters and no output schema, the description is complete. It explains what happens and what the return value is, which is sufficient for the agent to use it correctly.

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

Parameters4/5

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

The tool has zero parameters, so the description doesn't need to explain them. The baseline of 4 is appropriate since there's nothing to document.

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

Purpose5/5

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

The description clearly states the tool's function: 'Hard-resets the currently loaded game' with a useful analogy to a console reset button. It uses a specific verb and resource, and it's distinct from sibling tools like load_rom or control_emulator.

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 the tool is used when you want to reset the game, but it doesn't explicitly state when to use it versus alternatives. There's no mention of alternative tools or exclusions, so it relies on the reader's interpretation.

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

skip_framesSkip framesA

Advances the emulator by exactly N core-internal frames (frame-accurate, not wall-clock), then pauses and returns a fresh screenshot. Use this to let held inputs or animations play out. Above ~180 frames, native fast-forward is used so long waits do not block on real time.

ParametersJSON Schema
NameRequiredDescriptionDefault
countYesNumber of frames to advance.

TDQS

A4.4/5.0
Behavior4/5

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

With no annotations, the description carries full burden. It discloses frame-accurate vs wall-clock, pausing, returning a screenshot, and the fast-forward threshold above ~180 frames. It stops short of describing all possible side effects, but is adequate for a simple operation.

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?

Three concise sentences, all carrying essential information. No redundancy or filler.

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

Completeness4/5

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

For a single-parameter tool with no output schema, it explains purpose, return value, and performance behavior. It does not cover error conditions or boundary edge cases, but those are not critical for the core use case.

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

Parameters4/5

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

Schema coverage is 100%, so baseline is 3, but the description adds 'core-internal frames (frame-accurate, not wall-clock),' clarifying count meaning beyond the schema. The fast-forward threshold also provides practical context for choosing a count.

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

Purpose5/5

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

The description clearly states the tool advances the emulator by exactly N core-internal frames, a specific verb+resource with unique scope. It also notes that it returns a screenshot, distinguishing it from sibling tools like get_current_screen.

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

Usage Guidelines4/5

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

Explicitly suggests 'Use this to let held inputs or animations play out,' giving concrete use cases. It does not mention alternatives or exclusions, but the context is clear enough for tool selection.

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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 6 tool updatesv0.1.0
    • First observedcontrol_emulator
    • First observedget_current_screen
    • First observedlist_roms
    • First observedload_rom
    • First observedreset_emulator
    • First observedskip_frames

TDQS

A4.4/5.0

Scored across 6 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listing, loading, resetting, capturing, controlling, and advancing frames. There is no overlap or ambiguity between any pair of tools.

Naming Consistency5/5

All tool names follow the verb_noun pattern (list_roms, load_rom, reset_emulator, get_current_screen, control_emulator, skip_frames). The style is completely consistent across the set.

Tool Count5/5

6 tools is well-scoped for an emulator MCP server, providing the essential operations without bloat. The count aligns with the domain's core workflow.

Completeness4/5

The core emulator loop is covered: loading ROMs, controlling input, advancing frames, resetting, and capturing screens. However, save/load state functionality is absent, which is a common and useful feature for emulator workflows, though not strictly required for basic operation.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers