Skip to main content
Glama
README.md
# ro-mcp

An MCP server that drives a real Ragnarok Online client session against a
local [rAthena](https://github.com/rathena/rathena) server, so an agent can
log in, walk around, talk to NPCs and fight — and therefore *test* server
content end to end instead of reading log files.

The client is [OpenKore](https://github.com/OpenKore/openkore), driven through
a small control plugin (`openkore-plugin/mcpbridge.pl`) that exposes a
line-oriented JSON socket.

```
  Claude ⇄ MCP server (stdio)
         ⇄ JSON over TCP :24390
         OpenKore + mcpbridge plugin
         ⇄ RO protocol (plaintext)
         rAthena  login:6900  char:6121  map:5121
```

## Why OpenKore rather than a hand-rolled client

OpenKore ships an exact match for the packet version this server speaks —
`RagexeRE_2021_11_03` exists in its Send and Receive trees plus a matching
`recvpackets` table — and it already handles pathfinding, reconnects, map
changes and inventory. Reimplementing that was the alternative; reusing it
was not close.

Packet obfuscation is a non-issue here: rAthena resolves `PACKETVER > 20180307`
to zero obfuscation keys, which makes the XOR an identity operation. The
server confirms this at boot with
`Packet Obfuscation: Enabled. Keys: 0x00000000, 0x00000000, 0x00000000`.

## Requirements

| Requirement | Notes |
|---|---|
| Perl 5.34+ | macOS system Perl is fine |
| GNU readline | `brew install readline` — OpenKore's XSTools links against it |
| Xcode Command Line Tools | Perl's C headers live inside the SDK on modern macOS |
| Python 3 | only to run OpenKore's bundled SCons |
| Node.js 18+ | the MCP server itself |

## Setup

```bash
./setup.sh          # clone OpenKore, patch, build XSTools, install config + plugin
```

`setup.sh` is idempotent. It does four things the stock OpenKore build cannot
do unattended on Apple Silicon:

1. Points SConstruct at the `/opt/homebrew` readline prefix (it only knows
   the Intel `/usr/local` one).
2. Points it at Perl's headers inside the macOS SDK, since `Config.pm` reports
   a bare `/System/Library` path that no longer holds them.
3. Shims `python` → `python3` for the bundled SCons.
4. Installs our server entry, config overrides and the `mcpbridge` plugin —
   including adding `mcpbridge` to `loadPlugins_list`, without which the
   control socket never opens.

## Server-side prerequisites

The rAthena instance needs three settings for unattended login:

| File | Setting | Why |
|---|---|---|
| `conf/login_athena.conf` | `new_account: yes` | lets `name_M` auto-create an account on first login |
| `conf/char_athena.conf` | `pincode_enabled: no` | the PIN prompt blocks OpenKore's main loop, which stalls the control socket |
| `conf/char_athena.conf` | `char_new: yes` | character creation |

## The bridge protocol

Newline-delimited JSON, both directions, on `127.0.0.1:24390` (loopback only —
this grants full control of a game session).

```jsonc
// requests
{"id": 1, "op": "ping"}
{"id": 2, "op": "state"}
{"id": 3, "op": "run", "cmd": "move 155 180"}

// responses
{"id": 1, "ok": true, "pong": 1}
{"id": 3, "ok": false, "error": "unknown or rejected command: ..."}

// events, pushed as they happen
{"event": "npc_talk", "name": "TS Lab", "msg": "Welcome to the V8 smoke test."}
{"event": "npc_talk_responses", "responses": ["Mob spawn", "Reputation", "Cancel"]}
{"event": "map_changed", "map": "prontera"}
```

`op: run` forwards to OpenKore's own command layer rather than reimplementing
movement or NPC logic — OpenKore already knows how to path, retry and recover,
and duplicating that here would only rot.

## Usage

```bash
npm install && npm run build
node scripts/smoke.mjs      # end-to-end check against a running server
```

Register with an MCP client by pointing it at `dist/index.js`:

```jsonc
{ "mcpServers": { "ro": { "command": "node", "args": ["/path/to/ro-mcp/dist/index.js"] } } }
```

`RO_MCP_KORE_DIR` and `RO_MCP_PORT` override the OpenKore location and bridge port.

## Tools

| Tool | Purpose |
|---|---|
| `ro_start` / `ro_stop` | launch / shut down the session |
| `ro_state` | map, position, HP/SP, level, zeny, nearby NPCs and monsters |
| `ro_prompt` / `ro_answer` | inspect and answer OpenKore's interactive questions |
| `ro_create_char` / `ro_select_char` | character creation and selection |
| `ro_walk` / `ro_warp` | move within a map / travel to another map |
| `ro_talk_npc` | talk to an NPC, returning its dialog text and menu options |
| `ro_menu_select` / `ro_dialog_next` / `ro_close_dialog` | drive a conversation |
| `ro_attack` | attack a nearby monster |
| `ro_console` / `ro_command` | read console output / escape hatch |

### A note on prompts

OpenKore asks some questions on **stdin** — character creation, character
selection, password retry. Those are blocking reads inside its main loop,
so while one is pending the loop is stopped and the control socket cannot
answer. The MCP server therefore owns the child process's stdin as well as
the socket; `ro_prompt` surfaces a pending question and `ro_answer` replies
to it. If a tool ever times out, check `ro_prompt` first.

## Status

Verified end to end against a live server (`scripts/smoke.mjs`):

- account auto-creation, login, character creation and selection
- `ro_state` reporting map, position, HP/SP, level, zeny and nearby actors
- `ro_walk` moving the character and confirming arrival
- unreachable destinations rejected in ~30ms with the reason, rather than
  burning the full timeout — OpenKore reports these only on the console,
  so the arrival poll is raced against that message

- a full NPC conversation against TypeScript-scripted content
  (`scripts/npc-test.mjs`): reading dialog, parsing menu options, choosing
  a branch and reading the reply

Not yet exercised: `ro_attack`.

### Two async traps worth knowing

**Actor lists populate after map entry.** A `ro_state` taken immediately
after login legitimately reports no nearby NPCs; poll until they appear.

**A dialog's last message arrives as it closes.** Scripts commonly answer
and then close in one go, so treat `npc_talk_done` as "mark closed", never
as "discard the buffer" — otherwise the end of a conversation, usually the
part being asserted on, is exactly the part lost.

TDQS

A3.5/5.0

Scored across 17 tools

Disambiguation4/5

Most tools have clearly distinct purposes (start/stop/state/console, character creation/selection, movement, dialog steps, attack). Minor overlap exists between movement tools (ro_walk vs ro_warp vs ro_gm_warp) and dialog tools (ro_talk_npc, ro_menu_select, ro_dialog_next, ro_close_dialog), but the descriptions clarify when each is appropriate.

Naming Consistency5/5

All tools follow the `ro_` prefix with a consistent snake_case verb_noun pattern (e.g., ro_create_char, ro_menu_select, ro_gm_warp). Variations like ro_close_dialog or ro_dialog_next still adhere to the same convention, making the naming predictable and easy to infer.

Tool Count4/5

At 17 tools, the server is on the higher end but appropriate for a game automation bot that needs to cover startup, character management, movement, NPC interaction, and combat. The count feels justified given the domain, with only slight overlap (e.g., two warp tools) that could be consolidated.

Completeness4/5

The toolset covers the core lifecycle (start/stop/state), character setup, movement, NPC dialogs, and basic combat, with an escape hatch (ro_command) for anything else. Minor gaps exist (e.g., inventory or skill handling) but these are outside the apparent scope and can be addressed via the command tool.

Maintenance

ActivityMaintained
ResponsivenessNo issues