Skip to main content
Glama
Leuconoe

ryubing-mcp

by Leuconoe
README.md
# ryubing-mcp

`ryubing-mcp` is a local Model Context Protocol (MCP) server for controlling the
[Ryubing](https://git.ryujinx.app/projects/Ryubing) Nintendo Switch emulator.
It lets an MCP client start isolated emulator sessions, launch a user-owned
game, send controller input, capture the rendered frame, and inspect logs.

> [!IMPORTANT]
> Use the [Ryubing MCP Bridge custom build](https://github.com/Leuconoe/Ryubing/releases/tag/mcp-bridge-v0.2.0)
> to use the complete MCP feature set. An unmodified Ryubing executable works
> through the Windows patchless compatibility backend, but analog-stick and
> touchscreen tools are intentionally unavailable there.

Only use game, update, firmware, and key files that you are legally entitled to
use. This project does not distribute any of them.

## What is included

- A stdio MCP server for local MCP clients.
- Managed Ryubing processes with one retained copied profile per title by default.
- A full control-bridge backend for the recommended custom Ryubing build.
- A Windows patchless fallback for an unmodified Ryubing executable.
- Optional game-path confinement.
- Cursor-based log reading and renderer screenshots.
- Persistent per-session manifests and NDJSON logs, including after a managed
  process exits.
- Installation/environment checks that do not expose key contents.
- Read-only launch and patch preflight checks.
- A versioned capability contract exposed as both an MCP tool and resource so
  clients can discover bridge/patchless limitations without guessing.
- One-call diagnostic reports with repeated-log and gameplay-failure hints,
  exportable bundles, and bounded frame sequences for visual-loop evidence.
- Read-only font-atlas review harness for canvas/alpha/pixel-format, glyph
  rectangles, clipping, metric drift, and translated-text glyph coverage.
- A repository-bundled `nsw-workspace-cleanup` skill that correlates Ryubing and
  Eden handoffs, manifests, sessions, and artifacts into a read-only cleanup plan.
- Automated tests and live smoke-test scripts.

## Choose a backend

| Capability | Custom build (`bridge`) | Unmodified build (`patchless`) |
| --- | --- | --- |
| Windows managed sessions | Yes | Yes |
| Status and current title | Yes | Yes, inferred from the process/window |
| Digital buttons | Players 1-8 and handheld | Player 1 or handheld keyboard bindings |
| Analog sticks | Yes | **Pending / unavailable** |
| Touch tap, hold, and swipe | Yes | **Pending / unavailable** |
| Screenshots | PNG and JPEG renderer frames | Native PNG through the configured hotkey |
| Logs | Structured in-memory bridge logs | Parsed per-session file logs |
| Base game and session update launch | Yes | Yes |
| Window focus/hotkey dependency | No | Yes |
| Recommended for complete MCP use | **Yes** | Compatibility fallback only |

`backend: "auto"` selects patchless managed sessions on Windows. Therefore,
installing the custom build alone is not enough: set `backend` to `bridge` in
the JSON config when full controller and touch support is required.

## Requirements

- Windows 10/11 for the patchless backend. The bridge backend follows the
  platform support of the custom Ryubing build.
- Node.js 20 or newer.
- A working Ryubing profile containing its `Config.json`, keys, firmware, and
  any other data required to boot your games.
- The [custom Ryubing MCP Bridge release](https://github.com/Leuconoe/Ryubing/releases/tag/mcp-bridge-v0.2.0)
  for the complete tool set.

## Installation

### Option A: portable release layout

1. Download and extract the custom Ryubing build linked above.
2. Download the matching `v0.5.0` MCP package from the GitHub releases page, or
   clone this repository and build it with `npm ci`, `npm run build`, and
   `npm pack`.
3. Extract the generated `.tgz`; its files are under the `package/` directory.
4. Place the MCP package contents beside `Ryujinx.exe` and install production
   dependencies with `npm install --omit=dev`.
5. Prepare the `portable/` profile directory before creating a session.
6. Change `backend` to `bridge` in `ryubing-mcp.config.json`.

The resulting directory is self-contained:

```text
ryubing-portable/
|-- Ryujinx.exe
|-- Ryujinx.dll and other Ryubing runtime files
|-- portable/                         # ready profile template copied once per title
|   |-- Config.json
|   `-- system/                       # prod.keys, title.keys, firmware/NAND data
|-- dist/
|   `-- index.js
|-- node_modules/
|-- package.json
|-- ryubing-mcp.config.json
|-- ryubing-mcp.config.schema.json
`-- mcp-sessions/                     # created automatically
```

Run this once inside the directory:

```powershell
npm install --omit=dev
node .\dist\index.js
```

The second command starts an MCP stdio process and normally appears to wait;
that is expected. MCP clients start and communicate with it automatically.

### Option B: build from source

```powershell
git clone https://github.com/Leuconoe/ryubing-mcp.git
Set-Location ryubing-mcp
npm install
npm run typecheck
npm test
npm run build
```

Either copy the custom Ryubing runtime/profile into this directory or keep them
elsewhere and point an external JSON config at those paths.

## MCP client registration

### Zero-environment portable registration

When `Ryujinx.exe`, `portable/`, and the default config are beside `dist/`, no
environment variables are required:

```json
{
  "mcpServers": {
    "ryubing": {
      "command": "node",
      "args": ["D:/Apps/ryubing-portable/dist/index.js"]
    }
  }
}
```

### Externally managed config

To keep runtime paths and settings outside the MCP package, inject only the
config file path:

```json
{
  "mcpServers": {
    "ryubing": {
      "command": "node",
      "args": ["D:/Apps/ryubing-mcp/dist/index.js"],
      "env": {
        "RYUBING_MCP_CONFIG": "D:/Config/ryubing-mcp.config.json"
      }
    }
  }
}
```

Use forward slashes or escaped backslashes in JSON paths. Restart the MCP client
after changing its server registration or the Ryubing config.

## Configuration file

The server loads configuration in this order:

1. The file named by `RYUBING_MCP_CONFIG`.
2. `ryubing-mcp.config.json` in the MCP process working directory.
3. `ryubing-mcp.config.json` shipped beside the server package.

Relative `portable.root` paths are resolved from the directory containing the
selected config. Its three child paths are then resolved from `portable.root`.
Unknown settings and invalid values fail at startup instead of being ignored.

Complete bridge-oriented example:

```json
{
  "$schema": "./ryubing-mcp.config.schema.json",
  "backend": "bridge",
  "portable": {
    "root": ".",
    "executable": "Ryujinx.exe",
    "profileTemplate": "portable",
    "sessionRoot": "mcp-sessions"
  },
  "bridge": {
    "host": "127.0.0.1",
    "port": 49600,
    "timeoutMs": 10000,
    "launchTimeoutMs": 120000
  },
  "managed": {
    "maxSessions": 4,
    "portStart": 49610,
    "startupTimeoutMs": 30000,
    "retention": {
      "keepLatest": 3,
      "olderThanDays": 7,
      "autoPrune": true
    }
  }
}
```

| JSON setting | Default | Description |
| --- | --- | --- |
| `backend` | `auto` | `bridge` for full custom-build control, `patchless` for the Windows fallback, or `auto` |
| `portable.root` | `.` | Base directory for the following three portable paths |
| `portable.executable` | `Ryujinx.exe` | Custom or unmodified Ryubing apphost; a `.dll` is also accepted by bridge sessions |
| `portable.profileTemplate` | `portable` | Ready default profile copied for each title's first managed session |
| `portable.sessionRoot` | `mcp-sessions` | Parent directory for isolated session profiles |
| `bridge.host` | `127.0.0.1` | External/default bridge host; loopback values only |
| `bridge.port` | `49600` | External/default bridge port |
| `bridge.timeoutMs` | `10000` | Timeout for each bridge request |
| `bridge.launchTimeoutMs` | `120000` | Longer timeout for `game.launch`, which may perform application discovery and decryption |
| `managed.maxSessions` | `4` | Concurrent managed-session limit; maximum 32 |
| `managed.portStart` | `49610` | First port considered for managed bridge sessions |
| `managed.startupTimeoutMs` | `30000` | Time allowed for a newly launched emulator to become ready |
| `managed.retention.keepLatest` | `3` | Number of newest inactive unbound/exception profiles to retain; canonical title profiles are protected |
| `managed.retention.olderThanDays` | `7` | Remove inactive profiles older than this many days; active processes are never removed |
| `managed.retention.autoPrune` | `true` | Run the safe inactive-profile cleanup before creating and after stopping a session; explicit cleanup remains available when disabled |
| `allowedGameDirs` | omitted | Optional array of permitted game/update roots; omitted means unrestricted |

To restrict launchable files, add an allowlist. Relative entries are resolved
from the config directory:

```json
{
  "allowedGameDirs": [
    "games",
    "E:/NSW/_titles/_waitng"
  ]
}
```

`allowedGameDirs` is not required. Leave it out when arbitrary absolute game
paths must be usable. A configured allowlist rejects files outside its roots.

### Optional environment overrides

Operational settings should normally remain in JSON. These variables exist for
legacy launchers, CI, and secret injection, and override JSON when present:

| Environment variable | Purpose |
| --- | --- |
| `RYUBING_MCP_CONFIG` | Select one external JSON config; normally the only injected variable |
| `RYUBING_BACKEND` | Override `backend` |
| `RYUBING_CONTROL_HOST` | Override external bridge host |
| `RYUBING_CONTROL_PORT` | Override external bridge port |
| `RYUBING_CONTROL_TOKEN` | Inject the external bridge bearer token without storing it in JSON |
| `RYUBING_CONTROL_TIMEOUT_MS` | Override bridge request timeout |
| `RYUBING_GAME_LAUNCH_TIMEOUT_MS` | Override the operation-specific `game.launch` timeout |
| `RYUBING_ALLOWED_GAME_DIRS` | Platform-delimited path allowlist override |
| `RYUBING_EXECUTABLE` | Override the managed executable path |
| `RYUBING_PROFILE_TEMPLATE` | Override the profile-template path |
| `RYUBING_SESSION_ROOT` | Override the session-root path |
| `RYUBING_SESSION_PORT_START` | Override the first managed bridge port |
| `RYUBING_MAX_SESSIONS` | Override the managed-session limit |
| `RYUBING_SESSION_STARTUP_TIMEOUT_MS` | Override managed startup timeout |

## Profile and session model

There are two kinds of session:

- `default` represents an emulator bridge started outside this MCP server at
  `bridge.host:bridge.port`. Omitting `sessionId` targets it.
- A managed session is created by `ryubing_create_session`. A title's first
  session copies the configured ready default profile, including `Config.json`,
  `system/` keys, and registered firmware/NAND. Later calls for that title reuse
  the running process or restart the retained profile instead of cloning again.
- Each managed profile contains `.ryubing-mcp-session.json` and
  `.ryubing-mcp/logs.ndjson`. The manifest records lifecycle phase, last status,
  launch paths, exit code, and the last error. These files are retained when a
  process exits so diagnostics can distinguish a game failure from an MCP or
  bridge connection failure.
- Base, update, and child-program IDs belonging to one application share one
  title-session owner. For example, `...6000`, `...6001`, and `...6005` all map
  to the `...6000` owner. Exact patch paths and runtime evidence must still use
  the actual child program ID selected by the emulator.

For normal automation, use managed sessions:

1. Call `ryubing_check_environment` and resolve every `error` before a runtime test.
2. Call `ryubing_validate_launch` with the base/update paths. Resolve every
   `error` before starting a title.
3. Call `ryubing_create_session` with the validated `applicationId`. Normally
   omit `sessionId`; the server selects `title-<applicationId>` and returns the
   existing title session when one is already active.
4. Call `ryubing_launch_game` with the returned `sessionId` and use
   `waitFor: "running"` when the caller needs a readiness guarantee.
   A timeout is returned as retryable evidence by default; continue with
   `ryubing_wait_for_state` instead of issuing a second launch.
5. Use input, screenshot, frame-sequence, and log tools with the same
   `sessionId`.
6. If the game does not progress, call `ryubing_get_diagnostics` and, when
   sharing evidence, `ryubing_export_diagnostics` before stopping.
7. Call `ryubing_stop_session` when finished and retain the profile for diagnosis.

Tool-level failures use a stable JSON envelope even when the MCP result has
`isError: true`:

```json
{
  "error": {
    "code": "SESSION_TITLE_CONFLICT",
    "message": "...",
    "retryable": false
  }
}
```

Callers should inspect `isError` and `error.code` before consuming the result.
Timeout/readiness errors are marked retryable when the existing session can be
polled safely. The managed launcher also removes MCP configuration overrides
from the child emulator environment and injects only the per-session control
token and launch-specific variables.

`ryubing_launch_game` requires an explicit `sessionId`, including
`sessionId: "default"` when intentionally using an externally started bridge.
This prevents a caller that created a managed session from accidentally sending
the next title to the unrelated default emulator. Each normal managed session
is bound to one normalized application ID, and only one canonical managed
session may own that ID. Base/update paths for the same application ID can
change inside that retained session; a different title is rejected with
`SESSION_TITLE_CONFLICT`. Concurrent launches for one session are rejected with
`SESSION_LAUNCH_IN_PROGRESS`, so a timed-out request must be observed with
`ryubing_wait_for_state` rather than retried. Until a wait reaches a requested
state, another launch is rejected with `SESSION_LAUNCH_OUTCOME_UNKNOWN`, even
when it targets the same title.

The ready template is copied only for a title's first session, so the original
default emulator profile is never rewritten. A stopped canonical title profile
is automatically protected from retention and restarted in place; its copied
keys, firmware, saves, title keys, and settings therefore remain available.
Use `removeProfile: true` only when deliberately discarding that title profile.
Unbound sessions and reason-authorized parallel comparison sessions remain
subject to normal inactive retention so exceptions do not become another
unbounded storage source. Active processes and unrecognized directories are
never removed.

Patchless sessions also create a hard-linked runtime mirror, falling back to
normal copies when hard links are unavailable. This gives every process an
isolated `Logs` directory without normally duplicating the entire runtime.
Native PNG files used for patchless screenshots are temporary MCP evidence and
are pruned before and after each capture; returned image data remains available
to the MCP caller without building an unbounded `screenshots/` archive.

## MCP tool reference

Most emulator-control tools accept optional `sessionId`; omit it only when you
intentionally target an externally started default bridge. `ryubing_launch_game`
is the exception and requires an explicit ID to prevent title/session mix-ups.

### `ryubing_create_session`

Starts a managed process and returns its session metadata and initial status.
For normal use, provide the 16-digit base `applicationId` and omit `sessionId`;
the stable ID is `title-<applicationId>`. Repeating the call returns the running
canonical session (`reusedExisting=true`) or restarts its retained profile
(`reusedProfile=true`). It does not recopy the template or require keys and
firmware to be injected again. `reuseExisting` remains only for legacy unbound
profiles.

Creating an unbound session or a deliberate second session for the same title
requires `exceptionReason` (10-500 characters), for example a simultaneous
baseline/patched comparison. The reason is persisted in the owned manifest.
Without it, duplicate creation fails with `TITLE_SESSION_EXISTS` and identifies
the session to reuse.

### `ryubing_list_sessions`

Lists the external `default` entry plus all managed sessions, including backend,
state, PID, port where applicable, and profile directory.

### `ryubing_stop_session`

Stops one managed process. `sessionId` is required. `removeProfile` defaults to
`false`; setting it to `true` recursively removes that isolated profile after
the process exits. Canonical title profiles are protected automatically so they
can be restarted without key/firmware setup. `protectProfile: true` remains
useful for unbound or exception sessions whose evidence must survive automatic
retention. The external `default` session cannot be stopped here.

### `ryubing_get_session_storage`

Returns a read-only inventory of directories directly under
`portable.sessionRoot`, including ownership classification (`active`,
`reclaimable`, `protected`, `legacy`, or `unconfirmed`) and byte estimates.
Owned entries also expose `applicationId` and any `duplicateTitleReason`, making
pre-existing same-title profiles identifiable before explicit removal.
Directories without a matching MCP manifest are reported but never removed by
automatic cleanup.

### `ryubing_cleanup_sessions`

Lists or removes inactive managed profiles under `portable.sessionRoot`.
`dryRun` defaults to `true`; set it to `false` only after reviewing the
matched session IDs, paths, and byte estimate. The default policy keeps the
newest three inactive profiles and removes profiles beyond that count or older
than seven days. Canonical title profiles are excluded; remove one explicitly
with `ryubing_stop_session(removeProfile=true)` when it is no longer needed. The
cleanup tool only accepts MCP manifests directly under the configured session
root, skips active PIDs, and never scans games, saves, keys, firmware, patches,
or arbitrary directories. It still works when `managed.retention.autoPrune` is
disabled.

### Ryubing/Eden workspace cleanup skill

The repository includes
[`skills/nsw-workspace-cleanup`](skills/nsw-workspace-cleanup/SKILL.md) for
shared title workspaces used by Ryubing MCP and Eden MCP. Cleanup is inferred
from the complete handoff chain, exact paths and hashes, title/project IDs,
ownership manifests, active PIDs, and pending review state. Names such as
`tmp`, `copy`, `old`, or a timestamp and duplicate hashes are never deletion
authority by themselves.

The bundled collector is read-only and rejects `--apply`/`--delete`. Supply the
title or repository roots, both emulators' relevant handoffs, and their session
roots when applicable:

```powershell
node .\skills\nsw-workspace-cleanup\scripts\build-cleanup-context.mjs `
  --root "E:\NSW\_titles\Title [0100000000000000]" `
  --handoff "D:\workspace\____personal____\ryubing-mcp\HANDOFF.md" `
  --handoff "D:\workspace\____personal____\eden-mcp\HANDOFF.md" `
  --session-root "ryubing=E:\NSW\_tools\Ryubing\mcp-bridge-v0.1.0\sessions" `
  --session-root "eden=E:\NSW\_tools\Eden\mcp-v0.4.0\sessions" `
  --json
```

Use the resulting snapshot to generate `cleanup-plan.v1` with exact `KEEP`,
`REVIEW`, and `ELIGIBLE_AFTER_APPROVAL` records. A failed candidate can still be
the only evidence for an open diagnosis; an artifact referenced by either
emulator remains protected. If the inventory is truncated, a handoff is missing,
or title/session ownership conflicts, the plan must be `BLOCKED`. Actual cleanup
requires a separate user approval of exact instruction IDs and a fresh matching
snapshot. Prefer each emulator's MCP retention tools for owned sessions and
diagnostic bundles.

### `ryubing_get_status`

Returns protocol/emulator version, process ID, state, title ID/name, and frame
dimensions where available. States are `idle`, `loading`, `running`, `paused`,
or `stopping`.

### `ryubing_get_capabilities`

Returns the `emulator-mcp-capabilities` contract version `1.0` for the selected
session. It explicitly reports bridge-only analog/touch/multi-touch support,
available launch/capture/diagnostic operations, and current limitations. The
same default-session contract is available through the
`emulator://ryubing/capabilities` MCP resource for clients that discover
capabilities before selecting a tool.

### `ryubing_check_environment`

Checks the managed executable, profile template, `Config.json`, standard
`system/prod.keys` and `system/title.keys` locations, registered firmware,
allowlisted game roots, session-root write access, and the selected session.
The result has `overall: "ok"`, `"warning"`, or `"error"` plus a `checks` array;
it does not read or return key contents. Set `probeControl: true` to query the
selected bridge status as part of the report. This is the first tool to call
when setup or firmware/key installation is uncertain.

Ryubing uses `profile/system/` for keys with `--root-data-dir`. A legacy
`profile/keys/` directory is reported as a warning and is not treated as a
successful standard key installation.

### `ryubing_press_buttons`

Presses one or more buttons simultaneously and releases them after `durationMs`
(16-10000, default 100). `player` is 1-8 or `handheld`.

Supported button names:

```text
A B X Y L R ZL ZR PLUS MINUS L_STICK R_STICK
DPAD_UP DPAD_DOWN DPAD_LEFT DPAD_RIGHT
SL_LEFT SR_LEFT SL_RIGHT SR_RIGHT
```

Patchless mode accepts only Player 1/handheld and maps these names through the
`WindowKeyboard` bindings in the copied profile's `Config.json`.

### `ryubing_set_sticks`

Sets any supplied `leftX`, `leftY`, `rightX`, or `rightY` coordinate in the
inclusive range -1 to 1. At least one axis is required. `durationMs: 0` retains
the state; a positive duration up to 60000 resets both sticks afterward.
Requires the custom bridge build.

### `ryubing_touch`

Sends `tap`, `long_press`, or `swipe` using Switch logical-screen coordinates:
`x` 0-1279 and `y` 0-719. A swipe also requires `endX` and `endY`.
`durationMs` is 16-10000. Requires the custom bridge build.

### `ryubing_take_screenshot`

Returns metadata plus MCP image content. Bridge mode accepts `png` or `jpeg`
and JPEG quality 1-100. Patchless mode accepts PNG only, invokes Ryubing's
screenshot hotkey, and waits for the renderer PNG in the session profile.
Before bridge capture, the MCP checks `control.status` and requires non-zero
`frameWidth`/`frameHeight`; an early request returns retryable
`SCREENSHOT_NOT_READY` without calling the renderer capture path.

### `ryubing_get_logs`

Reads up to 2000 entries (`limit`, default 200). Levels are `trace`, `debug`,
`info`, `notice`, `warning`, `error`, and `critical`. Pass the returned
`nextCursor` as the next call's `cursor` to receive newer entries only. The
optional `contains` value performs a case-insensitive match against each entry's
category and message and returns filter metadata while preserving the source
cursor.

### `ryubing_get_diagnostics`

Collects a shareable JSON report without stopping or restarting the emulator.
It includes a `reportVersion`, capture time, selected session metadata, runtime
backend/platform, status, the requested trace-to-error log window,
cursor/truncation state, level counts, repeated-message first/last cursors,
and up to 100 warning/error/negative-log events with surrounding context. It
still returns logs when status collection fails; the `errors` object identifies
which part failed.

The `focus` argument prioritizes one of the common failure patterns:

| `focus` | What it looks for |
| --- | --- |
| `game_load_failure` | stuck `loading`/`stopping` state and boot/load/guest errors |
| `patch_loop` | repeated patch/mod/LayeredFS/RomFS/ExeFS/retry messages |
| `video_loop` | repeated video/movie/cutscene/NVDEC/codec/decoder messages |
| `patch_not_applied` | explicit missing, skipped, disabled, invalid, or failed patch messages |
| `all` | all four heuristic categories (default) |

Hints are evidence-based heuristics, not a proof of the root cause. Each hint
contains `summary`, bounded `evidence`, and `recommendations`; a focused call
with no matching signature returns a low-confidence “inconclusive” hint rather
than an empty result. Always pass the original `logs.entries`,
`observations.events`, and `observations.repeatedMessages` to the report when
escalating an issue. A useful capture is:

```json
{
  "sessionId": "qa-01",
  "cursor": "0",
  "limit": 2000,
  "minimumLevel": "trace",
  "focus": "all"
}
```

Capture a fresh diagnostic after the failure, before launching another title or
deleting the managed profile; the retained profile and cursor make repeated
failures comparable.

When the managed process has already exited, the tool falls back to the
manifest's last status and the persisted NDJSON log. The report exposes
`sources.status` (`live` or `manifest`) and `sources.logs` (`live` or
`persisted`) and adds a warning instead of hiding the original failure behind
`ECONNREFUSED`.

### `ryubing_export_diagnostics`

Collects the same report and writes `report.json`, the captured `logs.ndjson`,
and a SHA-256 `manifest.json` under the selected managed profile's
`.ryubing-mcp/diagnostics/<timestamp>-<pid>/` directory. The result includes
the exact paths, byte count, and redaction count. Absolute paths and
secret-like fields or inline values are redacted by default. Export is
local-only; game contents, screenshots, keys, firmware, and environment
variables are not copied. Only set `redact: false` when the bundle remains on
the same trusted machine.

### `ryubing_get_runtime_metrics` and `ryubing_advise_compatibility`

The metrics tool summarizes bounded FPS and frame-time samples plus shader and
video-failure signals found in structured logs. Missing counters remain `null`
or empty instead of being estimated. The advisor combines those metrics with
diagnostic hints and returns reversible, one-variable experiments with their
reason, tradeoff, and rollback. It never changes emulator settings.

### `ryubing_list_artifacts` and `ryubing_cleanup_artifacts`

Artifact listing is limited to Ryubing MCP's exact diagnostic-directory naming
convention. Cleanup applies age and keep-latest retention to the same bounded
set and defaults to `dryRun: true`. It does not scan or delete games, managed
profiles, saves, patches, keys, firmware, or arbitrary user files.

### `ryubing_capture_sequence`

Captures 2-12 frames at a bounded interval (50-10000 ms). The metadata includes
SHA-256 hashes, dimensions, optional bridge frame numbers, byte sizes, and
`changedFromPrevious`. It classifies exact-byte sequences as `frozen`,
`periodic`, or `moving` and reports the period and compared pairs. Image blocks
are included only while the total payload is under 24 MiB. Exact PNG/JPEG hashes
are strong evidence for identical output but less tolerant than Eden MCP's
grayscale perceptual comparison; compression differences can make equivalent
frames appear changed.

### `ryubing_run_input_script`

Validates all operations before sending the first input, then executes up to 100
`wait`, `buttons`, `sticks`, `touch`, or `wait_frame_change` steps. The
declared wait/hold/timeout budget is limited to five minutes. Each result contains
the operation, duration, success, and result or failure. Stick players touched by
the script are reset to zero after failure and by default after completion.

```json
{
  "sessionId": "qa-01",
  "steps": [
    {"op":"buttons","buttons":["L","R"],"player":1,"durationMs":120},
    {"op":"buttons","buttons":["A"],"player":1,"durationMs":80},
    {"op":"wait_frame_change","timeoutMs":10000,"pollMs":500}
  ]
}
```

Patchless sessions support `wait`, `buttons`, and screenshot-based frame
checkpoints. Stick and touch steps return the existing explicit `Pending` error
and the script preserves completed-step evidence.

### `ryubing_validate_launch`

Performs the launch checks without starting Ryubing: path allowlist and
extension, file/directory metadata, filename title-ID hints, and base/update
compatibility. `overall: "error"` blocks `ryubing_launch_game`; warnings are
non-fatal but should be reviewed.

### `ryubing_inspect_patch`

Scans an unpacked patch directory without modifying it. It reports title-ID
hints, `romfs`/`exefs`/LayeredFS markers, patch archive extensions, duplicate
relative paths, file count, and total bytes. This is a layout check only; it
does not decrypt NCA files or prove that a translation rendered in-game.

For the current Ryubing profile layout, install mods under
`mods/contents/<program-id>/<mod-name>/romfs|exefs`, or Atmosphere-compatible
content under `sdcard/atmosphere/contents/<program-id>/`. A legacy `load/`
directory is not searched by this build. In a multi-program title,
`<program-id>` is the exact child ID that consumes the patch, not necessarily
the base application ID used for session ownership.

### `ryubing_validate_font_atlas`

Runs a read-only before/after check for a translated PNG font atlas. Supply the
original atlas, candidate atlas, a JSON glyph map, and the translated text
sample used for required glyph coverage. The map can be a single
`glyphs` array, separate `reference`/`candidate` arrays, or section objects with
`glyphs` and `kernings`. Each glyph identifies one Unicode code point and its
`x`, `y`, `width`, `height`, optional `advance`, `baseline`, and signed
`offsetX`/`offsetY` (`xOffset`/`yOffset` aliases are accepted). Kerning records
identify `first`, `second`, and signed `amount` values.

The harness fails on canvas or pixel-format changes, missing alpha, duplicate or
overlapping rectangles, out-of-bounds glyphs, empty/clipped candidate glyphs,
position/advance/baseline/offset/kerning drift, suspicious ink-size or occupancy
changes, and missing glyphs in the supplied translated text sample. PNG and map
sizes, decompression output, glyph count, rectangle work, and overlap comparisons
are bounded to keep malformed inputs from exhausting the MCP process.

A clean first run is `PENDING_USER_REVIEW` and returns `approval.reviewToken`.
That token binds the reference PNG, candidate PNG, glyph-map JSON, translated
text, and tolerances. After the user approves the exact before/after evidence,
rerun with `approvedReviewToken` set to that token. Any artifact or parameter
change invalidates it with `APPROVAL_TOKEN_MISMATCH`; a boolean approval flag is
not accepted. This static PASS is still not a runtime or release PASS.

Example glyph map:

```json
{
  "reference": [{"codePoint":"U+AC00","x":2,"y":2,"width":32,"height":40,"advance":32,"baseline":30,"offsetY":-2}],
  "candidate": [{"codePoint":"U+AC00","x":2,"y":2,"width":32,"height":40,"advance":32,"baseline":30,"offsetY":-2}],
  "referenceKernings": [{"first":"U+AC00","second":"U+B098","amount":-1}],
  "candidateKernings": [{"first":"U+AC00","second":"U+B098","amount":-1}]
}
```

Keep failed candidates and the returned SHA-256 values in the title-local QA
evidence. Fix the atlas in a new temporary staging path, rerun the harness, and
request user review again; never overwrite the only reference/candidate pair.

### `ryubing_launch_game`

`sessionId` is required. Use the returned ID from `ryubing_create_session`, or
use the literal `default` only when the external bridge is the intended target.
The server checks the bridge state before launching, serializes launches per
session, and verifies any reported title ID against the requested application.
Once a session has a launch identity, switching to another title is blocked.
A different base/update path with the same
normalized application ID stays in that title's existing session.

Launches an absolute `.xci`, `.nsp`, `.nca`, `.nro`, or unpacked game directory.
Optional `updatePath` must be a matching update `.nsp`. The update applies only
to the isolated session. Supply the 16-digit hexadecimal `applicationId` when
the update filename does not expose a title ID.

For a user-owned encrypted dump that requires an external title key, either call
`ryubing_inject_title_key` first or pass the same object as `titleKey` on
`ryubing_launch_game`:

```json
{
  "sessionId": "qa-01",
  "basePath": "E:/Games/Example.nsp",
  "titleKey": {
    "rightsId": "00112233445566778899AABBCCDDEEFF",
    "key": "FFEEDDCCBBAA99887766554433221100"
  }
}
```

Both values must be exactly 32 hexadecimal characters. Injection is limited to
an idle isolated managed session and atomically merges into that session's
`system/title.keys`; the external `default` profile and profile template are
never changed. The key itself is not returned. Repeating the same pair is a
no-op. A different key for an existing Rights ID fails with
`TITLE_KEY_CONFLICT` unless `overwrite: true` is included. Ryubing reloads the
session key set as the title is initialized, so launch-time injection does not
require restarting the managed process.

`waitFor` defaults to `"accepted"` for compatibility. Set it to `"running"` to
poll the selected bridge until `control.status.state` is `running`; the bounded
`waitTimeoutMs` (default `managed.startupTimeoutMs`) returns
`targetReached=false`, `timedOut=true`, and `retryable=true` without stopping
the selected process. Set `keepRunningOnTimeout=false` only when legacy tool
error behavior is required. `bridge.launchTimeoutMs` is a separate, longer
timeout for the launch RPC itself.

If that RPC deadline expires before the bridge acknowledges the launch, the
default result is also non-terminal: `launch.accepted` is `"unknown"`,
`launch.requestTimedOut=true`, and `retryable=true`. This does not prove that the
emulator rejected the game. Keep the owned process, poll
`ryubing_wait_for_state`, and collect diagnostics from that session. Do not
issue a second `game.launch` while the first request's outcome is unknown;
`SESSION_LAUNCH_OUTCOME_UNKNOWN` remains active until a state wait reaches its
target or the session is stopped/recreated.

### `ryubing_wait_for_state`

Continues polling the existing selected session for `idle`, `loading`,
`running`, `paused`, or `stopping` without sending another `game.launch`. Use it
after a retryable launch timeout. The response includes the last status/error,
elapsed time, and whether another wait remains safe.

Example call arguments:

```json
{
  "sessionId": "qa-01",
  "basePath": "E:/NSW/_titles/_waitng/Example [0100000000000000].xci",
  "updatePath": "E:/NSW/_titles/_waitng/Example Update [0100000000000800].nsp",
  "applicationId": "0100000000000000"
}
```

## Full bridge security model

- The bridge binds only to `127.0.0.1`, `::1`, or `localhost`; remote network
  control is deliberately unsupported.
- Managed bridge sessions choose separate ports and generate a cryptographically
  random token for each process.
- On Windows the token is passed through a per-session token file rather than a
  visible command-line argument.
- The MCP server validates protocol version `0.1`, input ranges, paths, response
  IDs, screenshot encoding, and screenshot size.
- Keep MCP access local and do not expose stdio or bridge ports through an
  unauthenticated network proxy.

## Patchless behavior and limitations

Patchless mode is a compatibility implementation, not the complete control
protocol. It:

- Detects PID, emulator version, title name, and title ID from the process and
  Ryubing window title.
- Delivers configured digital keyboard keys directly to the Ryubing window.
- Uses Ryubing's configured screenshot hotkey and returns the new PNG.
- Parses the latest isolated runtime log using a numeric cursor.
- Writes a session-local `games/<titleId>/updates.json` when an update is used,
  then restarts Ryubing with the base game path.
- Returns a clear `Pending` error for analog-stick and touch requests.

Because it relies on native window/input behavior, keyboard bindings must exist
and security or overlay software may interfere. Switch to `backend: "bridge"`
before treating input automation as release-quality evidence.

## Troubleshooting

### First response for any abnormal gameplay

Do not immediately restart, delete the profile, or apply another patch. Preserve
the failing session so its cursor and profile remain useful:

1. Call `ryubing_check_environment({"sessionId":"qa-01","probeControl":true})`.
2. Call `ryubing_get_status({"sessionId":"qa-01"})`.
3. Call `ryubing_get_diagnostics` with `cursor: "0"`, `limit: 2000`,
   `minimumLevel: "trace"`, and the most relevant `focus` value below.
4. Save the complete JSON response, emulator build, firmware version, title ID,
   base/update paths, session ID, and the fresh screenshot separately.
5. Only then restart the title or try one controlled change.

The diagnostic `hints` are heuristics. A loader message, a running process, or a
successful patch copy alone does not prove that the game reached the expected
screen or that Korean data was rendered.

### Game does not load or remains on loading

Run `ryubing_validate_launch` first and resolve any title-ID mismatch. Then use
`focus: "game_load_failure"`. Check the environment report first: the
executable, profile, `system/prod.keys`, `system/title.keys`, and registered
firmware must be present. Confirm the base path exists and that an update is
matched to the base title. If the update filename does not contain the base
title ID, pass `applicationId` explicitly. Compare `status.state`, `titleId`,
and the last load/guest/error entries in the diagnostic report.

### Wrong patch causes a loop or prevents progression

Run `ryubing_inspect_patch` on each candidate and keep its output with the
report. Then use `focus: "patch_loop"`. Inspect `observations.repeatedMessages` and the
`patch_loop` evidence rather than only the final error. Keep exactly one enabled
patch candidate for the selected title root, verify the consumer title ID, and
restart the title after changing patch files. A retained managed profile should
be used for the before/after comparison; do not mix a default profile log with a
managed-session result.

### Video or cutscene repeats instead of progressing

Use `ryubing_capture_sequence` first, then `focus: "video_loop"` and the complete
diagnostic log window. Look for repeated NVDEC, codec, decoder, movie, cutscene,
media, or frame messages. Separate a video decoder failure from a patch loop:
the process can remain `running` while the render surface is not advancing.
Record the title ID, firmware, graphics backend, and whether the same scene
repeats after a clean unpatched launch.

### Patch exists but Korean data is not displayed

Use `focus: "patch_not_applied"`. Verify that the patch is staged under the
actual consumer title ID, that only the intended candidate is enabled, and that
the managed profile is the profile being launched. Restart the title after
staging; LayeredFS/ExeFS or file-copy messages are not proof that the selected
translation reached the rendered text. Compare a baseline capture with a fresh
patched capture and retain both diagnostic reports.

### Keys or firmware were installed manually but the game still fails

For a `--root-data-dir` profile, Ryubing reads key files from
`<profile>/system/`, not `<profile>/keys/`. Use the environment report to detect
the legacy location. Supply a real firmware ZIP containing NCA entries; an
incomplete archive or an unrelated executable ZIP will fail before installation.
New titles receive the corrected ready template automatically. An existing
canonical title session intentionally retains its own working copy to avoid
repeated key/firmware setup; correct that profile in place, or explicitly stop
it with `removeProfile: true` and create it again with the same `applicationId`
when a full reset is intended.

### `Ryubing MCP config file not found`

Check `RYUBING_MCP_CONFIG`, use an absolute path, and remember that JSON paths
need forward slashes or escaped backslashes.

### Managed executable or profile does not exist

Confirm `portable.root`, `portable.executable`, and
`portable.profileTemplate`. The executable must be a file and the template must
already be a usable directory. `sessionRoot` is created automatically.

### Bridge startup/readiness timeout

Verify that the executable is the custom bridge release and that config uses
`backend: "bridge"`. Increase `managed.startupTimeoutMs` for slow firmware/game
startup. The managed session searches available loopback ports beginning at
`managed.portStart`.

### `Pending: analog stick` or `Pending: touchscreen`

The session is using patchless mode. Install the custom build and explicitly set
`backend` to `bridge`.

### Patchless button is unbound or unsupported

Open the template profile in Ryubing and configure Player 1 `WindowKeyboard`
bindings so new titles inherit them. For an existing canonical title session,
either update its copied `Config.json` or explicitly remove and recreate that
title profile.

### Patchless screenshot times out

Confirm the screenshot hotkey in `Config.json` is bound and that Ryubing can
write PNG files under the session profile's `screenshots/` directory.

### Game path is rejected

Use an absolute supported path. If `allowedGameDirs` is present, the real file
must be inside one of those directories. Remove the setting to allow all paths.

### Update title ID cannot be inferred

Pass the base game's 16-digit `applicationId` explicitly. Do not pass the update
title ID ending in the update suffix.

### Old session directory already exists

Call `ryubing_create_session` with the same `applicationId`; a canonical title
profile is restarted in place automatically. `reuseExisting: true` is only for
legacy unbound profiles. If an intentional reset is required, remove the
previous session through `ryubing_stop_session` with `removeProfile: true` and
then create it again. Directories without a valid MCP manifest are never
deleted automatically.

### Emulator exits without an error log

When a managed process exits with `exitCode: null` and the persisted log has no
`error` or `critical` entry, diagnostics label the result as an inconclusive
runtime anomaly. A briefly reported child program, connection refusal after
exit, or absence of patch-loader records does not prove that the title or patch
failed. Preserve the session, collect diagnostics, and require an exact child
program plus `PatchExeFS`/`PatchRomFS` evidence before assigning a patch verdict.

## Verification and development

Run the local quality checks:

```powershell
npm run typecheck
npm test
npm run build
```

With an external bridge already running at the configured default endpoint:

```powershell
npm run smoke:live
```

For two managed patchless processes, configure the managed paths and set
`RYUBING_LIVE_BASE_A`, `RYUBING_LIVE_UPDATE_A`, and `RYUBING_LIVE_BASE_B` before
running:

```powershell
npm run smoke:parallel
```

The smoke tests require user-owned games and are not part of the normal unit
test run.

For a read-only preflight matrix over a directory such as
`E:/NSW/_titles/_waitng`, set the directory outside the MCP process config and
run:

```powershell
$env:RYUBING_GAME_MATRIX_DIR = 'E:\NSW\_titles\_waitng'
npm run qa:matrix
```

The matrix reports one `ryubing_validate_launch` result per `.xci`, `.nsp`,
`.nca`, or `.nro` file. It does not launch games or copy/modify their files;
use it before a controlled runtime matrix with one retained managed profile per
title.

## Releases and source

- MCP server releases: <https://github.com/Leuconoe/ryubing-mcp/releases>
- Custom Ryubing source fork: <https://github.com/Leuconoe/Ryubing>
- Custom Windows build: <https://github.com/Leuconoe/Ryubing/releases/tag/mcp-bridge-v0.2.0>

## License

This MCP server is licensed under the [MIT License](LICENSE). Ryubing and its
dependencies retain their respective licenses.

TDQS

A4/5.0

Scored across 10 tools

Disambiguation5/5

Each tool targets a distinct resource or action: session management, status, game launch, input methods, screenshot, and logs. There is no overlap, and descriptions clearly distinguish between session status and active game status.

Naming Consistency4/5

All tools use the 'ryubing_' prefix and mostly follow a verb_noun pattern (e.g., press_buttons, set_sticks, launch_game, list_sessions). The only deviation is ryubing_touch, which is a single verb, but it remains clear and does not cause confusion.

Tool Count5/5

Ten tools is a well-scoped size for an emulator control server, covering session lifecycle, input, diagnostics, and game launching without redundancy. Each tool earns its place.

Completeness4/5

The tool set covers the core workflow: create session, launch game, control input, take screenshots, read logs, and stop session. Missing advanced features like save states or configuration settings are minor gaps for the apparent purpose.

Maintenance

ActivitySlowing
ResponsivenessNo issues