Doramagic Project Pack · Human Manual

mcp-retroarch

Related topics: System Architecture, Quick Start Guide

Introduction

Related topics: System Architecture, Quick Start Guide

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Component Responsibilities

Continue reading this section for the full explanation and source context.

Section Connection Flow

Continue reading this section for the full explanation and source context.

Section UDP Communication Model

Continue reading this section for the full explanation and source context.

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:

AttributeValue
ProtocolMCP over stdio
Transport to RetroArchUDP (IPv4)
Default UDP port55355
LanguageTypeScript
MCP SDK version^1.12.0
LicenseMIT

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"| A

Component Responsibilities

ComponentRole
MCP ClientSends JSON-RPC requests via stdio
mcp-retroarchTranslates MCP tools to NCI UDP commands
RetroArch NCIExecutes commands on the running emulator

Sources: src/index.ts:1-20

How It Works

Connection Flow

  1. Startup: The MCP server initializes and creates a UDP socket for RetroArch communication
  2. Background probe: An asynchronous connection attempt retrieves RetroArch version
  3. 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 response

Sources: src/index.ts:8-17

UDP Communication Model

The RetroArch client implements two communication patterns:

PatternMethodUse Case
Fire-and-forgetsend(command)Pause toggle, reset, screenshot
Query/Responsequery(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:

  1. RetroArch is installed with Network Commands enabled
  2. A libretro core and game are loaded
  3. 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 VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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

ToolPurposeAddress Space
retroarch_read_memoryRead bytes from system memory maplibretro memory map
retroarch_write_memoryWrite bytes to system memorylibretro memory map
retroarch_read_ramRead bytes via CHEEVOS address spaceAchievement space
retroarch_write_ramWrite bytes via CHEEVOS spaceAchievement space

Memory read/write limitations:

  • Maximum 4096 bytes per call (NCI single-datagram limit)
  • write_memory returns byte count; write_ram does not acknowledge

Emulation Control

ToolPurpose
retroarch_pause_toggleToggle pause state
retroarch_frame_advanceStep exactly one frame
retroarch_resetHard reset the game

State Management

ToolPurpose
retroarch_save_state_currentSave to current slot
retroarch_load_state_currentLoad from current slot
retroarch_load_state_slotLoad from explicit slot number
retroarch_state_slot_plusIncrement slot pointer
retroarch_state_slot_minusDecrement slot pointer

Utility

ToolPurpose
retroarch_screenshotSave screenshot to RetroArch's configured directory
retroarch_show_messageDisplay notification overlay
retroarch_get_statusQuery current emulation state
retroarch_get_configRead a config parameter value

Sources: src/tools.ts

Address Space Differences

Understanding the distinction between address spaces is critical:

Address SpaceUsed ByNotes
libretro memory mapread_memory / write_memorySystem-specific layout (e.g., GBA EWRAM at 0x02000000)
CHEEVOS spaceread_ram / write_ramRetroAchievements address conventions

Example address mappings:

  • GBA ROM: 0x08000000 (memory map) vs 0x000000 (CHEEVOS)
  • SNES WRAM: 0x7E0000-0x7FFFFF (memory map) vs 0x000000 (CHEEVOS)

Sources: src/tools.ts

Verified Core Support

The following cores have been tested with mcp-retroarch:

SystemCoreread_memoryread_ramNotes
Game Boy Advancemgba_libretroGBA interrupt vector at 0x0000 visible
NESmesen_libretroFull 16-bit address space; WRAM at 0x0000-0x07FF
NESnestopia_libretroNo memory map; use read_ram
SNESsnes9x_libretroMemory 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:

  1. Reading memory back with read_ram
  2. Checking state with retroarch_get_status

Sources: CHANGELOG.md

Error Handling

Common Errors

ErrorCauseResolution
RetroArch query timed outNetwork Commands disabled or port mismatchVerify network_cmd_enable = "true" and matching ports
READ_CORE_MEMORY failed: no memory map definedCore doesn't expose memory mapUse retroarch_read_ram instead
READ_CORE_MEMORY failed: no descriptor for addressAddress outside core's memory regionsTry 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

FeatureStatusNotes
Game-pad input❌ Not availableNCI doesn't expose input; see mcp-mgba for GBA input
Save to specific slotLimitedOnly current slot save; must walk to target slot
Screenshot directoryNot configurable via NCICheck RetroArch GUI for configured path

Sources: README.md

ProjectPurpose
mcp-mgbaGBA via mGBA's Lua bridge (includes input + screenshot)
mcp-pinePCSX2 and PINE-speaking emulators

Sources: README.md

Sources: README.md

Quick Start Guide

Related topics: Introduction, Configuration

Section Related Pages

Continue reading this section for the full explanation and source context.

Section From npm (Recommended)

Continue reading this section for the full explanation and source context.

Section From Source

Continue reading this section for the full explanation and source context.

Section Via GUI

Continue reading this section for the full explanation and source context.

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:#e8f5e9

Sources: src/index.ts:8-14, src/retroarch.ts

Prerequisites

RequirementDetails
Node.jsVersion compatible with @modelcontextprotocol/sdk ^1.12.0
RetroArchVersion with Network Command Interface enabled
MCP ClientClaude Code or Claude Desktop
Network CommandsMust be enabled in RetroArch

Sources: package.json:20-26, README.md:40-50

Step 1: Install mcp-retroarch

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

  1. Open RetroArch
  2. Navigate to Settings → Network → Network Commands
  3. Set Network Commands to ON
  4. 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:

PlatformConfig 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 VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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

ToolPurpose
retroarch_get_statusQuery running state, system, game, CRC32
retroarch_pause_toggleToggle pause on/off
retroarch_frame_advanceAdvance exactly one frame
retroarch_resetHard reset the running game
retroarch_show_messageDisplay notification on RetroArch window

Memory Operations

ToolDescription
retroarch_read_memoryRead from libretro system memory map
retroarch_write_memoryWrite to libretro system memory map
retroarch_read_ramRead from CHEEVOS (achievements) address space
retroarch_write_ramWrite to CHEEVOS address space

Savestate Management

ToolPurpose
retroarch_save_state_currentSave to currently-selected slot
retroarch_load_state_currentLoad from currently-selected slot
retroarch_load_state_slotLoad from explicit slot number
retroarch_state_slot_plusIncrement slot pointer
retroarch_state_slot_minusDecrement slot pointer

Media & Config

ToolPurpose
retroarch_screenshotSave screenshot to RetroArch's screenshot directory
retroarch_get_configRead 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

SystemCoreread_memoryread_ramNotes
Game Boy Advancemgba_libretroGBA interrupt vector at 0x0000
NESmesen_libretroFull 16-bit NES address space
NESnestopia_libretroCHEEVOS only
SNESsnes9x_libretroMemory map not exposed

Sources: README.md:30-45

Troubleshooting

SymptomCause / Fix
RetroArch query timed outNetwork Commands not enabled, or port mismatch. Confirm network_cmd_enable = "true" in retroarch.cfg
READ_CORE_MEMORY failed: no memory map definedCore doesn't advertise memory map. Try retroarch_read_ram as fallback
READ_CORE_MEMORY failed: no descriptor for addressAddress outside core's memory regions
Screenshots don't appearCheck RetroArch's screenshot directory via Settings → Directory → Screenshot
Can't save to specific slotNCI limitation—use state_slot_plus/state_slot_minus to walk to target slot

Sources: README.md:115-130

Next Steps

Sources: README.md

Sources: README.md

System Architecture

Related topics: Introduction, Data Flow

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Layer 1: MCP Server Entry Point

Continue reading this section for the full explanation and source context.

Section Layer 2: UDP Transport Module

Continue reading this section for the full explanation and source context.

Section Layer 3: MCP Tools Definition

Continue reading this section for the full explanation and source context.

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 --> RA

Sources: 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 response

Connection Management:

MethodPurpose
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:

ParameterDefaultDescription
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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:

CategoryTools
Memoryretroarch_read_memory, retroarch_write_memory, retroarch_read_ram, retroarch_write_ram
Stateretroarch_save_state_current, retroarch_load_state_current, retroarch_load_state_slot, retroarch_state_slot_plus, retroarch_state_slot_minus
Controlretroarch_pause_toggle, retroarch_frame_advance, retroarch_reset
Mediaretroarch_screenshot, retroarch_show_message
Inforetroarch_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 ToolNCI CommandResponse Pattern
retroarch_get_versionVERSIONString version
retroarch_get_statusGET_STATUSGET_STATUS {state} {system},{game},crc32={crc}
retroarch_read_memoryREAD_CORE_MEMORY {addr} {len}Binary bytes
retroarch_write_memoryWRITE_CORE_MEMORY {addr} {bytes}{count} bytes written
retroarch_read_ramREAD_CORE_RAM {addr} {len}Binary bytes (CHEEVOS)
retroarch_write_ramWRITE_CORE_RAM {addr} {bytes}Fire-and-forget
retroarch_pause_togglePAUSE_TOGGLEFire-and-forget
retroarch_frame_advanceFRAMEADVANCEFire-and-forget
retroarch_resetRESETFire-and-forget
retroarch_save_state_currentSAVE_STATEFire-and-forget
retroarch_load_state_currentLOAD_STATEFire-and-forget
retroarch_load_state_slotLOAD_STATE {slot}Fire-and-forget
retroarch_screenshotSCREENSHOTFire-and-forget
retroarch_show_messageSET_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 ConditionCauseResolution
RetroArch query timed outNetwork Commands not enabled or port mismatchVerify network_cmd_enable = "true" in retroarch.cfg
no memory map definedCore doesn't advertise system memory mapUse retroarch_read_ram as fallback
no descriptor for addressAddress outside core's memory regionsUse different core or address
query already in flightMultiple concurrent queries attemptedWait 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.

SystemTypical Address Range
GBA EWRAM0x02000000 - 0x0203FFFF
SNES WRAM0x7E0000 - 0x7FFFFF
Genesis 68K RAM0xFF0000 - 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

DependencyVersionPurpose
@modelcontextprotocol/sdk^1.12.0MCP server implementation
@types/node^22.0.0TypeScript definitions
typescript^5.5.0Build tooling

Sources: package.json

Tested Cores Compatibility

SystemCoreread_memoryread_ramNotes
Game Boy Advancemgba_libretroGBA interrupt vector at 0x0000
NESmesen_libretroFull 16-bit address space
NESnestopia_libretroCHEEVOS only
SNESsnes9x_libretroN/AMemory map not exposed
PSXswanstation_libretroUse 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

Section Related Pages

Continue reading this section for the full explanation and source context.

Section 1. MCP Request Ingress

Continue reading this section for the full explanation and source context.

Section 2. Tool Handler Processing

Continue reading this section for the full explanation and source context.

Section 3. UDP Query Execution

Continue reading this section for the full explanation and source context.

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:

LayerTransportProtocol
MCP Client ↔ mcp-retroarchstdioJSON-RPC 2.0
mcp-retroarch ↔ RetroArchUDP (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"| D

Request-Response Flow

1. MCP Request Ingress

When an MCP client invokes a tool (e.g., retroarch_read_memory), the following occurs:

  1. The client sends a JSON-RPC 2.0 request via stdio
  2. src/index.ts receives the request and routes it to the appropriate tool handler in src/tools.ts
  3. 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 dump

Sources: 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_toggle
  • retroarch_reset
  • retroarch_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

  1. Tool handler receives address and length parameters
  2. RetroArch client calls readMemory() or readRam()
  3. UDP query sent with address and byte count
  4. Response parsing extracts bytes from NCI response
  5. 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

APIAcknowledgmentUse Case
write_memoryYes (returns byte count)Precise writes, cheats
write_ramNo (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

ErrorCauseUser Message
RetroArch query timed outNetwork Commands disabled or wrong portCheck network_cmd_enable in retroarch.cfg
no memory map definedCore doesn't expose system memory mapUse read_ram / write_ram instead
no descriptor for addressAddress outside core's memory regionsUse different address or core
retroarch query already in flightConcurrent query attemptedTool 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
VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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 ToolNCI CommandTransportExpects Response
retroarch_get_statusGET_STATUSqueryYes
retroarch_get_configGET_CONFIG_PARAMqueryYes
retroarch_read_memoryREAD_CORE_MEMORYqueryYes
retroarch_read_ramREAD_CORE_RAMqueryYes
retroarch_write_memoryWRITE_CORE_MEMORYqueryYes
retroarch_write_ramWRITE_CORE_RAMsendNo
retroarch_save_state_currentSAVE_STATEsendNo
retroarch_load_state_currentLOAD_STATEsendNo
retroarch_load_state_slotLOAD_STATEsendNo
retroarch_pause_togglePAUSE_TOGGLEsendNo
retroarch_frame_advanceFRAMEADVANCEsendNo
retroarch_resetRESETsendNo
retroarch_screenshotSCREENSHOTsendNo
retroarch_show_messageSHOW_MSGsendNo

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:

VersionBehavior
≤ 0.1.0Blocked ~5s on VERSION query timeout
≥ 0.1.1Non-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:

  1. MCP Layer: JSON-RPC over stdio for client-server communication
  2. Tool Layer: Parameter validation and response formatting in src/tools.ts
  3. Transport Layer: UDP socket management in src/retroarch.ts
  4. 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

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Transport Layer

Continue reading this section for the full explanation and source context.

Section System Memory Map (memory)

Continue reading this section for the full explanation and source context.

Section CHEEVOS Address Space (ram)

Continue reading this section for the full explanation and source context.

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| E

Transport 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 CommandREAD_CORE_MEMORY / WRITE_CORE_MEMORYREAD_CORE_RAM / WRITE_CORE_RAM
Address SpaceSystem memory busCHEEVOS (achievement) space
Core RequirementCore must expose memory mapWorks via CHEEVOS interface
FallbackFalls 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_MEMORY returns 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:

SystemCHEEVOS WRAM StartSystem Bus WRAM Start
SNES0x0000000x7E0000
GBA0x030000000x02000000 (EWRAM)

Sources: src/tools.ts:95-120

Tool Reference

`retroarch_read_memory`

Read bytes via the libretro core's system memory map.

ParameterTypeRequiredDescription
addressintegerStarting address in system memory map
lengthintegerBytes 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.

ParameterTypeRequiredDescription
addressintegerStarting address in system memory map
bytesinteger[]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_current first)

Sources: src/tools.ts:100-140

`retroarch_read_ram`

Read bytes via the CHEEVOS (achievement) address space.

ParameterTypeRequiredDescription
addressintegerStarting address in CHEEVOS space
lengthintegerBytes 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.

ParameterTypeRequiredDescription
addressintegerStarting address in CHEEVOS space
bytesinteger[]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| F

Write 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

SystemRegionSystem BusCHEEVOS Space
GBAEWRAM0x02000000-0x0203FFFF0x03000000 offset
GBAIWRAM0x03000000-0x03007FFFDirect
SNESWRAM0x7E0000-0x7FFFFF0x000000
Genesis68K RAM0xFF0000-0xFFFFFFN/A

Sources: src/tools.ts:85-95

Limitations

LimitationCauseWorkaround
Max 4096 bytes/callNCI single-datagram sizeBatch larger reads in 4 KiB chunks
No "save to slot N"NCI protocol limitationWalk slot pointer with state_slot_plus/state_slot_minus
write_ram has no ackRetroArch doesn't respond to WRITE_CORE_RAMFollow up with retroarch_read_ram
Address spaces differCHEEVOS vs system busUse _memory tools when possible
Memory regions boundedCore exposes specific regionsSome addresses (VRAM, etc.) may be inaccessible

Sources: src/tools.ts:145-155

Configuration

Environment VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Tool Parameters

Continue reading this section for the full explanation and source context.

Section Return Values

Continue reading this section for the full explanation and source context.

Section How Slots Work

Continue reading this section for the full explanation and source context.

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:

LayerTechnologyRole
MCP Protocolstdio + JSON-RPCClient-facing tool interface
BridgeTypeScript (src/retroarch.ts)Protocol translation and UDP communication
TransportUDP datagramsDirect communication with RetroArch NCI
TargetRetroArch NCISavestate command execution

Savestate Tools

ToolNCI CommandPurpose
retroarch_save_state_currentSAVE_STATESave to currently-selected slot
retroarch_load_state_currentLOAD_STATELoad from currently-selected slot
retroarch_load_state_slotLOAD_STATE_SLOT NLoad from explicit slot N (1-9)
retroarch_state_slot_plusSTATE_SLOT_PLUSIncrement slot pointer
retroarch_state_slot_minusSTATE_SLOT_MINUSDecrement 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:

ToolReturn 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.

OperationAvailable?Workaround
Save to current slot✅ Yesretroarch_save_state_current
Save to specific slot❌ NoWalk slot pointer with plus/minus, then save
Load from current slot✅ Yesretroarch_load_state_current
Load from specific slot✅ Yesretroarch_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:

  1. Confirm network_cmd_enable = "true" in retroarch.cfg
  2. Verify network_cmd_port = "55355" matches RETROARCH_PORT environment variable
  3. 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 VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP port (must match network_cmd_port in retroarch.cfg)
ToolRelationship
retroarch_get_statusUse to verify game state before loading states
retroarch_frame_advanceStep frames after loading state to verify restore
retroarch_show_messageEcho 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

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Transport Layer

Continue reading this section for the full explanation and source context.

Section Serialization Constraint

Continue reading this section for the full explanation and source context.

Section Pause Toggle

Continue reading this section for the full explanation and source context.

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 --> B

Key transport behaviors:

MethodBehaviorUse Case
send(command)Fire-and-forget, no response expectedPause, reset, frame advance
query(command)Sends command, awaits one UDP responseMemory reads, status queries
connect()Binds UDP socket to ephemeral portTransport initialization
disconnect()Closes socketCleanup

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 --> E

Important: 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.

PropertyValue
Input Schema{} (no parameters)
Return"Pause toggled" (UDP-send confirmation only)
Fire-and-forgetYes — 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.

PropertyValue
Input Schema{} (no parameters)
PrerequisiteEmulation must be paused
Use CaseFrame-precise input automation, animation inspection
AlternativeFor 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.

PropertyValue
Input Schema{} (no parameters)
Return"Game reset" (UDP-send confirmation only)
BehaviorImmediately 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

ToolPurposeInputReturn
retroarch_save_state_currentSave to current slotNone"Saved to current slot"
retroarch_load_state_currentLoad from current slotNone"Loaded from current slot"
retroarch_load_state_slotLoad from explicit slot{ slot: number }"Loaded from slot N"
retroarch_state_slot_plusIncrement slot pointerNoneSlot number confirmation
retroarch_state_slot_minusDecrement slot pointerNoneSlot 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:

  1. Call retroarch_state_slot_plus five times
  2. 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.

PropertyValue
Input Schema{} (no parameters)
Return"Screenshot saved to RetroArch's configured screenshot directory"
DirectoryMust be verified in RetroArch GUI: Settings → Directory → Screenshot
NCI LimitationThe 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).

PropertyValue
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)
QueueingMessages are NOT queued — rapid calls replace the previous message
Use CaseDebug 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| B

Important: 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

SymptomCauseFix
RetroArch query timed outNetwork Commands disabled or port mismatchVerify network_cmd_enable = "true" in retroarch.cfg
retroarch query already in flightCode bug or concurrent tool callsEnsure serial tool execution
Screenshot not where expectedScreenshot saved to RetroArch's configured directoryCheck 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 VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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

ToolTypeInputFire-and-ForgetVerified Return
retroarch_pause_toggleControlNoneYesConfirmation only
retroarch_frame_advanceControlNoneYesConfirmation only
retroarch_resetControlNoneYesConfirmation only
retroarch_save_state_currentStateNoneYesConfirmation only
retroarch_load_state_currentStateNoneYesConfirmation only
retroarch_load_state_slotState{ slot }YesConfirmation only
retroarch_state_slot_plusStateNoneYesConfirmation only
retroarch_state_slot_minusStateNoneYesConfirmation only
retroarch_screenshotVisualNoneYesConfirmation only
retroarch_show_messageVisual{ message }YesConfirmation 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

Section Related Pages

Continue reading this section for the full explanation and source context.

Section System Memory Map (memory tools)

Continue reading this section for the full explanation and source context.

Section CHEEVOS Address Space (ram tools)

Continue reading this section for the full explanation and source context.

Section Memory Tool Comparison

Continue reading this section for the full explanation and source context.

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:

CategoryToolsPurpose
Status & Configretroarch_get_status, retroarch_get_configQuery emulator state and configuration
Memory Accessretroarch_read_memory, retroarch_write_memory, retroarch_read_ram, retroarch_write_ramRead/write emulated system memory
Savestateretroarch_save_state_current, retroarch_load_state_current, retroarch_load_state_slot, retroarch_state_slot_plus, retroarch_state_slot_minusManage game save states
Emulator Controlretroarch_pause_toggle, retroarch_frame_advance, retroarch_resetControl emulation execution
Media & UIretroarch_screenshot, retroarch_show_messageCapture 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:

SystemMemory RegionAddress Range
GBA EWRAMExternal Work RAM0x02000000-0x0203FFFF
SNES WRAMWork RAM0x7E0000-0x7FFFFF
Genesis 68K RAMMain RAM0xFF0000-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 _memory tools as the starting point for memory access
  • Fall back to _ram tools when _memory returns 'no memory map defined' (older cores)
  • read_ram confirmed working for SwanStation (PSX), Mesen (NES) Sources: README.md

Memory Tool Comparison

Feature_memory tools_ram tools
NCI CommandREAD_CORE_MEMORY / WRITE_CORE_MEMORYREAD_CORE_RAM / WRITE_CORE_RAM
Address SpaceSystem memory mapCHEEVOS (achievements)
Write AcknowledgmentReturns byte countNo acknowledgment (fire-and-forget)
Core SupportRequires memory mapBroader (achievements-compatible cores)
Max Bytes/Call40964096

Status & Configuration Tools

retroarch_get_status

Queries the current emulator state including run-state, loaded ROM, and CRC32.

Input: No parameters

Returns:

  • State: playing|paused
  • System: SYSTEM_ID
  • Game: BASENAME
  • CRC32: 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:

ParameterTypeRequiredDescription
namestringYesConfiguration parameter name

Notes:

  • Uses GET_CONFIG_PARAM command
  • RetroArch whitelists exposed parameters; non-whitelisted names error
  • screenshot_directory is 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:

ParameterTypeRequiredConstraintsDescription
addressintegerYes≥ 0Starting address in system memory map
lengthintegerYes1-4096Number 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:

ParameterTypeRequiredConstraintsDescription
addressintegerYes≥ 0Starting address in system memory map
bytesarrayYes1-4096 elements, each 0-255Byte 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:

ParameterTypeRequiredConstraintsDescription
addressintegerYes≥ 0Starting address in CHEEVOS space
lengthintegerYes1-4096Number of bytes to read

Returns: ADDR_HEX [N bytes, CHEEVOS]: followed by space-separated uppercase hex bytes

Notes:

  • Fallback when _memory returns 'no memory map defined'
  • CHEEVOS addresses follow RetroAchievements conventions (e.g., SNES WRAM starts at 0x000000, not 0x7E0000) Sources: src/tools.ts

retroarch_write_ram

Writes bytes to the CHEEVOS address space via WRITE_CORE_RAM.

Parameters:

ParameterTypeRequiredConstraintsDescription
addressintegerYes≥ 0Starting address in CHEEVOS space
bytesarrayYes1-4096 elements, each 0-255Byte 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_ram after writing Sources: src/tools.ts

Savestate Management

The NCI protocol has limitations for savestate operations:

  1. No direct "save to slot N" - only save to the currently-selected slot
  2. No query for current slot - client must track the slot pointer
  3. 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:

ParameterTypeRequiredDescription
slotintegerYesSavestate 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_status first 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_directory via GET_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:

ParameterTypeRequiredDescription
messagestringYesMessage 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:

  1. Network Commands not enabled in RetroArch
  2. Port mismatch between RETROARCH_PORT and RetroArch's network_cmd_port
  3. 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 VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP port (must match network_cmd_port in retroarch.cfg)

Error Handling

Error MessageCause / Fix
RetroArch query timed outNetwork Commands not enabled; port mismatch; UDP dropped
READ_CORE_MEMORY failed: no memory map definedCore doesn't advertise memory map; use read_ram instead
READ_CORE_MEMORY failed: no descriptor for addressAddress outside core's memory regions
Screenshots don't appearCheck RetroArch's screenshot directory setting via GUI
Can't save to specific slot directlyNCI limitation; walk slot pointer with state_slot_plus/minus first

Tool Summary Table

ToolInput ParametersReturnsSide Effects
retroarch_get_statusEmulator state infoNone
retroarch_get_confignameConfig valueNone
retroarch_read_memoryaddress, lengthHex dumpNone
retroarch_write_memoryaddress, bytesByte countDisables hardcore mode
retroarch_read_ramaddress, lengthHex dump (CHEEVOS)None
retroarch_write_ramaddress, bytesConfirmationDisables hardcore mode
retroarch_pause_togglePause toggledChanges run-state
retroarch_frame_advanceFrame advancedAdvances emulation
retroarch_resetGame resetHard-resets game
retroarch_screenshotScreenshot pathCreates file
retroarch_show_messagemessageMessage displayedShows notification
retroarch_save_state_currentSave confirmedOverwrites current slot
retroarch_load_state_currentLoad confirmedLoads current slot
retroarch_load_state_slotslotLoad confirmedLoads specified slot
retroarch_state_slot_plusSlot movedIncrements slot pointer
retroarch_state_slot_minusSlot movedDecrements slot pointer

Source: https://github.com/dmang-dev/mcp-retroarch / Human Manual

Core Compatibility

Related topics: Memory Read/Write Operations

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Memory Access Methods Comparison

Continue reading this section for the full explanation and source context.

Section Address Space Differences

Continue reading this section for the full explanation and source context.

Section Memory Read/Write

Continue reading this section for the full explanation and source context.

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:

  1. Whether the loaded core exposes a system memory map via libretro
  2. Whether the core responds to the CHEEVOS (RetroAchievements) read API
  3. 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

Aspectretroarch_read_memoryretroarch_read_ram
NCI CommandREAD_CORE_MEMORYREAD_CORE_RAM
Address SpaceLibretro system memory mapCHEEVOS achievement address space
Core RequirementCore must expose memory map descriptorsWorks if core exposes CHEEVOS
FallbackAuto-fall back to read_ram on "no memory map"No further fallback
Use CasePrimary tool for memory inspectionFallback / 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"]
    end

For 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:

SystemCoreread_memoryread_ramNotes
Game Boy Advancemgba_libretroGBA interrupt vector table visible at 0x0000 (d3 00 00 ea ...)
NESmesen_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.
NESnestopia_libretro❌ (no memory map)CHEEVOS only. 64 KB bound. For NES + memory map, prefer Mesen.
SNESsnes9x_libretroStatus not fully documented in current release

Sources: README.md

Feature Support by Core

Memory Read/Write

CapabilitySupportedDetails
System memory mapCore-dependentOnly cores that expose descriptors via READ_CORE_MEMORY
CHEEVOS RAM readMost coresAchievement API is widely supported
Memory writeSame pathways as readsWRITE_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:

OperationSupportNotes
Save to current slotUses SAVE_STATE_CURRENT
Load from current slotUses LOAD_STATE_CURRENT
Load from explicit slotUses LOAD_STATE with slot number
Save to explicit slotNCI 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:#ccffcc

Decision Criteria

  1. Start with retroarch_read_memory — It provides access to the native system memory map and is the preferred pathway.
  1. Fall back to retroarch_read_ram when:
  • The core returns "no memory map defined"
  • You need CHEEVOS achievement-style addresses
  • Working with cores like nestopia_libretro that don't expose system memory maps
  1. Write operations follow the same path as your read operations.

Sources: src/tools.ts

Core-Specific Behaviors

NES Cores Comparison

FeatureMesenNestopia
System memory map✅ Exposed❌ Not exposed
CHEEVOS RAM✅ 64 KB bound✅ 64 KB bound
Full address space✅ 0x0000-0xFFFFLimited to first 64 KB
RecommendationPreferred for memory workUse for achievements only

GBA with mgba_libretro

The mgba_libretro core provides full memory map access:

  • Interrupt vector table at 0x0000 is readable via read_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

SymptomCauseSolution
READ_CORE_MEMORY failed: no memory map definedCore doesn't expose system memory mapUse retroarch_read_ram instead
READ_CORE_MEMORY failed: no descriptor for addressAddress outside core's memory regionsUse a different core or check valid address range
RetroArch query timed outNetwork Commands not enabled or port mismatchEnable in RetroArch GUI or set network_cmd_enable = "true"
Inconsistent responsesUDP datagram drops under loadRetry the query—mcp-retroarch doesn't auto-retry

Sources: README.md

Configuration for Core Support

Environment VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host
RETROARCH_PORT55355UDP 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

PlatformToolFeatures
Game Boy Advancemcp-mgbaMemory + button input + screenshot via mGBA Lua bridge
PCSX2, etc.mcp-pineMemory + 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

Section Related Pages

Continue reading this section for the full explanation and source context.

Section Configuration Precedence

Continue reading this section for the full explanation and source context.

Section Non-blocking Startup

Continue reading this section for the full explanation and source context.

Section Method 1: GUI Configuration

Continue reading this section for the full explanation and source context.

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:

  1. Network transport — where to send UDP commands (host and port)
  2. RetroArch NCI readiness — ensuring RetroArch has Network Commands enabled
  3. 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] --> C

Environment Variables

The MCP server reads the following environment variables at startup to configure its UDP transport layer.

Environment VariableDefaultPurpose
RETROARCH_HOST127.0.0.1UDP destination host — the machine running RetroArch
RETROARCH_PORT55355UDP 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

  1. Navigate to Settings → Network → Network Commands
  2. Set Network Commands to ON
  3. 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:

CategoryParameterReturnsNotes
Directoriessavefile_directoryAbsolute pathUser's save file directory
Directoriessavestate_directoryAbsolute pathSavestate directory
Directoriessystem_directoryAbsolute pathSystem/BIOS directory
Directoriescache_directoryAbsolute pathCache location
Directorieslog_dirAbsolute pathLog file directory
Directoriesruntime_log_directoryAbsolute pathRuntime log directory
Directoriescore_assets_directoryAbsolute pathDownloaded core assets
User Datanetplay_nicknameStringNetplay display name
Videovideo_fullscreentrue / falseFullscreen toggle
Videovideo_vsynctrue / falseVertical sync toggle
Audioaudio_mute_enabletrue / falseAudio 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:

  1. The parameter name is not in RetroArch's NCI whitelist
  2. The value contains characters that break the line-based reply parser (rare — embedded newlines or null bytes)
  3. 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:

PlatformPath
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

SymptomCause / Fix
RetroArch query timed outNetwork 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 definedThe loaded libretro core doesn't advertise a system memory map. Try retroarch_read_ram (CHEEVOS path).
READ_CORE_MEMORY failed: no descriptor for addressThe 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 expectRetroArch 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

LayerSettingDefaultSource
MCP ServerRETROARCH_HOST127.0.0.1Environment variable
MCP ServerRETROARCH_PORT55355Environment variable
RetroArchnetwork_cmd_enable"true"retroarch.cfg or GUI
RetroArchnetwork_cmd_port55355retroarch.cfg or GUI

Sources: README.md:30-35, 52-54

Version History

VersionDateConfiguration Changes
0.1.22026-05-15Tool description quality pass — improved configuration parameter documentation
0.1.12026-05-11Non-blocking startup — server no longer waits for RetroArch connectivity
0.1.02026-05-10Initial 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.

medium Configuration risk needs validation

Users may get misleading failures or incomplete behavior unless configuration is checked carefully.

medium README/documentation is current enough for a first validation pass.

The project should not be treated as fully validated until this signal is reviewed.

medium Maintainer activity is unknown

Users cannot judge support quality until recent activity, releases, and issue response are checked.

medium no_demo

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.

Sources 1

Count of project-level external discussion links exposed on this manual page.

Use Review before install

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.

Source: Project Pack community evidence and pitfall evidence