Skip to content

Commit 84eeb8b

Browse files
committed
feat: initial mcp-retroarch project — MCP server for RetroArch
RetroArch's Network Control Interface (NCI) is a text-based UDP protocol on port 55355 that ships with every RetroArch build. This project is a thin TypeScript MCP server that translates MCP tool calls into NCI commands, so any MCP client (Claude Desktop, Claude Code, etc.) can drive any libretro core — read/write memory, save/ load state, pause/frame-advance/reset, take screenshots, and show on-screen messages. - src/retroarch.ts: serial UDP client for the NCI text protocol - src/tools.ts: 17 MCP tools covering memory r/w (system map + CHEEVOS), save/load state suite, emulator control, screenshot, show-message, get-config - src/index.ts: stdio MCP entry; configurable target via RETROARCH_HOST / RETROARCH_PORT env vars End-to-end verified against RetroArch 1.22.2 with the mGBA core loaded on totp-gba-test: GET_STATUS returned game_boy_advance + game name + CRC32 READ_CORE_MEMORY at 0x0000 returned the GBA's interrupt vectors (recognizable as ARM "ea" branch instructions) READ_CORE_RAM at 0x0000 returned a different address space (CHEEVOS), confirming both paths are wired up correctly SHOW_MSG rendered a notification on the RetroArch overlay Honest scope: the NCI doesn't expose game-pad input — only menu navigation and hotkeys. Controller injection requires the separate "Remote RetroPad" core on UDP port 55400+, which is out of scope for v0.1.0. For input + screenshot on Game Boy Advance, see mcp-mgba.
0 parents  commit 84eeb8b

12 files changed

Lines changed: 2245 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
build:
12+
name: build (node ${{ matrix.node }} on ${{ matrix.os }})
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os: [ubuntu-latest, windows-latest, macos-latest]
18+
node: [18, 20, 22]
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Setup Node ${{ matrix.node }}
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: ${{ matrix.node }}
27+
cache: npm
28+
29+
- name: Install
30+
run: npm ci --ignore-scripts
31+
32+
- name: Type-check & build
33+
run: npm run build
34+
35+
- name: Verify shebang and bin
36+
shell: bash
37+
run: |
38+
head -1 dist/index.js | grep -q '^#!/usr/bin/env node$'
39+
test -f dist/index.js
40+
test -f dist/retroarch.js
41+
test -f dist/tools.js
42+
echo "build artifacts OK"

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
dist/
3+
*.js.map
4+
.rstk/
5+
.scratch/
6+
*.png

CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [0.1.0] - 2026-05-10
11+
12+
Initial public release.
13+
14+
### Added
15+
16+
- **UDP client (`src/retroarch.ts`)** for RetroArch's text-based Network
17+
Control Interface. Serial-by-default (one query in flight at a time)
18+
to keep matching simple — the NCI doesn't carry request IDs and
19+
matching by command-name echo is fragile.
20+
- **MCP server (`dist/index.js`)** with eager probe at startup; tools
21+
start working as soon as RetroArch is reachable.
22+
- **17 MCP tools**: `retroarch_ping`, `retroarch_get_status`,
23+
`retroarch_get_config`, `retroarch_read_memory` /
24+
`retroarch_write_memory` (system memory map), `retroarch_read_ram` /
25+
`retroarch_write_ram` (CHEEVOS fallback), `retroarch_pause_toggle`,
26+
`retroarch_frame_advance`, `retroarch_reset`, `retroarch_screenshot`,
27+
`retroarch_show_message`, save/load state suite (current slot, explicit
28+
slot, slot pointer +/-).
29+
- **Configurable target** via `RETROARCH_HOST` and `RETROARCH_PORT` env
30+
vars.
31+
- **Cross-platform install** via `npm install -g mcp-retroarch`,
32+
`npx -y mcp-retroarch`, or clone-and-build.
33+
- **GitHub Actions CI** matrix on Node 18/20/22 across
34+
Linux / macOS / Windows.
35+
36+
### Known limitations
37+
38+
- **Game-pad input not exposed.** The NCI doesn't expose
39+
controller-button injection — only RetroArch hotkeys (pause, reset,
40+
state slots, etc.). RetroArch has a separate "Remote RetroPad" core
41+
on UDP port 55400+ that does, but it requires loading that specific
42+
core, which means you can't drive a normal libretro emulation core
43+
through it. Out of scope for v0.1.0; may revisit.
44+
- **Save-state slot targeting is two-step.** NCI's `SAVE_STATE` only
45+
saves to the currently-selected slot. To save to slot N, walk the
46+
slot pointer to N first via `state_slot_plus` / `state_slot_minus`.
47+
48+
[Unreleased]: https://github.com/dmang-dev/mcp-retroarch/compare/v0.1.0...HEAD
49+
[0.1.0]: https://github.com/dmang-dev/mcp-retroarch/releases/tag/v0.1.0

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 dmang-dev
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# mcp-retroarch
2+
3+
[![npm version](https://img.shields.io/npm/v/mcp-retroarch.svg)](https://www.npmjs.com/package/mcp-retroarch)
4+
[![npm downloads](https://img.shields.io/npm/dm/mcp-retroarch.svg)](https://www.npmjs.com/package/mcp-retroarch)
5+
[![CI](https://github.com/dmang-dev/mcp-retroarch/actions/workflows/ci.yml/badge.svg)](https://github.com/dmang-dev/mcp-retroarch/actions/workflows/ci.yml)
6+
[![License: MIT](https://img.shields.io/npm/l/mcp-retroarch.svg)](LICENSE)
7+
8+
An [MCP](https://modelcontextprotocol.io) server that bridges Claude (and any other MCP client) to [RetroArch](https://www.retroarch.com/) via its built-in **Network Control Interface** (UDP, port 55355).
9+
10+
Works against any libretro core (NES, SNES, Genesis, GB/GBC/GBA, PSX, N64, etc.) — give the model memory r/w, save-state automation, screenshot, pause / frame-advance / reset, and on-screen messages.
11+
12+
## What it can do
13+
14+
| Capability | Available? | Notes |
15+
|---|---|---|
16+
| Memory read / write || Two paths: `READ_CORE_MEMORY` (system memory map, preferred) and `READ_CORE_RAM` (CHEEVOS, fallback) |
17+
| Save / load state || Current slot or explicit slot for load; save is current-slot-only (NCI limitation) |
18+
| Screenshot || Saved to RetroArch's configured screenshot directory |
19+
| Pause / frame advance || `PAUSE_TOGGLE` flips state; `FRAMEADVANCE` steps one frame |
20+
| Reset || Hard-reset the running game |
21+
| On-screen message || Useful for "look here" cues during scripted runs |
22+
| Game info || Title, system, CRC32 |
23+
| **Game-pad input** || **NCI doesn't expose this.** RetroArch has a separate "Remote RetroPad" core on UDP port 55400 that does, but it requires loading that specific core (you can't drive an existing emulation core through it). Not in scope for v0.1.0. |
24+
25+
If you need game-pad input on Game Boy Advance specifically, see [mcp-mgba](https://github.com/dmang-dev/mcp-mgba). For PCSX2 (memory + savestate only, no input/screenshot), see [mcp-pine](https://github.com/dmang-dev/mcp-pine).
26+
27+
## How it works
28+
29+
```
30+
+----------------+ stdio +-----------------+ UDP :55355 +-----------------+
31+
| MCP client | JSON-RPC | mcp-retroarch | text proto | RetroArch |
32+
| (Claude etc) | -----------> | (Node.js) | ------------> | (NCI enabled) |
33+
+----------------+ +-----------------+ +-----------------+
34+
```
35+
36+
## Requirements
37+
38+
- **RetroArch** (any recent version) with Network Commands enabled
39+
- **Node.js 18+**
40+
41+
## Install
42+
43+
### Option A — install from npm (recommended)
44+
45+
```bash
46+
npm install -g mcp-retroarch
47+
```
48+
49+
### Option B — `npx` (no install)
50+
51+
```bash
52+
npx -y mcp-retroarch
53+
```
54+
55+
### Option C — clone and develop
56+
57+
```bash
58+
git clone https://github.com/dmang-dev/mcp-retroarch
59+
cd mcp-retroarch
60+
npm install
61+
```
62+
63+
## Enable RetroArch's Network Control Interface
64+
65+
Either:
66+
- **GUI:** Settings → Network → Network Commands → **ON**, then confirm `Network Cmd Port` is `55355` (the default)
67+
- **Or via `retroarch.cfg`:**
68+
```ini
69+
network_cmd_enable = "true"
70+
network_cmd_port = "55355"
71+
```
72+
73+
Then launch any libretro core + game. The NCI is always-on once enabled — no script to load.
74+
75+
## Register with your MCP client
76+
77+
### Claude Code
78+
79+
```bash
80+
claude mcp add retroarch --scope user mcp-retroarch
81+
```
82+
83+
Verify:
84+
```bash
85+
claude mcp list
86+
# retroarch: mcp-retroarch - ✓ Connected
87+
```
88+
89+
### Claude Desktop
90+
91+
Edit `claude_desktop_config.json`:
92+
93+
| Platform | Path |
94+
|---|---|
95+
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
96+
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
97+
| Linux | `~/.config/Claude/claude_desktop_config.json` |
98+
99+
```json
100+
{
101+
"mcpServers": {
102+
"retroarch": {
103+
"command": "mcp-retroarch"
104+
}
105+
}
106+
}
107+
```
108+
109+
Restart Claude Desktop after editing.
110+
111+
## Configuration
112+
113+
| Env var | Default | Purpose |
114+
|---------------------|---------------|---------|
115+
| `RETROARCH_HOST` | `127.0.0.1` | UDP destination host |
116+
| `RETROARCH_PORT` | `55355` | UDP port (must match `network_cmd_port` in `retroarch.cfg`) |
117+
118+
## Tools
119+
120+
| Tool | Description |
121+
|------|-------------|
122+
| `retroarch_ping` | Verify reachability — returns RetroArch version |
123+
| `retroarch_get_status` | State (playing/paused), system, game, CRC32 |
124+
| `retroarch_get_config` | Read named RetroArch config values (e.g. `savestate_directory`) |
125+
| `retroarch_read_memory` / `retroarch_write_memory` | Memory r/w via system memory map |
126+
| `retroarch_read_ram` / `retroarch_write_ram` | Memory r/w via CHEEVOS address space (fallback when no memory map) |
127+
| `retroarch_pause_toggle` | Toggle pause state |
128+
| `retroarch_frame_advance` | Step one frame (only effective while paused) |
129+
| `retroarch_reset` | Hardware-reset the running game |
130+
| `retroarch_screenshot` | Save a screenshot to RetroArch's screenshot directory |
131+
| `retroarch_show_message` | Display a notification on the RetroArch window |
132+
| `retroarch_save_state_current` | Save to currently-selected slot |
133+
| `retroarch_load_state_current` | Load from currently-selected slot |
134+
| `retroarch_load_state_slot` | Load from explicit slot number |
135+
| `retroarch_state_slot_plus` / `retroarch_state_slot_minus` | Change current slot pointer (NCI has no "set slot to N") |
136+
137+
See [`docs/RECIPES.md`](docs/RECIPES.md) for end-to-end examples.
138+
139+
## Troubleshooting
140+
141+
| Symptom | Cause / Fix |
142+
|---|---|
143+
| `RetroArch query timed out` | Network Commands aren't enabled in RetroArch, or the port doesn't match `RETROARCH_PORT`. Confirm `network_cmd_enable = "true"` in `retroarch.cfg`. |
144+
| `READ_CORE_MEMORY failed: no memory map defined` | The loaded libretro core doesn't advertise a system memory map. Try `retroarch_read_ram` (CHEEVOS path) — many cores expose CHEEVOS even without a memory map. |
145+
| `READ_CORE_MEMORY failed: no descriptor for address` | The address isn't covered by the core's memory map. Either a different core would expose it, or the address you want is outside the system bus (e.g. video memory in some cores). |
146+
| Screenshots don't appear where I expect | RetroArch saves to its configured screenshot directory. The NCI doesn't expose `screenshot_directory` via `GET_CONFIG_PARAM`, so check the value via RetroArch's GUI: Settings → Directory → Screenshot. |
147+
| Can't save to a specific state slot directly | NCI limitation, not a bug. The protocol only exposes "save to current slot" — you have to walk the slot pointer to your target with `state_slot_plus`/`state_slot_minus`, then save. |
148+
149+
## Development
150+
151+
```bash
152+
npm install
153+
npm run dev # tsc --watch
154+
```
155+
156+
Smoke test against a running RetroArch:
157+
```bash
158+
node .scratch/smoke.cjs
159+
```
160+
161+
## License
162+
163+
[MIT](LICENSE)
164+
165+
## Related
166+
167+
- [mcp-mgba](https://github.com/dmang-dev/mcp-mgba) — Game Boy Advance via mGBA's Lua bridge (includes button input + screenshot)
168+
- [mcp-pine](https://github.com/dmang-dev/mcp-pine) — PINE-speaking emulators (PCSX2 et al.) — memory + savestate only
169+
- [RetroArch NCI documentation](https://docs.libretro.com/development/retroarch/network-control-interface/)

0 commit comments

Comments
 (0)