Doramagic Project Pack · Human Manual
mcp-retroarch
Related topics: System Architecture, Quick Start Guide
Introduction
Related topics: System Architecture, Quick Start Guide
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: System Architecture, Quick Start Guide
Introduction
mcp-retroarch is a Model Context Protocol (MCP) server that bridges AI assistants (such as Claude) with the RetroArch emulator through its Network Control Interface (NCI). This enables AI-driven automation of retro gaming tasks including memory inspection, save state management, screenshot capture, and frame-by-frame execution control.
Project Overview
mcp-retroarch exposes RetroArch's NCI functionality as MCP tools, allowing AI agents to:
- Read and write emulated system memory
- Manage save states across slots
- Capture screenshots
- Control emulation execution (pause, frame advance, reset)
- Display notifications within the emulator
Key characteristics:
| Attribute | Value |
|---|---|
| Protocol | MCP over stdio |
| Transport to RetroArch | UDP (IPv4) |
| Default UDP port | 55355 |
| Language | TypeScript |
| MCP SDK version | ^1.12.0 |
| License | MIT |
Sources: README.md
Architecture
The system consists of three primary components communicating across two protocols:
graph TD
subgraph "MCP Client Layer"
A[Claude / AI Assistant]
end
subgraph "mcp-retroarch Process"
B[MCP SDK<br/>stdio JSON-RPC]
C[RetroArch Client<br/>src/retroarch.ts]
end
subgraph "RetroArch"
D[Network Control<br/>Interface]
end
A -->|"MCP JSON-RPC"| B
B -->|Tool calls| C
C -->|"UDP Datagrams"| D
D -->|"UDP Response"| C
C -->|"MCP Response"| B
B -->|"JSON-RPC"| AComponent Responsibilities
| Component | Role |
|---|---|
| MCP Client | Sends JSON-RPC requests via stdio |
| mcp-retroarch | Translates MCP tools to NCI UDP commands |
| RetroArch NCI | Executes commands on the running emulator |
Sources: src/index.ts:1-20
How It Works
Connection Flow
- Startup: The MCP server initializes and creates a UDP socket for RetroArch communication
- Background probe: An asynchronous connection attempt retrieves RetroArch version
- Ready signal: Server signals readiness via stderr and stdout
sequenceDiagram
participant RA as RetroArch
participant MCP as mcp-retroarch
participant Client as MCP Client
MCP->>MCP: Create UDP socket
Note over MCP: Background probe<br/>(fire-and-forget)
MCP->>RA: VERSION command
RA-->>MCP: RetroArch version
MCP->>Client: Ready notification (stdout)
Client->>MCP: Tool call (e.g., read_memory)
MCP->>RA: READ_CORE_MEMORY
RA-->>MCP: Memory bytes
MCP-->>Client: Hex dump responseSources: src/index.ts:8-17
UDP Communication Model
The RetroArch client implements two communication patterns:
| Pattern | Method | Use Case |
|---|---|---|
| Fire-and-forget | send(command) | Pause toggle, reset, screenshot |
| Query/Response | query(command) | Memory reads, status queries |
// Fire-and-forget: no response expected
async send(command: string): Promise<void>
// Query with timeout: awaits UDP response
async query(command: string): Promise<Buffer>
Timeout behavior: Queries default to a configurable timeout. If no response arrives, the promise rejects with a timeout error. The client maintains serial request semantics—one query must complete before the next begins.
Sources: src/retroarch.ts:43-64
Serial Request Constraint
The implementation enforces serial requests at the protocol level:
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
This means concurrent tool calls from the MCP client will queue or fail if queries overlap.
Sources: src/retroarch.ts:56-59
Prerequisites
Before using mcp-retroarch, ensure:
- RetroArch is installed with Network Commands enabled
- A libretro core and game are loaded
- Network Command Interface is active
Enabling Network Commands in RetroArch
GUI Method:
- Navigate to Settings → Network → Network Commands → ON
- Confirm Network Cmd Port is
55355(default)
Configuration File Method (retroarch.cfg):
network_cmd_enable = "true"
network_cmd_port = "55355"
Sources: README.md
Configuration
mcp-retroarch is configured via environment variables:
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Sources: README.md
Available Tools
The following MCP tools are exposed by mcp-retroarch:
Memory Operations
| Tool | Purpose | Address Space |
|---|---|---|
retroarch_read_memory | Read bytes from system memory map | libretro memory map |
retroarch_write_memory | Write bytes to system memory | libretro memory map |
retroarch_read_ram | Read bytes via CHEEVOS address space | Achievement space |
retroarch_write_ram | Write bytes via CHEEVOS space | Achievement space |
Memory read/write limitations:
- Maximum 4096 bytes per call (NCI single-datagram limit)
write_memoryreturns byte count;write_ramdoes not acknowledge
Emulation Control
| Tool | Purpose |
|---|---|
retroarch_pause_toggle | Toggle pause state |
retroarch_frame_advance | Step exactly one frame |
retroarch_reset | Hard reset the game |
State Management
| Tool | Purpose |
|---|---|
retroarch_save_state_current | Save to current slot |
retroarch_load_state_current | Load from current slot |
retroarch_load_state_slot | Load from explicit slot number |
retroarch_state_slot_plus | Increment slot pointer |
retroarch_state_slot_minus | Decrement slot pointer |
Utility
| Tool | Purpose |
|---|---|
retroarch_screenshot | Save screenshot to RetroArch's configured directory |
retroarch_show_message | Display notification overlay |
retroarch_get_status | Query current emulation state |
retroarch_get_config | Read a config parameter value |
Sources: src/tools.ts
Address Space Differences
Understanding the distinction between address spaces is critical:
| Address Space | Used By | Notes |
|---|---|---|
| libretro memory map | read_memory / write_memory | System-specific layout (e.g., GBA EWRAM at 0x02000000) |
| CHEEVOS space | read_ram / write_ram | RetroAchievements address conventions |
Example address mappings:
- GBA ROM:
0x08000000(memory map) vs0x000000(CHEEVOS) - SNES WRAM:
0x7E0000-0x7FFFFF(memory map) vs0x000000(CHEEVOS)
Sources: src/tools.ts
Verified Core Support
The following cores have been tested with mcp-retroarch:
| System | Core | read_memory | read_ram | Notes |
|---|---|---|---|---|
| Game Boy Advance | mgba_libretro | ✅ | ✅ | GBA interrupt vector at 0x0000 visible |
| NES | mesen_libretro | ✅ | ✅ | Full 16-bit address space; WRAM at 0x0000-0x07FF |
| NES | nestopia_libretro | ❌ | ✅ | No memory map; use read_ram |
| SNES | snes9x_libretro | ❌ | — | Memory map not exposed |
Sources: README.md
Fire-and-Forget Semantics
Most NCI commands do not receive acknowledgments from RetroArch. The response message from mcp-retroarch indicates only that the UDP datagram was sent, not that RetroArch received or acted on it.
Exceptions:
retroarch_write_memory— returns byte count (NCI acknowledges this command)retroarch_read_memory/retroarch_read_ram— returns actual data read
Verification strategy: For fire-and-forget commands, verify success by:
- Reading memory back with
read_ram - Checking state with
retroarch_get_status
Sources: CHANGELOG.md
Error Handling
Common Errors
| Error | Cause | Resolution |
|---|---|---|
RetroArch query timed out | Network Commands disabled or port mismatch | Verify network_cmd_enable = "true" and matching ports |
READ_CORE_MEMORY failed: no memory map defined | Core doesn't expose memory map | Use retroarch_read_ram instead |
READ_CORE_MEMORY failed: no descriptor for address | Address outside core's memory regions | Try different core or address |
UDP reliability note: UDP datagrams may be dropped under load even on loopback. If a single call times out, retry the operation.
Sources: README.md
Development
Project Structure
mcp-retroarch/
├── src/
│ ├── index.ts # MCP server entry point
│ ├── retroarch.ts # UDP client for NCI protocol
│ └── tools.ts # Tool definitions and handlers
├── package.json
├── tsconfig.json
└── .scratch/
└── smoke.cjs # Manual smoke test
Running Locally
npm install
npm run dev # tsc --watch (development mode)
node .scratch/smoke.cjs # Smoke test against running RetroArch
Sources: README.md, package.json
Limitations
| Feature | Status | Notes |
|---|---|---|
| Game-pad input | ❌ Not available | NCI doesn't expose input; see mcp-mgba for GBA input |
| Save to specific slot | Limited | Only current slot save; must walk to target slot |
| Screenshot directory | Not configurable via NCI | Check RetroArch GUI for configured path |
Sources: README.md
Related Projects
| Project | Purpose |
|---|---|
| mcp-mgba | GBA via mGBA's Lua bridge (includes input + screenshot) |
| mcp-pine | PCSX2 and PINE-speaking emulators |
Sources: README.md
Sources: README.md
Quick Start Guide
Related topics: Introduction, Configuration
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: Introduction, Configuration
Quick Start Guide
This guide walks you through setting up mcp-retroarch, a Model Context Protocol (MCP) server that enables AI assistants to interact with RetroArch through its Network Command Interface (NCI). The integration provides memory inspection, savestate management, screenshot capture, and emulator control via JSON-RPC over stdio.
Overview
mcp-retroarch bridges AI coding assistants (like Claude Code or Claude Desktop) with RetroArch by:
- Exposing RetroArch NCI commands as MCP tools
- Communicating over stdio (JSON-RPC) with the MCP client
- Sending UDP datagrams to RetroArch's Network Command Interface on port 55355
Sources: README.md
System Architecture
graph TD
A["MCP Client<br/>(Claude Code/Desktop)"] -->|stdio JSON-RPC| B["mcp-retroarch<br/>MCP Server"]
B -->|UDP :55355| C["RetroArch<br/>Network Commands"]
C -->|Emulator Control| D["Libretro Core<br/>+ Game ROM"]
B -->|Background Probe| C
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#e8f5e9Sources: src/index.ts:8-14, src/retroarch.ts
Prerequisites
| Requirement | Details |
|---|---|
| Node.js | Version compatible with @modelcontextprotocol/sdk ^1.12.0 |
| RetroArch | Version with Network Command Interface enabled |
| MCP Client | Claude Code or Claude Desktop |
| Network Commands | Must be enabled in RetroArch |
Sources: package.json:20-26, README.md:40-50
Step 1: Install mcp-retroarch
From npm (Recommended)
npm install -g mcp-retroarch
This makes the mcp-retroarch command globally available.
Sources: package.json:1-10
From Source
git clone https://github.com/dmang-dev/mcp-retroarch.git
cd mcp-retroarch
npm install
npm run build # or use npm run dev for watch mode
Sources: README.md:100-105
Step 2: Enable Network Commands in RetroArch
RetroArch's NCI must be enabled before mcp-retroarch can communicate with it.
Via GUI
- Open RetroArch
- Navigate to Settings → Network → Network Commands
- Set Network Commands to ON
- Confirm Network Cmd Port is
55355(default)
Via Configuration File
Edit retroarch.cfg (located in RetroArch's config directory):
network_cmd_enable = "true"
network_cmd_port = "55355"
Sources: README.md:40-50
Verify Setup
Launch RetroArch and load any libretro core with a game. The Network Command Interface is always-on once enabled—no additional scripts required.
Step 3: Register with Your MCP Client
Claude Code
Register retroarch as a user-level MCP server:
claude mcp add retroarch --scope user mcp-retroarch
Verify registration:
claude mcp list
# retroarch: mcp-retroarch - ✓ Connected
Sources: README.md:52-60
Claude Desktop
Edit the platform-specific configuration file:
| Platform | Config Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
Add the mcpServers section:
{
"mcpServers": {
"retroarch": {
"command": "mcp-retroarch"
}
}
}
Restart Claude Desktop after editing.
Sources: README.md:62-78
Step 4: Configure Environment Variables
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Set these before running Claude Code or Claude Desktop if your RetroArch runs on a different host or port:
export RETROARCH_HOST=192.168.1.100
export RETROARCH_PORT=55355
Sources: README.md:80-85
Available Tools
Once connected, the following MCP tools become available:
Emulator Control
| Tool | Purpose |
|---|---|
retroarch_get_status | Query running state, system, game, CRC32 |
retroarch_pause_toggle | Toggle pause on/off |
retroarch_frame_advance | Advance exactly one frame |
retroarch_reset | Hard reset the running game |
retroarch_show_message | Display notification on RetroArch window |
Memory Operations
| Tool | Description |
|---|---|
retroarch_read_memory | Read from libretro system memory map |
retroarch_write_memory | Write to libretro system memory map |
retroarch_read_ram | Read from CHEEVOS (achievements) address space |
retroarch_write_ram | Write to CHEEVOS address space |
Savestate Management
| Tool | Purpose |
|---|---|
retroarch_save_state_current | Save to currently-selected slot |
retroarch_load_state_current | Load from currently-selected slot |
retroarch_load_state_slot | Load from explicit slot number |
retroarch_state_slot_plus | Increment slot pointer |
retroarch_state_slot_minus | Decrement slot pointer |
Media & Config
| Tool | Purpose |
|---|---|
retroarch_screenshot | Save screenshot to RetroArch's screenshot directory |
retroarch_get_config | Read a RetroArch configuration parameter |
Sources: README.md:1-30, src/tools.ts
Step 5: Smoke Test
Verify connectivity by running the smoke test script against a running RetroArch:
node .scratch/smoke.cjs
Sources: README.md:107-110
First Commands to Try
Check Emulator Status
retroarch_get_status
Expected output:
State: playing
System: snes9x_libretro
Game: Super Metroid (USA).sfc
CRC32: 12345678
Pause and Read Memory
retroarch_pause_toggle
retroarch_read_memory(address=0x7E0000, length=16)
Take a Screenshot
retroarch_screenshot
Sources: src/tools.ts:1-50
Verified Core Compatibility
| System | Core | read_memory | read_ram | Notes |
|---|---|---|---|---|
| Game Boy Advance | mgba_libretro | ✅ | ✅ | GBA interrupt vector at 0x0000 |
| NES | mesen_libretro | ✅ | ✅ | Full 16-bit NES address space |
| NES | nestopia_libretro | ❌ | ✅ | CHEEVOS only |
| SNES | snes9x_libretro | ❌ | — | Memory map not exposed |
Sources: README.md:30-45
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
RetroArch query timed out | Network Commands not enabled, or port mismatch. Confirm network_cmd_enable = "true" in retroarch.cfg |
READ_CORE_MEMORY failed: no memory map defined | Core doesn't advertise memory map. Try retroarch_read_ram as fallback |
READ_CORE_MEMORY failed: no descriptor for address | Address outside core's memory regions |
| Screenshots don't appear | Check RetroArch's screenshot directory via Settings → Directory → Screenshot |
| Can't save to specific slot | NCI limitation—use state_slot_plus/state_slot_minus to walk to target slot |
Sources: README.md:115-130
Next Steps
- Review docs/RECIPES.md for end-to-end examples
- Explore related MCP servers: mcp-mgba for GBA input + screenshots
- Consult RetroArch NCI documentation for protocol details
Sources: README.md
Sources: README.md
System Architecture
Related topics: Introduction, Data Flow
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: Introduction, Data Flow
System Architecture
Overview
mcp-retroarch is a Model Context Protocol (MCP) server that bridges MCP clients (such as Claude Code or Claude Desktop) with RetroArch's Network Control Interface (NCI). It enables AI assistants to interact with running emulated games through memory inspection, state manipulation, and emulator control.
Sources: README.md
High-Level Architecture
The system follows a three-layer architecture:
graph TD
subgraph "Client Layer"
MCP[MCP Client<br/>Claude Code / Claude Desktop]
end
subgraph "Bridge Layer"
MCP_Server[MCP Server<br/>mcp-retroarch]
Tools[MCP Tools<br/>tools.ts]
Transport[UDP Transport<br/>retroarch.ts]
end
subgraph "Target Layer"
RA[RetroArch<br/>Running Emulator]
NCI[Network Control<br/>Interface]
end
MCP -->|"stdio JSON-RPC"| MCP_Server
MCP_Server --> Tools
Tools --> Transport
Transport -->|"UDP :55355"| NCI
NCI --> RASources: README.md
Component Architecture
Layer 1: MCP Server Entry Point
File: src/index.ts
The main entry point initializes the MCP server using the @modelcontextprotocol/sdk package and establishes a background connectivity probe to RetroArch.
// Simplified initialization flow
const server = new Server(
{ name: "mcp-retroarch", version: "0.1.2" },
{ capabilities: { tools: {} } }
);
ra.connect()
.then(() => ra.getVersion())
.then((v) => process.stderr.write(`[mcp-retroarch] connected to ${ra.describeTarget()} — RetroArch ${v}\n`))
.catch((err) => process.stderr.write(`[mcp-retroarch] note: RetroArch not reachable yet...\n`));
Key responsibilities:
- Initialize MCP server with stdio transport
- Register all tool handlers
- Perform fire-and-forget connectivity probe on startup
- Handle fatal errors gracefully
Sources: src/index.ts
Layer 2: UDP Transport Module
File: src/retroarch.ts
The transport layer manages UDP socket communication with RetroArch's NCI. It implements a serial query pattern where only one request can be in-flight at a time.
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
participant Transport as UDP Transport
participant RA as RetroArch NCI
Client->>Server: tool_call
Server->>Transport: send(query)
Transport->>RA: UDP datagram
RA-->>Transport: UDP response
Transport-->>Server: Buffer
Server-->>Client: JSON responseConnection Management:
| Method | Purpose |
|---|---|
connect() | Initialize UDP socket, bind to random port |
disconnect() | Close socket and cleanup |
send(command) | Fire-and-forget send (for hotkey commands) |
query(command) | Send and await response with timeout |
Configuration parameters:
| Parameter | Default | Description |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Sources: src/retroarch.ts, README.md
Layer 3: MCP Tools Definition
File: src/tools.ts
All available MCP tools are defined with JSON Schema input validation and descriptive documentation following a PURPOSE / USAGE / BEHAVIOR / RETURNS template.
Tool Categories:
| Category | Tools |
|---|---|
| Memory | retroarch_read_memory, retroarch_write_memory, retroarch_read_ram, retroarch_write_ram |
| State | retroarch_save_state_current, retroarch_load_state_current, retroarch_load_state_slot, retroarch_state_slot_plus, retroarch_state_slot_minus |
| Control | retroarch_pause_toggle, retroarch_frame_advance, retroarch_reset |
| Media | retroarch_screenshot, retroarch_show_message |
| Info | retroarch_get_status, retroarch_get_config, retroarch_get_version |
Sources: src/tools.ts
Data Flow
Memory Read Flow
graph LR
A[Client:<br/>retroarch_read_memory] --> B[Validate:<br/>address, length]
B --> C[Send:<br/>READ_CORE_MEMORY]
C --> D{RetroArch<br/>Memory Map<br/>Available?}
D -->|Yes| E[Return bytes<br/>via UDP]
D -->|No| F[Error:<br/>no memory map defined]
E --> G[Format hex dump<br/>ADDR [N bytes]: XX XX...]Memory Write Flow
graph LR
A[Client:<br/>retroarch_write_memory] --> B[Validate:<br/>address, bytes array]
B --> C[Send:<br/>WRITE_CORE_MEMORY]
C --> D[RetroArch<br/>Acknowledges<br/>byte count]
D --> E[Return count<br/>to client]Sources: src/retroarch.ts, src/tools.ts
Command Mapping
The following table shows how MCP tools map to RetroArch NCI commands:
| MCP Tool | NCI Command | Response Pattern |
|---|---|---|
retroarch_get_version | VERSION | String version |
retroarch_get_status | GET_STATUS | GET_STATUS {state} {system},{game},crc32={crc} |
retroarch_read_memory | READ_CORE_MEMORY {addr} {len} | Binary bytes |
retroarch_write_memory | WRITE_CORE_MEMORY {addr} {bytes} | {count} bytes written |
retroarch_read_ram | READ_CORE_RAM {addr} {len} | Binary bytes (CHEEVOS) |
retroarch_write_ram | WRITE_CORE_RAM {addr} {bytes} | Fire-and-forget |
retroarch_pause_toggle | PAUSE_TOGGLE | Fire-and-forget |
retroarch_frame_advance | FRAMEADVANCE | Fire-and-forget |
retroarch_reset | RESET | Fire-and-forget |
retroarch_save_state_current | SAVE_STATE | Fire-and-forget |
retroarch_load_state_current | LOAD_STATE | Fire-and-forget |
retroarch_load_state_slot | LOAD_STATE {slot} | Fire-and-forget |
retroarch_screenshot | SCREENSHOT | Fire-and-forget |
retroarch_show_message | SET_MESSAGE "{msg}" | Fire-and-forget |
Sources: src/retroarch.ts
Transport Characteristics
Serial Query Pattern
The transport layer enforces serial query execution:
async query(command: string): Promise<Buffer> {
if (!this.socket) await this.connect();
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
// ... timeout handling
}
Key behaviors:
- Only one UDP query can be in-flight at a time
- Queries timeout after configurable
timeoutMs(default: 5000ms) - Fire-and-forget commands (
send()) bypass the serial queue
Error Handling
| Error Condition | Cause | Resolution |
|---|---|---|
RetroArch query timed out | Network Commands not enabled or port mismatch | Verify network_cmd_enable = "true" in retroarch.cfg |
no memory map defined | Core doesn't advertise system memory map | Use retroarch_read_ram as fallback |
no descriptor for address | Address outside core's memory regions | Use different core or address |
query already in flight | Multiple concurrent queries attempted | Wait for previous query to complete |
Sources: README.md, src/retroarch.ts
Memory Address Spaces
mcp-retroarch exposes two distinct memory address spaces:
System Memory Map (`read_memory` / `write_memory`)
Uses READ_CORE_MEMORY / WRITE_CORE_MEMORY via the libretro core's system memory map.
| System | Typical Address Range |
|---|---|
| GBA EWRAM | 0x02000000 - 0x0203FFFF |
| SNES WRAM | 0x7E0000 - 0x7FFFFF |
| Genesis 68K RAM | 0xFF0000 - 0xFFFFFF |
CHEEVOS Address Space (`read_ram` / `write_ram`)
Uses READ_CORE_RAM / WRITE_CORE_RAM via the RetroAchievements address space. Uses different conventions per system (e.g., SNES WRAM starts at 0x000000 in CHEEVOS space).
Sources: src/tools.ts
Dependencies
File: package.json
| Dependency | Version | Purpose |
|---|---|---|
@modelcontextprotocol/sdk | ^1.12.0 | MCP server implementation |
@types/node | ^22.0.0 | TypeScript definitions |
typescript | ^5.5.0 | Build tooling |
Sources: package.json
Tested Cores Compatibility
| System | Core | read_memory | read_ram | Notes |
|---|---|---|---|---|
| Game Boy Advance | mgba_libretro | ✅ | ✅ | GBA interrupt vector at 0x0000 |
| NES | mesen_libretro | ✅ | ✅ | Full 16-bit address space |
| NES | nestopia_libretro | ❌ | ✅ | CHEEVOS only |
| SNES | snes9x_libretro | ❌ | N/A | Memory map not exposed |
| PSX | swanstation_libretro | ❌ | ✅ | Use read_ram |
Sources: README.md
Project Structure
mcp-retroarch/
├── src/
│ ├── index.ts # MCP server entry point
│ ├── retroarch.ts # UDP transport & NCI protocol
│ └── tools.ts # Tool definitions & handlers
├── docs/
│ └── RECIPES.md # End-to-end usage examples
├── package.json
├── tsconfig.json
└── README.md
Sources: README.md
Source: https://github.com/dmang-dev/mcp-retroarch / Human Manual
Data Flow
Related topics: System Architecture, Memory Read/Write Operations
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: System Architecture, Memory Read/Write Operations
Data Flow
This page documents the complete data flow through the mcp-retroarch system, from MCP client requests through UDP communication with RetroArch's Network Control Interface (NCI).
Architecture Overview
mcp-retroarch bridges two distinct communication protocols:
| Layer | Transport | Protocol |
|---|---|---|
| MCP Client ↔ mcp-retroarch | stdio | JSON-RPC 2.0 |
| mcp-retroarch ↔ RetroArch | UDP (port 55355) | NCI text commands |
Sources: README.md:1-50
graph TB
subgraph "MCP Layer (stdio)"
A["MCP Client<br/>(Claude Code, Claude Desktop)"]
end
subgraph "mcp-retroarch Process"
B["src/index.ts<br/>MCP Server Entry"]
C["src/tools.ts<br/>Tool Handlers"]
D["src/retroarch.ts<br/>RetroArch Client"]
end
subgraph "RetroArch"
E["Network Control Interface<br/>(UDP :55355)"]
end
A -->|"JSON-RPC 2.0<br/>stdio"| B
B -->|routes| C
C -->|invokes methods| D
D -->|"NCI commands<br/>UDP"| E
E -->|"UDP response"| DRequest-Response Flow
1. MCP Request Ingress
When an MCP client invokes a tool (e.g., retroarch_read_memory), the following occurs:
- The client sends a JSON-RPC 2.0 request via stdio
src/index.tsreceives the request and routes it to the appropriate tool handler insrc/tools.ts- The tool handler validates parameters against the input schema
Sources: src/tools.ts:1-100
2. Tool Handler Processing
Each tool maps to a specific case in the tool handler switch statement:
case "retroarch_read_memory": {
const bytes = await ra.readMemory(p.address as number, p.length as number);
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" ");
return ok(`${addrHex(p.address as number)} [${bytes.length} bytes]:\n${hex}`);
}
Sources: src/tools.ts:20-30
3. UDP Query Execution
The RetroArch class in src/retroarch.ts handles all network communication:
sequenceDiagram
participant MCP as MCP Client
participant Server as mcp-retroarch
participant Socket as UDP Socket
participant RA as RetroArch NCI
MCP->>Server: retroarch_read_memory(0x02000000, 256)
Server->>Socket: send("READ_CORE_MEMORY 02000000 0100")
Socket->>RA: UDP datagram to 127.0.0.1:55355
RA-->>Socket: "READ_CORE_MEMORY OK [256 bytes]"
Socket-->>Server: Buffer response
Server-->>MCP: JSON-RPC response with hex dumpSources: src/retroarch.ts:1-50
Socket Communication Model
Connection Lifecycle
graph LR
A[Lazy Connect] -->|on first query| B[Socket Created]
B --> C[bind to random port]
C --> D[ready for send/receive]
D --> E[on tool call]
E --> F[send UDP datagram]
F --> G[wait for response]
G -->|timeout| H[Error: query timed out]
G -->|response| I[resolve promise]Sources: src/retroarch.ts:20-45
Query Serialization
The query() method enforces serial execution:
async query(command: string): Promise<Buffer> {
if (!this.socket) await this.connect();
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
return new Promise<Buffer>((resolve, reject) => {
let timer: NodeJS.Timeout | null = setTimeout(() => {
this.pending = null;
reject(new Error(
`RetroArch query "${command.split(" ")[0]}" timed out after ${this.timeoutMs}ms ` +
`— is RetroArch running with Network Commands enabled?`,
));
}, this.timeoutMs);
this.pending = (data) => {
if (timer) { clearTimeout(timer); timer = null; }
resolve(data);
};
// ...
});
}
Sources: src/retroarch.ts:70-95
Key characteristics:
- Lazy connection: Socket is created on first query, not at startup
- Serial queries: Only one pending query at a time (prevents response ambiguity)
- Timeout handling: Configurable timeout with cleanup on resolution or expiry
Fire-and-Forget Commands
Some commands use send() instead of query() for hotkey-style operations:
async send(command: string): Promise<void> {
if (!this.socket) await this.connect();
return new Promise((resolve, reject) => {
this.socket!.send(command, this.port, this.host, (err) =>
err ? reject(err) : resolve(),
);
});
}
These commands do not wait for or expect a response from RetroArch:
retroarch_pause_toggleretroarch_resetretroarch_screenshot
Sources: src/retroarch.ts:55-68
Memory Read/Write Data Paths
Two Memory APIs
mcp-retroarch exposes two distinct memory access paths:
graph TD
A[Memory Request] --> B{Which API?}
B -->|System Memory Map| C[READ_CORE_MEMORY<br/>WRITE_CORE_MEMORY]
B -->|CHEEVOS Space| D[READ_CORE_RAM<br/>WRITE_CORE_RAM]
C --> E[Full system bus access]
C --> F[Precise memory regions]
C --> G[Returns byte count on write]
D --> H[Achievement address space]
D --> I[Limited to 64KB on some cores]
D --> J[No acknowledgment on write<br/>"fire-and-forget"]Sources: src/tools.ts:100-200
Memory Read Flow
- Tool handler receives
addressandlengthparameters - RetroArch client calls
readMemory()orreadRam() - UDP query sent with address and byte count
- Response parsing extracts bytes from NCI response
- Hex encoding converts bytes to space-separated hex string
case "retroarch_read_memory": {
const bytes = await ra.readMemory(p.address as number, p.length as number);
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" ");
return ok(`${addrHex(p.address as number)} [${bytes.length} bytes]:\n${hex}`);
}
Memory Write Flow
| API | Acknowledgment | Use Case |
|---|---|---|
write_memory | Yes (returns byte count) | Precise writes, cheats |
write_ram | No (fire-and-forget) | Fallback when memory map unavailable |
Sources: src/tools.ts:30-50
Error Flow
graph TD
A[Tool Call] --> B{UDP Send Success?}
B -->|No| C[Reject with send error]
B -->|Yes| D{Ack received?}
D -->|No| E[Timeout after 5000ms]
E --> F["Error: RetroArch query timed out"]
D -->|Yes| G{NCI Response OK?}
G -->|No| H[Parse error or invalid response]
G -->|Yes| I[Return success data]
C --> J[Error logged to stderr]
F --> J
H --> J
I --> K[JSON-RPC response to MCP client]Sources: src/retroarch.ts:80-90
Error Scenarios
| Error | Cause | User Message |
|---|---|---|
RetroArch query timed out | Network Commands disabled or wrong port | Check network_cmd_enable in retroarch.cfg |
no memory map defined | Core doesn't expose system memory map | Use read_ram / write_ram instead |
no descriptor for address | Address outside core's memory regions | Use different address or core |
retroarch query already in flight | Concurrent query attempted | Tool calls are serialized |
Sources: README.md:troubleshooting
Response Data Format
Memory Read Response
ADDR_HEX [N bytes]:
AB CD EF 12 34 56 78 9A ...
Example:
0x02000000 [16 bytes]:
03 D0 00 EA 03 D1 00 EA 03 D2 00 EA 03 D3 00 EA
Status Response
State: playing
System: snes
Game: Super Mario World (USA).sfc
CRC32: 1A9FBD77
Config Response
KEY = VALUE
Configuration and Environment
graph LR
subgraph "Environment Variables"
RA_HOST["RETROARCH_HOST<br/>default: 127.0.0.1"]
RA_PORT["RETROARCH_PORT<br/>default: 55355"]
end
subgraph "Runtime Config"
SOCKET["UDP Socket"]
TIMEOUT["timeoutMs<br/>default: 5000"]
end
RA_HOST -->|target IP| SOCKET
RA_PORT -->|target port| SOCKET| Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Sources: README.md:configuration
Startup Connectivity Probe
The server performs a non-blocking connectivity check on startup:
ra.connect()
.then(() => ra.getVersion())
.then((v) => process.stderr.write(`[mcp-retroarch] connected to ${ra.describeTarget()} — RetroArch ${v}\n`))
.catch((err) => process.stderr.write(
`[mcp-retroarch] note: RetroArch not reachable yet (${ra.describeTarget()}): ${err}\n` +
` Enable Network Commands in retroarch.cfg...\n`,
));
This probe is fire-and-forget and never blocks MCP server readiness.
Sources: src/index.ts:1-20
Tool-to-Command Mapping
| MCP Tool | NCI Command | Transport | Expects Response |
|---|---|---|---|
retroarch_get_status | GET_STATUS | query | Yes |
retroarch_get_config | GET_CONFIG_PARAM | query | Yes |
retroarch_read_memory | READ_CORE_MEMORY | query | Yes |
retroarch_read_ram | READ_CORE_RAM | query | Yes |
retroarch_write_memory | WRITE_CORE_MEMORY | query | Yes |
retroarch_write_ram | WRITE_CORE_RAM | send | No |
retroarch_save_state_current | SAVE_STATE | send | No |
retroarch_load_state_current | LOAD_STATE | send | No |
retroarch_load_state_slot | LOAD_STATE | send | No |
retroarch_pause_toggle | PAUSE_TOGGLE | send | No |
retroarch_frame_advance | FRAMEADVANCE | send | No |
retroarch_reset | RESET | send | No |
retroarch_screenshot | SCREENSHOT | send | No |
retroarch_show_message | SHOW_MSG | send | No |
Sources: src/tools.ts:1-300 Sources: src/retroarch.ts:100-200
Non-Blocking Architecture
Unlike earlier versions, the MCP server does not wait for RetroArch connectivity on startup:
| Version | Behavior |
|---|---|
| ≤ 0.1.0 | Blocked ~5s on VERSION query timeout |
| ≥ 0.1.1 | Non-blocking, probe runs in background |
This change eliminated startup delay when RetroArch is not running.
Sources: CHANGELOG.md:0.1.1
Summary
The data flow through mcp-retroarch follows a clear layered architecture:
- MCP Layer: JSON-RPC over stdio for client-server communication
- Tool Layer: Parameter validation and response formatting in
src/tools.ts - Transport Layer: UDP socket management in
src/retroarch.ts - NCI Layer: RetroArch Network Control Interface commands
The design prioritizes:
- Serial query execution to prevent response ambiguity
- Lazy connection to avoid startup blocking
- Fire-and-forget for hotkey commands to reduce latency
- Explicit acknowledgment for writes that support it
Sources: README.md:1-50
Memory Read/Write Operations
Related topics: Core Compatibility, MCP Tools Reference
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: Core Compatibility, MCP Tools Reference
Memory Read/Write Operations
Overview
The mcp-retroarch project exposes memory read/write functionality through RetroArch's Network Control Interface (NCI), enabling MCP clients to inspect and mutate the emulated system's memory in real-time. This capability is fundamental for debugging, cheat implementation, game state inspection, and scripted automation.
Memory operations target the emulated system—not the host machine—and support two distinct address space APIs depending on the loaded libretro core's capabilities.
Architecture
Memory operations flow through a serial UDP transport layer to RetroArch's NCI endpoint:
graph TD
A["MCP Client<br/>retroarch_read_memory<br/>retroarch_write_ram"] --> B["mcp-retroarch<br/>src/tools.ts"]
B --> C["RetroArchClient<br/>src/retroarch.ts"]
C -->|UDP :55355| D["RetroArch NCI"]
D -->|Memory Map API<br/>or CHEEVOS API| E["libretro Core<br/>Emulated System Memory"]
F["READ_CORE_MEMORY<br/>WRITE_CORE_MEMORY"] -.->|system bus| E
G["READ_CORE_RAM<br/>WRITE_CORE_RAM"] -.->|CHEEVOS space| ETransport Layer
The UDP socket is initialized in src/retroarch.ts with the following behavior:
- Query mode (
query()): Sends a command and awaits exactly one UDP response, throwing on timeout (~5 seconds) - Fire-and-forget mode (
send()): Sends a command without waiting for acknowledgment
// Serial query with timeout: src/retroarch.ts:60-85
async query(command: string): Promise<Buffer> {
if (!this.socket) await this.connect();
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
return new Promise<Buffer>((resolve, reject) => {
let timer: NodeJS.Timeout | null = setTimeout(() => {
this.pending = null;
reject(new Error(
`RetroArch query "${command.split(" ")[0]}" timed out after ${this.timeoutMs}ms ` +
`— is RetroArch running with Network Commands enabled?`,
));
}, this.timeoutMs);
// ...
});
}
Sources: src/retroarch.ts:60-75
Two Memory APIs
mcp-retroarch exposes two independent memory interfaces, each mapping to a different RetroArch NCI command family:
| Aspect | _memory API | _ram API |
|---|---|---|
| NCI Command | READ_CORE_MEMORY / WRITE_CORE_MEMORY | READ_CORE_RAM / WRITE_CORE_RAM |
| Address Space | System memory bus | CHEEVOS (achievement) space |
| Core Requirement | Core must expose memory map | Works via CHEEVOS interface |
| Fallback | Falls back to _ram if "no memory map defined" | N/A (always available on supported cores) |
| Write Acknowledgment | ✅ Returns byte count | ❌ Fire-and-forget |
Sources: src/tools.ts:1-150
System Memory Map (`_memory`)
The _memory tools use the libretro core's exposed system memory descriptors. This is the preferred approach when available because:
- Addresses follow the native system bus layout (e.g., SNES WRAM at
0x7E0000-0x7FFFFF) WRITE_CORE_MEMORYreturns acknowledgment with actual byte count written- Read-only descriptors are honored (write stops at boundary)
CHEEVOS Address Space (`_ram`)
The _ram tools use RetroAchievements' address space conventions, which differ from the native system bus:
| System | CHEEVOS WRAM Start | System Bus WRAM Start |
|---|---|---|
| SNES | 0x000000 | 0x7E0000 |
| GBA | 0x03000000 | 0x02000000 (EWRAM) |
Sources: src/tools.ts:95-120
Tool Reference
`retroarch_read_memory`
Read bytes via the libretro core's system memory map.
| Parameter | Type | Required | Description |
|---|---|---|---|
address | integer | ✅ | Starting address in system memory map |
length | integer | ✅ | Bytes to read (1-4096) |
Returns: Header line ADDR_HEX [N bytes]: followed by space-separated uppercase hex bytes.
Error Conditions:
"no memory map defined"— Core doesn't expose system memory map"no descriptor for address"— Address outside core's memory regions- Timeout after ~5 seconds
Sources: src/tools.ts:70-100
`retroarch_write_memory`
Write bytes via the libretro core's system memory map. This is the only write tool that returns acknowledgment.
| Parameter | Type | Required | Description |
|---|---|---|---|
address | integer | ✅ | Starting address in system memory map |
bytes | integer[] | ✅ | Byte values 0-255 (max 4096) |
Returns: Wrote N bytes → ADDR_HEX where N is confirmed written count.
Side Effects:
- Disables RetroArch's hardcore mode for the session
- Destructive (no undo; use
retroarch_save_state_currentfirst)
Sources: src/tools.ts:100-140
`retroarch_read_ram`
Read bytes via the CHEEVOS (achievement) address space.
| Parameter | Type | Required | Description |
|---|---|---|---|
address | integer | ✅ | Starting address in CHEEVOS space |
length | integer | ✅ | Bytes to read (1-4096) |
Returns: Header line ADDR_HEX [N bytes, CHEEVOS]: followed by hex dump.
Note: RetroArch may return fewer bytes than requested at memory-region boundaries.
Sources: src/tools.ts:120-150
`retroarch_write_ram`
Write bytes via the CHEEVOS address space. Fire-and-forget—no acknowledgment.
| Parameter | Type | Required | Description |
|---|---|---|---|
address | integer | ✅ | Starting address in CHEEVOS space |
bytes | integer[] | ✅ | Byte values 0-255 (max 4096) |
Returns: Wrote N bytes → ADDR_HEX (CHEEVOS, no ack)
Verification: Follow up with retroarch_read_ram at the same address to confirm the write landed.
Sources: src/tools.ts:150-180
Usage Workflow
Read Memory (System Bus)
// Read 16 bytes from SNES WRAM at 0x7E0000
const result = await tools.retroarch_read_memory({
address: 0x7E0000,
length: 16
});
// Returns: "7E0000 [16 bytes]:
// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
Write Memory with Verification
graph LR
A["Save State<br/>retroarch_save_state_current"] --> B["Write Memory<br/>retroarch_write_memory"]
B --> C{"Response received?"}
C -->|Yes| D["Read Back<br/>retroarch_read_memory"]
D --> E["Verify bytes match"]
C -->|No/Timeout| F["Load State<br/>retroarch_load_state_current"]
E -->|Mismatch| FWrite RAM (CHEEVOS) with Verification
graph LR
A["Save State"] --> B["Write RAM<br/>retroarch_write_ram"]
B --> C["Read Back<br/>retroarch_read_ram"]
C --> D{"Bytes match?"}
D -->|Yes| E["Continue"]
D -->|No| F["Load State<br/>retroarch_load_state_current"]Common Address Ranges
| System | Region | System Bus | CHEEVOS Space |
|---|---|---|---|
| GBA | EWRAM | 0x02000000-0x0203FFFF | 0x03000000 offset |
| GBA | IWRAM | 0x03000000-0x03007FFF | Direct |
| SNES | WRAM | 0x7E0000-0x7FFFFF | 0x000000 |
| Genesis | 68K RAM | 0xFF0000-0xFFFFFF | N/A |
Sources: src/tools.ts:85-95
Limitations
| Limitation | Cause | Workaround |
|---|---|---|
| Max 4096 bytes/call | NCI single-datagram size | Batch larger reads in 4 KiB chunks |
| No "save to slot N" | NCI protocol limitation | Walk slot pointer with state_slot_plus/state_slot_minus |
write_ram has no ack | RetroArch doesn't respond to WRITE_CORE_RAM | Follow up with retroarch_read_ram |
| Address spaces differ | CHEEVOS vs system bus | Use _memory tools when possible |
| Memory regions bounded | Core exposes specific regions | Some addresses (VRAM, etc.) may be inaccessible |
Sources: src/tools.ts:145-155
Configuration
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Sources: README.md:configuration
Sources: src/retroarch.ts:60-75
Savestate Management
Related topics: MCP Tools Reference
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: MCP Tools Reference
Savestate Management
Overview
Savestate Management in mcp-retroarch enables programmatic save and load operations for game sessions running under RetroArch's Network Command Interface (NCI). The system provides five MCP tools that wrap RetroArch's savestate NCI commands, allowing MCP clients to snapshot emulator state, restore from previous snapshots, and navigate between save slots.
The savestate system operates over UDP to RetroArch's NCI on port 55355, with mcp-retroarch acting as a bridge between the MCP JSON-RPC protocol and RetroArch's line-based command protocol.
Architecture
┌─────────────────┐ stdio JSON-RPC ┌──────────────────────┐ UDP :55355 ┌─────────────────┐
│ MCP Client │◄───────────────────►│ mcp-retroarch │◄──────────────►│ RetroArch │
│ (Claude Code, │ │ (src/retroarch.ts) │ │ (NCI Server) │
│ Claude Desktop)│ └──────────────────────┘ └─────────────────┘
└─────────────────┘
The savestate subsystem relies on:
| Layer | Technology | Role |
|---|---|---|
| MCP Protocol | stdio + JSON-RPC | Client-facing tool interface |
| Bridge | TypeScript (src/retroarch.ts) | Protocol translation and UDP communication |
| Transport | UDP datagrams | Direct communication with RetroArch NCI |
| Target | RetroArch NCI | Savestate command execution |
Savestate Tools
| Tool | NCI Command | Purpose |
|---|---|---|
retroarch_save_state_current | SAVE_STATE | Save to currently-selected slot |
retroarch_load_state_current | LOAD_STATE | Load from currently-selected slot |
retroarch_load_state_slot | LOAD_STATE_SLOT N | Load from explicit slot N (1-9) |
retroarch_state_slot_plus | STATE_SLOT_PLUS | Increment slot pointer |
retroarch_state_slot_minus | STATE_SLOT_MINUS | Decrement slot pointer |
Tool Parameters
#### retroarch_load_state_slot
{
"name": "retroarch_load_state_slot",
"arguments": {
"slot": {
"type": "integer",
"minimum": 1,
"maximum": 9,
"description": "Savestate slot number (1-9). RetroArch uses 1-based slot indexing."
}
}
}
Return Values
All savestate tools return single-line confirmation messages:
| Tool | Return Message |
|---|---|
retroarch_save_state_current | "Saved to current slot" |
retroarch_load_state_current | "Loaded from current slot" |
retroarch_load_state_slot | "Loaded from slot N" |
retroarch_state_slot_plus | "Slot: N" |
retroarch_state_slot_minus | "Slot: N" |
Note: These return messages are UDP-send confirmations only. The NCI does not acknowledge command receipt. If a savestate operation fails silently, the tool will still return a success-like message.
Sources: src/tools.ts:77-79
Slot-Based Savestate Model
How Slots Work
RetroArch's savestate system uses a slot-based model with 10 slots (0-9 by default, though some builds use 1-9). Each slot holds one savestate file. When you save to an occupied slot, the existing savestate is overwritten.
The Slot Pointer
RetroArch maintains an internal current slot pointer that determines which slot SAVE_STATE and LOAD_STATE (without a slot argument) target. The slot pointer can be changed using:
graph LR
A["Current Slot = N"] --> B["state_slot_plus"]
B --> C["Current Slot = N+1"]
C --> D["state_slot_plus"]
D --> E["Current Slot = N+2"]
A --> F["state_slot_minus"]
F --> G["Current Slot = N-1"]
G --> H["state_slot_minus"]
H --> I["Current Slot = N-2"]Slot Navigation Workflow
To save to slot 5 when currently at slot 1:
graph TD
A["Start: Slot = 1"] --> B["retroarch_state_slot_plus"]
B --> C["Slot = 2"]
C --> D["retroarch_state_slot_plus"]
D --> E["Slot = 3"]
E --> F["retroarch_state_slot_plus"]
F --> G["Slot = 4"]
G --> H["retroarch_state_slot_plus"]
H --> I["Slot = 5"]
I --> J["retroarch_save_state_current"]
J --> K["Saved to slot 5"]Critical Limitations
No Direct Save-to-Slot
The NCI protocol does not expose a SAVE_STATE_SLOT N command. This is a protocol limitation, not a bug in mcp-retroarch.
| Operation | Available? | Workaround |
|---|---|---|
| Save to current slot | ✅ Yes | retroarch_save_state_current |
| Save to specific slot | ❌ No | Walk slot pointer with plus/minus, then save |
| Load from current slot | ✅ Yes | retroarch_load_state_current |
| Load from specific slot | ✅ Yes | retroarch_load_state_slot |
Sources: README.md
Current Slot Not Queryable
The NCI does not expose a command to query the current slot pointer. Clients must track slot position client-side or use retroarch_show_message to echo the slot number.
graph TD
A["Track slot client-side"] --> B["State variable: currentSlot"]
A --> C["Use show_message for confirmation"]
C --> D["retroarch_show_message with 'Slot: N'"]UDP Communication Details
Query/Response Pattern
The savestate bridge uses serial UDP communication with timeout handling:
async query(command: string): Promise<Buffer> {
if (!this.socket) await this.connect();
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
return new Promise<Buffer>((resolve, reject) => {
let timer: NodeJS.Timeout | null = setTimeout(() => {
this.pending = null;
reject(new Error(
`RetroArch query "${command.split(" ")[0]}" timed out after ${this.timeoutMs}ms`,
));
}, this.timeoutMs);
// ...
});
}
Sources: src/retroarch.ts:72-89
Fire-and-Forget Commands
retroarch_save_state_current, retroarch_state_slot_plus, and retroarch_state_slot_minus use fire-and-forget semantics:
async send(command: string): Promise<void> {
if (!this.socket) await this.connect();
return new Promise((resolve, reject) => {
this.socket!.send(command, this.port, this.host, (err) =>
err ? reject(err) : resolve(),
);
});
}
Sources: src/retroarch.ts:63-69
Best Practices
Establish Rollback Points
Before memory writes or destructive operations, always save the current state:
// 1. Save rollback point
retroarch_save_state_current();
// 2. Perform memory operations
retroarch_write_memory(address, bytes);
// 3. If something goes wrong, restore
retroarch_load_state_current();
Pre-Check Before Toggle Operations
Since the NCI only exposes toggle commands, verify state before operations that depend on pause state:
graph TD
A["retroarch_get_status"] --> B{"state: playing?"}
B -->|Yes, want to pause| C["retroarch_pause_toggle"]
B -->|No, want to unpause| C
C --> D["retroarch_save_state_current"]
D --> E["Continue workflow"]Slot Tracking Pattern
let currentSlot = 1; // Track client-side
async function saveToSlot(targetSlot) {
while (currentSlot < targetSlot) {
await retroarch_state_slot_plus();
currentSlot++;
}
while (currentSlot > targetSlot) {
await retroarch_state_slot_minus();
currentSlot--;
}
await retroarch_save_state_current();
}
Error Handling
Timeout Errors
If RetroArch doesn't respond within the timeout period (default: 5000ms), the tool returns an error:
RetroArch query "SAVE_STATE" timed out after 5000ms — is RetroArch running with Network Commands enabled?
Troubleshooting:
- Confirm
network_cmd_enable = "true"in retroarch.cfg - Verify
network_cmd_port = "55355"matchesRETROARCH_PORTenvironment variable - Check that a game/ROM is currently loaded (NCI requires active content)
UDP Drop Handling
UDP datagrams can be dropped under load even on loopback. If a call times out but a retry succeeds, that's normal UDP behavior—the bridge does not auto-retry.
Configuration
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Related Tools
| Tool | Relationship |
|---|---|
retroarch_get_status | Use to verify game state before loading states |
retroarch_frame_advance | Step frames after loading state to verify restore |
retroarch_show_message | Echo current slot number for tracking |
Changelog
v0.1.2 (2026-05-15)
Documentation improvements:
- Slot-based savestate model now explicitly documented
- Fire-and-forget UDP semantics surfaced in tool descriptions
- NCI limitation ("no save to slot N") clearly documented with workaround
Sources: CHANGELOG.md
Sources: src/tools.ts:77-79
Emulation Control
Related topics: MCP Tools Reference
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: MCP Tools Reference
Emulation Control
Overview
Emulation Control in mcp-retroarch encompasses the set of tools and mechanisms that allow MCP clients to interact with a running RetroArch emulator. This includes pausing/resuming execution, advancing frames one at a time, resetting games, capturing screenshots, displaying on-screen notifications, and managing save states.
The control layer operates exclusively through RetroArch's Network Control Interface (NCI), a UDP-based command protocol that listens on port 55355 by default. All control commands are sent as plain-text line-terminated strings over UDP, and the majority are fire-and-forget — RetroArch does not acknowledge receipt or execution status.
Architecture
Transport Layer
The UDP transport is implemented in src/retroarch.ts. The communication model uses a single persistent UDP socket with promise-based request/response handling.
graph TD
A["MCP Client<br/>stdio JSON-RPC"] --> B["mcp-retroarch<br/>src/index.ts"]
B --> C["RetroArchClient<br/>src/retroarch.ts"]
C --> D["UDP Socket<br/>127.0.0.1:55355"]
D --> E["RetroArch NCI"]
F["NCI Response"] --> D
D --> G["Pending Promise<br/>resolve/reject"]
G --> BKey transport behaviors:
| Method | Behavior | Use Case |
|---|---|---|
send(command) | Fire-and-forget, no response expected | Pause, reset, frame advance |
query(command) | Sends command, awaits one UDP response | Memory reads, status queries |
connect() | Binds UDP socket to ephemeral port | Transport initialization |
disconnect() | Closes socket | Cleanup |
Sources: src/retroarch.ts:1-50
Serialization Constraint
UDP datagrams are line-terminated. This has a critical implication for the retroarch_show_message tool: any message containing \n or \r will be truncated at the first newline, with the remainder silently dropped on the wire.
Core Control Tools
Pause Toggle
Tool: retroarch_pause_toggle
Toggles RetroArch's pause state between paused and running.
graph LR
A["Call retroarch_get_status"] --> B{"state?"}
B -->|"playing"| C["retroarch_pause_toggle<br/>→ paused"]
B -->|"paused"| D["retroarch_pause_toggle<br/>→ playing"]
C --> E["Verify with retroarch_get_status"]
D --> EImportant: The NCI exposes only a toggle, not separate pause and unpause commands. Without first checking the current state via retroarch_get_status, the result is indeterminate.
| Property | Value |
|---|---|
| Input Schema | {} (no parameters) |
| Return | "Pause toggled" (UDP-send confirmation only) |
| Fire-and-forget | Yes — no verification of actual pause state |
Sources: src/tools.ts
Frame Advance
Tool: retroarch_frame_advance
Advances the emulation by exactly one frame. This is only effective while emulation is paused — calling FRAMEADVANCE while running is a no-op.
| Property | Value |
|---|---|
| Input Schema | {} (no parameters) |
| Prerequisite | Emulation must be paused |
| Use Case | Frame-precise input automation, animation inspection |
| Alternative | For large frame jumps, use retroarch_save_state_current / retroarch_load_state_current |
Sources: src/tools.ts
Reset
Tool: retroarch_reset
Performs a hard reset of the currently loaded game. The core is reloaded and execution begins from the game's reset vector.
| Property | Value |
|---|---|
| Input Schema | {} (no parameters) |
| Return | "Game reset" (UDP-send confirmation only) |
| Behavior | Immediately interrupts current execution |
Sources: src/tools.ts
Save State Management
State Slots
RetroArch maintains a current state slot pointer (0-9 typically). The save state tools operate on this pointer.
graph TD
A["Current Slot = N"] --> B["retroarch_save_state_current"]
B --> C["Save → Slot N"]
A --> D["retroarch_load_state_slot<br/>slot: M"]
D --> E{"M == N?"}
E -->|Yes| F["Load from Slot N"]
E -->|No| G["retroarch_state_slot_plus/minus<br/>to walk to M"]
G --> H["Load from Slot M"]Available Tools
| Tool | Purpose | Input | Return |
|---|---|---|---|
retroarch_save_state_current | Save to current slot | None | "Saved to current slot" |
retroarch_load_state_current | Load from current slot | None | "Loaded from current slot" |
retroarch_load_state_slot | Load from explicit slot | { slot: number } | "Loaded from slot N" |
retroarch_state_slot_plus | Increment slot pointer | None | Slot number confirmation |
retroarch_state_slot_minus | Decrement slot pointer | None | Slot number confirmation |
Known Limitation
The NCI protocol does not expose a command to save directly to a specific slot. To save to slot 5 when the pointer is on slot 0:
- Call
retroarch_state_slot_plusfive times - Call
retroarch_save_state_current
This is an NCI limitation, not a bug in mcp-retroarch.
Sources: README.md
Visual Feedback Tools
Screenshot
Tool: retroarch_screenshot
Captures the current frame and saves it to RetroArch's configured screenshot directory.
| Property | Value |
|---|---|
| Input Schema | {} (no parameters) |
| Return | "Screenshot saved to RetroArch's configured screenshot directory" |
| Directory | Must be verified in RetroArch GUI: Settings → Directory → Screenshot |
| NCI Limitation | The screenshot_directory param is NOT exposed via GET_CONFIG_PARAM |
Sources: src/tools.ts, README.md
On-Screen Message
Tool: retroarch_show_message
Displays a single-line notification overlay on the RetroArch window for approximately 3 seconds (RetroArch's default notification timeout).
| Property | Value | |
|---|---|---|
| Input Schema | { message: string } | Required, minLength: 1 |
| Message Limits | ≤80 characters recommended; newlines (\n, \r) truncate | |
| Duration | ~3 seconds (configurable via input_overlay_show_inputs_port settings) | |
| Queueing | Messages are NOT queued — rapid calls replace the previous message | |
| Use Case | Debug output, progress markers, "look here" cues |
graph LR
A["Message: 'Frame 1234'"] --> B["NCI OSD Overlay"]
C["Rapid Second Call"] --> D["Message: 'Frame 1235'"]
D -->|Before A renders| BImportant: Because messages replace each other, do not issue multiple show_message calls in rapid succession without verification that the previous message was read.
Sources: src/tools.ts
UDP Transport Deep Dive
Socket Lifecycle
From src/retroarch.ts:
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
const sock = dgram.createSocket("udp4");
sock.once("error", (err) => reject(err));
sock.bind(0, () => {
sock.on("message", (msg) => {
const cb = this.pending;
if (!cb) return; // unsolicited or late reply — drop
this.pending = null;
cb(msg);
});
// ...
});
});
}
Query Timeout
The query() method enforces a configurable timeout (default ~5 seconds):
async query(command: string): Promise<Buffer> {
if (!this.socket) await this.connect();
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
return new Promise<Buffer>((resolve, reject) => {
let timer: NodeJS.Timeout | null = setTimeout(() => {
this.pending = null;
reject(new Error("RetroArch query timed out"));
}, this.timeoutMs);
// ...
});
}
Serial constraint: Only one query can be in-flight at a time. The client enforces this with a pending callback reference.
Sources: src/retroarch.ts
Error Handling
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
RetroArch query timed out | Network Commands disabled or port mismatch | Verify network_cmd_enable = "true" in retroarch.cfg |
retroarch query already in flight | Code bug or concurrent tool calls | Ensure serial tool execution |
| Screenshot not where expected | Screenshot saved to RetroArch's configured directory | Check Settings → Directory → Screenshot |
Background Connectivity
The MCP server performs a fire-and-find connectivity probe on startup in src/index.ts:
ra.connect()
.then(() => ra.getVersion())
.then((v) => process.stderr.write(`[mcp-retroarch] connected to ${ra.describeTarget()} — RetroArch ${v}\n`))
.catch((err) => process.stderr.write(
`[mcp-retroarch] note: RetroArch not reachable yet (${ra.describeTarget()}): ${err}\n` +
` Enable Network Commands in retroarch.cfg (network_cmd_enable / network_cmd_port)\n`
));
This does not block server startup — tool calls will connect on demand.
Sources: src/index.ts
Configuration
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
RetroArch-side configuration (via GUI or retroarch.cfg):
network_cmd_enable = "true"
network_cmd_port = "55355"
Or via RetroArch GUI: Settings → Network → Network Commands → ON
Sources: README.md
Tool Summary Table
| Tool | Type | Input | Fire-and-Forget | Verified Return |
|---|---|---|---|---|
retroarch_pause_toggle | Control | None | Yes | Confirmation only |
retroarch_frame_advance | Control | None | Yes | Confirmation only |
retroarch_reset | Control | None | Yes | Confirmation only |
retroarch_save_state_current | State | None | Yes | Confirmation only |
retroarch_load_state_current | State | None | Yes | Confirmation only |
retroarch_load_state_slot | State | { slot } | Yes | Confirmation only |
retroarch_state_slot_plus | State | None | Yes | Confirmation only |
retroarch_state_slot_minus | State | None | Yes | Confirmation only |
retroarch_screenshot | Visual | None | Yes | Confirmation only |
retroarch_show_message | Visual | { message } | Yes | Confirmation only |
Dependencies
The emulation control functionality depends on:
{
"@modelcontextprotocol/sdk": "^1.12.0"
}
The @modelcontextprotocol/sdk provides the MCP server framework that exposes these tools via stdio JSON-RPC to compatible MCP clients (Claude Code, Claude Desktop, etc.).
Sources: package.json
Sources: src/retroarch.ts:1-50
MCP Tools Reference
Related topics: Memory Read/Write Operations, Savestate Management, Emulation Control
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: Memory Read/Write Operations, Savestate Management, Emulation Control
MCP Tools Reference
The mcp-retroarch project exposes a collection of Model Context Protocol (MCP) tools that enable programmatic control of RetroArch through its Network Control Interface (NCI). These tools provide capabilities ranging from memory inspection and modification to savestate management and emulator control.
Overview
mcp-retroarch acts as a bridge between MCP clients and RetroArch's UDP-based NCI protocol. The MCP server communicates with RetroArch over UDP port 55355 (default), translating JSON-RPC requests from MCP clients into NCI commands. Sources: README.md
+----------------+ stdio +-----------------+ UDP :55355 +-----------------+
| MCP client | JSON-RPC | mcp-retroarch | NCI commands | RetroArch |
+----------------+ +-----------------+ +-----------------+
Tool Categories
The tools are organized into five functional categories:
| Category | Tools | Purpose |
|---|---|---|
| Status & Config | retroarch_get_status, retroarch_get_config | Query emulator state and configuration |
| Memory Access | retroarch_read_memory, retroarch_write_memory, retroarch_read_ram, retroarch_write_ram | Read/write emulated system memory |
| Savestate | retroarch_save_state_current, retroarch_load_state_current, retroarch_load_state_slot, retroarch_state_slot_plus, retroarch_state_slot_minus | Manage game save states |
| Emulator Control | retroarch_pause_toggle, retroarch_frame_advance, retroarch_reset | Control emulation execution |
| Media & UI | retroarch_screenshot, retroarch_show_message | Capture screenshots and display notifications |
Memory Architecture
mcp-retroarch provides two distinct memory access APIs, each using different address spaces and underlying NCI commands. Sources: src/tools.ts
System Memory Map (`_memory` tools)
The _memory tools use READ_CORE_MEMORY and WRITE_CORE_MEMORY commands, accessing the libretro core's system memory map. This is the preferred method when the loaded core advertises a memory map. Sources: src/tools.ts
Address conventions vary by system:
| System | Memory Region | Address Range |
|---|---|---|
| GBA EWRAM | External Work RAM | 0x02000000-0x0203FFFF |
| SNES WRAM | Work RAM | 0x7E0000-0x7FFFFF |
| Genesis 68K RAM | Main RAM | 0xFF0000-0xFFFFFF |
CHEEVOS Address Space (`_ram` tools)
The _ram tools use READ_CORE_RAM and WRITE_CORE_RAM commands (the older CHEEVOS/achievements API). These tools access the RetroAchievements address space, which follows per-system conventions distinct from the system memory map. Sources: src/tools.ts
When to use which:
- Use
_memorytools as the starting point for memory access - Fall back to
_ramtools when_memoryreturns'no memory map defined'(older cores) read_ramconfirmed working for SwanStation (PSX), Mesen (NES) Sources: README.md
Memory Tool Comparison
| Feature | _memory tools | _ram tools |
|---|---|---|
| NCI Command | READ_CORE_MEMORY / WRITE_CORE_MEMORY | READ_CORE_RAM / WRITE_CORE_RAM |
| Address Space | System memory map | CHEEVOS (achievements) |
| Write Acknowledgment | Returns byte count | No acknowledgment (fire-and-forget) |
| Core Support | Requires memory map | Broader (achievements-compatible cores) |
| Max Bytes/Call | 4096 | 4096 |
Status & Configuration Tools
retroarch_get_status
Queries the current emulator state including run-state, loaded ROM, and CRC32.
Input: No parameters
Returns:
State: playing|pausedSystem: SYSTEM_IDGame: BASENAMECRC32: HEX or (none reported)
Returns 'No content loaded' when RetroArch is at the menu with no ROM. Sources: src/tools.ts
retroarch_get_config
Reads a single RetroArch configuration parameter by name.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Configuration parameter name |
Notes:
- Uses
GET_CONFIG_PARAMcommand - RetroArch whitelists exposed parameters; non-whitelisted names error
screenshot_directoryis NOT exposed via this API Sources: src/tools.ts
Memory Access Tools
retroarch_read_memory
Reads bytes from the system memory map via READ_CORE_MEMORY.
Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
address | integer | Yes | ≥ 0 | Starting address in system memory map |
length | integer | Yes | 1-4096 | Number of bytes to read |
Returns: ADDR_HEX [N bytes]: followed by space-separated uppercase hex bytes
Notes:
- Returns fewer bytes if read crosses a memory-region boundary
- Works whether emulation is paused or running
- Throws error if core doesn't expose a memory map Sources: src/tools.ts
retroarch_write_memory
Writes bytes to the system memory map via WRITE_CORE_MEMORY.
Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
address | integer | Yes | ≥ 0 | Starting address in system memory map |
bytes | array | Yes | 1-4096 elements, each 0-255 | Byte values to write |
Returns: Wrote N bytes → ADDR_HEX
Notes:
- DESTRUCTIVE - overwrites existing data with no undo
- Disables RetroArch's hardcore mode for the session
- Returns byte count (the only NCI write command that acknowledges) Sources: src/tools.ts
retroarch_read_ram
Reads bytes from the CHEEVOS address space via READ_CORE_RAM.
Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
address | integer | Yes | ≥ 0 | Starting address in CHEEVOS space |
length | integer | Yes | 1-4096 | Number of bytes to read |
Returns: ADDR_HEX [N bytes, CHEEVOS]: followed by space-separated uppercase hex bytes
Notes:
- Fallback when
_memoryreturns'no memory map defined' - CHEEVOS addresses follow RetroAchievements conventions (e.g., SNES WRAM starts at
0x000000, not0x7E0000) Sources: src/tools.ts
retroarch_write_ram
Writes bytes to the CHEEVOS address space via WRITE_CORE_RAM.
Parameters:
| Parameter | Type | Required | Constraints | Description |
|---|---|---|---|---|
address | integer | Yes | ≥ 0 | Starting address in CHEEVOS space |
bytes | array | Yes | 1-4096 elements, each 0-255 | Byte values to write |
Returns: Wrote N bytes → ADDR_HEX (CHEEVOS, no ack)
Notes:
- DESTRUCTIVE and fire-and-forget - no verification
- Disables RetroArch's hardcore mode
- RetroArch does not acknowledge this command
- Verify writes with
retroarch_read_ramafter writing Sources: src/tools.ts
Savestate Management
The NCI protocol has limitations for savestate operations:
- No direct "save to slot N" - only save to the currently-selected slot
- No query for current slot - client must track the slot pointer
- Load supports both current slot and explicit slot numbers Sources: README.md
retroarch_save_state_current
Saves the current game state to the currently-selected slot.
Input: No parameters
Returns: Saved to current slot
retroarch_load_state_current
Loads game state from the currently-selected slot.
Input: No parameters
Returns: Loaded from current slot
retroarch_load_state_slot
Loads game state from an explicit slot number.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
slot | integer | Yes | Savestate slot number (0-9 typically) |
Returns: Loaded from slot N
retroarch_state_slot_plus / retroarch_state_slot_minus
Moves the current slot pointer up or down. Used to navigate to a target slot before saving.
Input: No parameters
Returns: Slot navigation confirmation
Workflow for saving to a specific slot:
graph TD
A[Start] --> B{Current slot = target?}
B -->|Yes| E[retroarch_save_state_current]
B -->|No| C{Increment or decrement?}
C -->|Plus| D[retroarch_state_slot_plus]
C -->|Minus| F[retroarch_state_slot_minus]
D --> B
F --> B
E --> G[Done]Emulator Control Tools
retroarch_pause_toggle
Toggles RetroArch's pause state.
Input: No parameters
Returns: Pause toggled
Notes:
- NCI exposes only a toggle, not separate pause/unpause
- Call
retroarch_get_statusfirst to check current state if needed Sources: src/tools.ts
retroarch_frame_advance
Advances emulation by exactly one frame.
Input: No parameters
Returns: Advanced one frame
retroarch_reset
Hard-resets the running game.
Input: No parameters
Returns: Game reset
Media & UI Tools
retroarch_screenshot
Captures a screenshot and saves it to RetroArch's configured screenshot directory.
Input: No parameters
Returns: Screenshot saved to RetroArch's configured screenshot directory
Notes:
- The NCI doesn't expose
screenshot_directoryviaGET_CONFIG_PARAM - Check the configured path via RetroArch GUI: Settings → Directory → Screenshot Sources: README.md
retroarch_show_message
Displays a notification on the RetroArch window.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Message text to display |
Returns: Showed: {message}
Transport Semantics
All tools communicate with RetroArch via UDP datagrams. This has important implications:
Fire-and-Forget Commands
Most NCI commands (write_ram, pause_toggle, frame_advance, reset, save_state, etc.) do not receive acknowledgment from RetroArch. The success message returned by the tool confirms only that the UDP packet was sent, not that RetroArch received or acted on it. Sources: src/tools.ts
Query Commands with Acknowledgment
The retroarch_write_memory command is the only write command that returns a byte count from RetroArch, providing limited verification of the write operation.
Timeout Handling
UDP queries timeout after a configurable interval (default: 5000ms). Common timeout causes:
- Network Commands not enabled in RetroArch
- Port mismatch between
RETROARCH_PORTand RetroArch'snetwork_cmd_port - UDP datagrams dropped under load (even on loopback) Sources: README.md
The bridge does not auto-retry on timeout; retry the call if needed. Sources: README.md
Configuration
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in retroarch.cfg) |
Error Handling
| Error Message | Cause / Fix |
|---|---|
RetroArch query timed out | Network Commands not enabled; port mismatch; UDP dropped |
READ_CORE_MEMORY failed: no memory map defined | Core doesn't advertise memory map; use read_ram instead |
READ_CORE_MEMORY failed: no descriptor for address | Address outside core's memory regions |
| Screenshots don't appear | Check RetroArch's screenshot directory setting via GUI |
| Can't save to specific slot directly | NCI limitation; walk slot pointer with state_slot_plus/minus first |
Tool Summary Table
| Tool | Input Parameters | Returns | Side Effects |
|---|---|---|---|
retroarch_get_status | — | Emulator state info | None |
retroarch_get_config | name | Config value | None |
retroarch_read_memory | address, length | Hex dump | None |
retroarch_write_memory | address, bytes | Byte count | Disables hardcore mode |
retroarch_read_ram | address, length | Hex dump (CHEEVOS) | None |
retroarch_write_ram | address, bytes | Confirmation | Disables hardcore mode |
retroarch_pause_toggle | — | Pause toggled | Changes run-state |
retroarch_frame_advance | — | Frame advanced | Advances emulation |
retroarch_reset | — | Game reset | Hard-resets game |
retroarch_screenshot | — | Screenshot path | Creates file |
retroarch_show_message | message | Message displayed | Shows notification |
retroarch_save_state_current | — | Save confirmed | Overwrites current slot |
retroarch_load_state_current | — | Load confirmed | Loads current slot |
retroarch_load_state_slot | slot | Load confirmed | Loads specified slot |
retroarch_state_slot_plus | — | Slot moved | Increments slot pointer |
retroarch_state_slot_minus | — | Slot moved | Decrements slot pointer |
Source: https://github.com/dmang-dev/mcp-retroarch / Human Manual
Core Compatibility
Related topics: Memory Read/Write Operations
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: Memory Read/Write Operations
Core Compatibility
Overview
Core Compatibility refers to the ability of mcp-retroarch to interact with different libretro cores running inside RetroArch. Since each emulator core implements the libretro API differently—and exposes different memory maps and features—mcp-retroarch provides multiple memory access pathways and behavioral workarounds to maximize cross-core support.
The MCP server communicates with RetroArch via the Network Control Interface (NCI), a UDP-based command protocol. The level of functionality available depends on:
- Whether the loaded core exposes a system memory map via libretro
- Whether the core responds to the CHEEVOS (RetroAchievements) read API
- Which specific NCI commands the core supports
Sources: README.md
Memory Access Architecture
mcp-retroarch provides two distinct memory reading pathways, each targeting a different address space:
Memory Access Methods Comparison
| Aspect | retroarch_read_memory | retroarch_read_ram |
|---|---|---|
| NCI Command | READ_CORE_MEMORY | READ_CORE_RAM |
| Address Space | Libretro system memory map | CHEEVOS achievement address space |
| Core Requirement | Core must expose memory map descriptors | Works if core exposes CHEEVOS |
| Fallback | Auto-fall back to read_ram on "no memory map" | No further fallback |
| Use Case | Primary tool for memory inspection | Fallback / older cores |
Sources: src/tools.ts:1-200
Address Space Differences
The two memory paths use fundamentally different addressing schemes:
graph TB
subgraph "Libretro Memory Map read_memory"
A["SNES WRAM: 0x7E0000-0x7FFFFF"]
B["GBA EWRAM: 0x02000000-0x0203FFFF"]
C["Genesis 68K RAM: 0xFF0000-0xFFFFFF"]
end
subgraph "CHEEVOS Address Space read_ram"
D["SNES WRAM: 0x000000"]
E["GBA WRAM: 0x02000000"]
F["NES WRAM: 0x0000"]
endFor example, SNES WRAM appears at:
- System memory map:
0x7E0000 - CHEEVOS space:
0x000000
Sources: src/tools.ts
Tested Cores Matrix
The following cores have been verified end-to-end with mcp-retroarch:
| System | Core | read_memory | read_ram | Notes |
|---|---|---|---|---|
| Game Boy Advance | mgba_libretro | ✅ | ✅ | GBA interrupt vector table visible at 0x0000 (d3 00 00 ea ...) |
| NES | mesen_libretro | ✅ (only NES core tested that does) | ✅ | Full 16-bit NES address space exposed. WRAM at 0x0000-0x07FF, mirrored to 0x1FFF. CHEEVOS bounded to first 64 KB. |
| NES | nestopia_libretro | ❌ (no memory map) | ✅ | CHEEVOS only. 64 KB bound. For NES + memory map, prefer Mesen. |
| SNES | snes9x_libretro | ❌ | — | Status not fully documented in current release |
Sources: README.md
Feature Support by Core
Memory Read/Write
| Capability | Supported | Details |
|---|---|---|
| System memory map | Core-dependent | Only cores that expose descriptors via READ_CORE_MEMORY |
| CHEEVOS RAM read | Most cores | Achievement API is widely supported |
| Memory write | Same pathways as reads | WRITE_CORE_MEMORY or WRITE_CORE_RAM |
Important limitation: Memory writes via NCI automatically disable RetroArch's hardcore mode for the rest of the session.
Sources: src/tools.ts
Savestate Operations
Savestate functionality is supported universally across cores since it uses RetroArch's internal state management:
| Operation | Support | Notes |
|---|---|---|
| Save to current slot | ✅ | Uses SAVE_STATE_CURRENT |
| Load from current slot | ✅ | Uses LOAD_STATE_CURRENT |
| Load from explicit slot | ✅ | Uses LOAD_STATE with slot number |
| Save to explicit slot | ❌ | NCI limitation—must walk slot pointer |
Sources: README.md
Game-Pad Input
Not supported via mcp-retroarch. The NCI protocol does not expose game-pad input. RetroArch has a separate "Remote RetroPad" mechanism on UDP port 55400, but it requires loading a specific core and cannot drive an existing emulation session.
Sources: README.md
Choosing the Right Memory Tool
graph TD
A["Need to read/write memory?"] --> B{"Does read_memory return<br/>'no memory map defined'?"}
B -->|Yes| C["Use retroarch_read_ram"]
B -->|No| D["Use retroarch_read_memory"]
C --> E["Address in CHEEVOS space"]
D --> F["Address in libretro memory map"]
style B fill:#ffcccc
style C fill:#ccffcc
style D fill:#ccffccDecision Criteria
- Start with
retroarch_read_memory— It provides access to the native system memory map and is the preferred pathway.
- Fall back to
retroarch_read_ramwhen:
- The core returns "no memory map defined"
- You need CHEEVOS achievement-style addresses
- Working with cores like
nestopia_libretrothat don't expose system memory maps
- Write operations follow the same path as your read operations.
Sources: src/tools.ts
Core-Specific Behaviors
NES Cores Comparison
| Feature | Mesen | Nestopia |
|---|---|---|
| System memory map | ✅ Exposed | ❌ Not exposed |
| CHEEVOS RAM | ✅ 64 KB bound | ✅ 64 KB bound |
| Full address space | ✅ 0x0000-0xFFFF | Limited to first 64 KB |
| Recommendation | Preferred for memory work | Use for achievements only |
GBA with mgba_libretro
The mgba_libretro core provides full memory map access:
- Interrupt vector table at
0x0000is readable viaread_memory - Example data visible:
d3 00 00 ea ...
Sources: README.md
UDP Communication Model
mcp-retroarch uses a serial query model with timeout handling:
sequenceDiagram
participant MCP as MCP Client
participant Bridge as mcp-retroarch
participant RA as RetroArch NCI
MCP->>Bridge: Tool call (e.g., retroarch_read_memory)
Bridge->>Bridge: Check if socket connected
Bridge->>RA: UDP query (e.g., READ_CORE_MEMORY ...)
Note over Bridge: Set timeout timer
RA-->>Bridge: UDP response
Bridge->>Bridge: Clear timeout, resolve promise
Bridge-->>MCP: JSON-RPC response
Note over Bridge,RA: Timeout → Error "RetroArch query timed out"Key behaviors:
- Only one query can be in flight at a time
- Default timeout: configurable, typically 5000ms
- UDP is unreliable—retries may be needed under load
Sources: src/retroarch.ts:1-100
Troubleshooting Core Compatibility
| Symptom | Cause | Solution |
|---|---|---|
READ_CORE_MEMORY failed: no memory map defined | Core doesn't expose system memory map | Use retroarch_read_ram instead |
READ_CORE_MEMORY failed: no descriptor for address | Address outside core's memory regions | Use a different core or check valid address range |
RetroArch query timed out | Network Commands not enabled or port mismatch | Enable in RetroArch GUI or set network_cmd_enable = "true" |
| Inconsistent responses | UDP datagram drops under load | Retry the query—mcp-retroarch doesn't auto-retry |
Sources: README.md
Configuration for Core Support
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host |
RETROARCH_PORT | 55355 | UDP port (must match network_cmd_port in RetroArch config) |
RetroArch must have Network Commands enabled:
- GUI: Settings → Network → Network Commands → ON
- Config file:
network_cmd_enable = "true"
Sources: README.md
Architecture Summary
+----------------+ stdio +-----------------+ UDP :55355 +-----------------+
| MCP client | JSON-RPC | mcp-retroarch | commands | RetroArch |
| |◄────────────►| |◄────────────►| (any core) |
+----------------+ +-----------------+ +-----------------+
The bridge translates MCP tool calls into NCI UDP commands. Core compatibility depends entirely on what the underlying libretro core exposes via the NCI protocol.
Sources: README.md
Related Tools for Other Platforms
| Platform | Tool | Features |
|---|---|---|
| Game Boy Advance | mcp-mgba | Memory + button input + screenshot via mGBA Lua bridge |
| PCSX2, etc. | mcp-pine | Memory + savestate only via PINE protocol |
These alternatives may provide better compatibility for their respective platforms when NCI-based cores lack features.
Sources: README.md
Configuration
Related topics: Quick Start Guide
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Continue reading this section for the full explanation and source context.
Related Pages
Related topics: Quick Start Guide
Configuration
The Configuration system in mcp-retroarch encompasses two distinct layers: the MCP server's runtime settings (controlled via environment variables) and RetroArch's Network Control Interface (NCI) parameters (queried at runtime). Together, they establish the communication bridge between an MCP client and a running RetroArch instance.
Overview
mcp-retroarch acts as a bridge server that translates MCP tool calls into UDP datagrams sent to RetroArch's Network Command Interface. Configuration determines:
- Network transport — where to send UDP commands (host and port)
- RetroArch NCI readiness — ensuring RetroArch has Network Commands enabled
- Runtime parameter discovery — querying RetroArch's internal configuration state
Sources: README.md:30-35
graph TD
A[MCP Client] -->|JSON-RPC over stdio| B[mcp-retroarch Server]
B -->|UDP datagrams| C{RetroArch Network Commands}
C -->|Enabled?| D[Commands processed]
C -->|Disabled| E[Timeout / Error]
F[Environment Variables] --> B
G[retroarch.cfg] --> CEnvironment Variables
The MCP server reads the following environment variables at startup to configure its UDP transport layer.
| Environment Variable | Default | Purpose |
|---|---|---|
RETROARCH_HOST | 127.0.0.1 | UDP destination host — the machine running RetroArch |
RETROARCH_PORT | 55355 | UDP port — must match network_cmd_port in RetroArch's config |
Sources: README.md:52-54
Configuration Precedence
Environment variables are read directly without any configuration file parsing. If not set, the defaults apply. The server does not support configuration via ~/.retroarch.cfg or similar — only environment variables control mcp-retroarch itself.
Non-blocking Startup
The MCP server starts immediately without blocking on RetroArch connectivity. A background probe attempts to connect and log the RetroArch version, but tool calls connect on demand if RetroArch is unreachable at startup.
Sources: src/index.ts:20-27
// Background connectivity probe — fire-and-forget, never blocks the server.
ra.connect()
.then(() => ra.getVersion())
.then((v) => process.stderr.write(`[mcp-retroarch] connected to ${ra.describeTarget()} — RetroArch ${v}\n`))
.catch((err) => process.stderr.write(
`[mcp-retroarch] note: RetroArch not reachable yet (${ra.describeTarget()}): ${err}\n` +
` Enable Network Commands in retroarch.cfg (network_cmd_enable / network_cmd_port)\n` +
` or Settings > Network > Network Commands. Tool calls will connect on demand.\n`,
));
Sources: src/index.ts:20-27
RetroArch Network Commands Setup
For mcp-retroarch to communicate with RetroArch, Network Commands must be enabled in RetroArch itself. This is a one-time setup per RetroArch installation.
Method 1: GUI Configuration
- Navigate to Settings → Network → Network Commands
- Set Network Commands to ON
- Confirm Network Cmd Port is
55355(the default)
Sources: README.md:25-28
Method 2: Configuration File
Add or edit the following in retroarch.cfg:
network_cmd_enable = "true"
network_cmd_port = "55355"
Sources: README.md:30-35
Port Matching Requirement
The port configured in RetroArch (network_cmd_port) must match the RETROARCH_PORT environment variable used by mcp-retroarch. Mismatches result in RetroArch query timed out errors.
Sources: README.md:52-54
graph LR
A[mcp-retroarch<br/>RETROARCH_PORT] -->|55355| B{Port Match?}
C[RetroArch<br/>network_cmd_port] -->|55355| B
B -->|Yes| D[Communication OK]
B -->|No| E[Timeout Error]Reading RetroArch Configuration
The retroarch_get_config tool queries RetroArch's internal configuration state via the NCI GET_CONFIG_PARAM command. This reads runtime values from RetroArch, not the static config file.
Sources: src/tools.ts:80-96
Available Configuration Parameters
RetroArch's NCI exposes a whitelist of configuration keys. The following parameters are confirmed available:
| Category | Parameter | Returns | Notes |
|---|---|---|---|
| Directories | savefile_directory | Absolute path | User's save file directory |
| Directories | savestate_directory | Absolute path | Savestate directory |
| Directories | system_directory | Absolute path | System/BIOS directory |
| Directories | cache_directory | Absolute path | Cache location |
| Directories | log_dir | Absolute path | Log file directory |
| Directories | runtime_log_directory | Absolute path | Runtime log directory |
| Directories | core_assets_directory | Absolute path | Downloaded core assets |
| User Data | netplay_nickname | String | Netplay display name |
| Video | video_fullscreen | true / false | Fullscreen toggle |
| Video | video_vsync | true / false | Vertical sync toggle |
| Audio | audio_mute_enable | true / false | Audio mute toggle |
Sources: src/tools.ts:80-96
Notable Exclusions
The screenshot_directory parameter is intentionally NOT exposed by RetroArch's NCI whitelist. Screenshots are saved to RetroArch's configured screenshot directory, but this cannot be queried programmatically. Check the value manually via Settings → Directory → Screenshot.
Sources: src/tools.ts:80-96
Current Savestate Slot
There is no NCI key for querying the currently-selected savestate slot. Track the slot number client-side by using retroarch_state_slot_plus and retroarch_state_slot_minus, or start from slot 1 (NCI default).
Sources: src/tools.ts:80-96
Tool Definition
{
"name": "retroarch_get_config",
"description": "PURPOSE: Read a single RetroArch configuration parameter by name via the NCI GET_CONFIG_PARAM command.",
"inputSchema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Config key — same snake_case ASCII identifier RetroArch uses in retroarch.cfg"
}
}
}
}
Sources: src/tools.ts:80-96
Return Format
NAME = VALUE
Where VALUE is the raw string as stored in retroarch.cfg:
- Paths are unquoted
- Booleans are
'true'/'false' - Integers are decimal
Sources: src/tools.ts:80-96
Error Conditions
The tool returns an error if:
- The parameter name is not in RetroArch's NCI whitelist
- The value contains characters that break the line-based reply parser (rare — embedded newlines or null bytes)
- The UDP query times out (RetroArch not reachable)
Sources: src/tools.ts:80-96
UDP Transport Layer
The communication between mcp-retroarch and RetroArch uses UDP datagrams on a serial-by-default basis.
Connection Behavior
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
const sock = dgram.createSocket("udp4");
sock.once("error", (err) => reject(err));
sock.bind(0, () => {
sock.on("message", (msg) => {
const cb = this.pending;
if (!cb) return; // unsolicited or late reply — drop
this.pending = null;
cb(msg);
});
sock.on("error", () => { /* swallow late errors */ });
this.socket = sock;
resolve();
});
});
}
Sources: src/retroarch.ts:10-27
Serial Query Model
The NCI protocol carries no request IDs, so matching responses by command-name echo is fragile. mcp-retroarch uses a serial-by-default approach: only one query can be in flight at a time.
async query(command: string): Promise<Buffer> {
if (!this.socket) await this.connect();
if (this.pending) {
throw new Error("retroarch query already in flight (client is serial)");
}
// ... send and wait for response
}
Sources: src/retroarch.ts:50-58
Fire-and-Forget Commands
Hotkey-style commands (pause toggle, frame advance, etc.) use fire-and-forget semantics since the NCI doesn't acknowledge them:
async send(command: string): Promise<void> {
if (!this.socket) await this.connect();
return new Promise((resolve, reject) => {
this.socket!.send(command, this.port, this.host, (err) =>
err ? reject(err) : resolve(),
);
});
}
Sources: src/retroarch.ts:35-43
MCP Client Registration
After configuring mcp-retroarch, register it with your MCP client.
Claude Code
claude mcp add retroarch --scope user mcp-retroarch
Verify registration:
claude mcp list
# retroarch: mcp-retroarch - ✓ Connected
Sources: README.md:38-43
Claude Desktop
Edit claude_desktop_config.json:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
{
"mcpServers": {
"retroarch": {
"command": "mcp-retroarch"
}
}
}
Sources: README.md:45-57
After editing, restart Claude Desktop for changes to take effect.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
RetroArch query timed out | Network Commands aren't enabled in RetroArch, or the port doesn't match RETROARCH_PORT. Confirm network_cmd_enable = "true" in retroarch.cfg. UDP datagrams can be dropped under load even on loopback — if a single call times out but a retry succeeds, that's the cause. |
READ_CORE_MEMORY failed: no memory map defined | The loaded libretro core doesn't advertise a system memory map. Try retroarch_read_ram (CHEEVOS path). |
READ_CORE_MEMORY failed: no descriptor for address | The address isn't covered by the core's memory map. Either a different core would expose it, or the address is outside the system bus. |
| Screenshots don't appear where I expect | RetroArch saves to its configured screenshot directory. This cannot be queried via NCI — check via Settings → Directory → Screenshot. |
Sources: README.md:85-98
Development Configuration
For local development:
npm install
npm run dev # tsc --watch
Sources: README.md:75-79
Smoke test against a running RetroArch:
node .scratch/smoke.cjs
Sources: README.md:80-82
Configuration Summary
| Layer | Setting | Default | Source |
|---|---|---|---|
| MCP Server | RETROARCH_HOST | 127.0.0.1 | Environment variable |
| MCP Server | RETROARCH_PORT | 55355 | Environment variable |
| RetroArch | network_cmd_enable | "true" | retroarch.cfg or GUI |
| RetroArch | network_cmd_port | 55355 | retroarch.cfg or GUI |
Sources: README.md:30-35, 52-54
Version History
| Version | Date | Configuration Changes |
|---|---|---|
| 0.1.2 | 2026-05-15 | Tool description quality pass — improved configuration parameter documentation |
| 0.1.1 | 2026-05-11 | Non-blocking startup — server no longer waits for RetroArch connectivity |
| 0.1.0 | 2026-05-10 | Initial release with UDP client and MCP server |
Sources: CHANGELOG.md:1-30
Sources: README.md:30-35
Doramagic Pitfall Log
Source-linked risks stay visible on the manual page so the preview does not read like a recommendation.
Users may get misleading failures or incomplete behavior unless configuration is checked carefully.
The project should not be treated as fully validated until this signal is reviewed.
Users cannot judge support quality until recent activity, releases, and issue response are checked.
The project may affect permissions, credentials, data exposure, or host boundaries.
Doramagic Pitfall Log
Doramagic extracted 7 source-linked risk signals. Review them before installing or handing real data to the project.
1. Configuration risk: Configuration risk needs validation
- Severity: medium
- Finding: Configuration risk is backed by a source signal: Configuration risk needs validation. Treat it as a review item until the current version is checked.
- User impact: Users may get misleading failures or incomplete behavior unless configuration is checked carefully.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: capability.host_targets | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | host_targets=mcp_host, claude
2. Capability assumption: README/documentation is current enough for a first validation pass.
- Severity: medium
- Finding: README/documentation is current enough for a first validation pass.
- User impact: The project should not be treated as fully validated until this signal is reviewed.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: capability.assumptions | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | README/documentation is current enough for a first validation pass.
3. Maintenance risk: Maintainer activity is unknown
- Severity: medium
- Finding: Maintenance risk is backed by a source signal: Maintainer activity is unknown. Treat it as a review item until the current version is checked.
- User impact: Users cannot judge support quality until recent activity, releases, and issue response are checked.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: evidence.maintainer_signals | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | last_activity_observed missing
4. Security or permission risk: no_demo
- Severity: medium
- Finding: no_demo
- User impact: The project may affect permissions, credentials, data exposure, or host boundaries.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: downstream_validation.risk_items | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | no_demo; severity=medium
5. Security or permission risk: no_demo
- Severity: medium
- Finding: no_demo
- User impact: The project may affect permissions, credentials, data exposure, or host boundaries.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: risks.scoring_risks | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | no_demo; severity=medium
6. Maintenance risk: issue_or_pr_quality=unknown
- Severity: low
- Finding: issue_or_pr_quality=unknown。
- User impact: Users cannot judge support quality until recent activity, releases, and issue response are checked.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: evidence.maintainer_signals | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | issue_or_pr_quality=unknown
7. Maintenance risk: release_recency=unknown
- Severity: low
- Finding: release_recency=unknown。
- User impact: Users cannot judge support quality until recent activity, releases, and issue response are checked.
- Recommended check: Open the linked source, confirm whether it still applies to the current version, and keep the first run isolated.
- Evidence: evidence.maintainer_signals | github_repo:1234498337 | https://github.com/dmang-dev/mcp-retroarch | release_recency=unknown
Source: Doramagic discovery, validation, and Project Pack records
Community Discussion Evidence
These external discussion links are review inputs, not standalone proof that the project is production-ready.
Count of project-level external discussion links exposed on this manual page.
Open the linked issues or discussions before treating the pack as ready for your environment.
Community Discussion Evidence
Doramagic exposes project-level community discussion separately from official documentation. Review these links before using mcp-retroarch with real data or production workflows.
- Configuration risk needs validation - GitHub / issue
Source: Project Pack community evidence and pitfall evidence