Skip to main content
Glama

rct2-agent

An MCP server + OpenRCT2 plugin that lets an agent sidecar-manage your park while you play: read the state, act on the business (prices, staff, marketing, loans), lay footpaths, put up shops and build rides, see the park (screenshots), and control time.

Building stops short of track. The agent can place footpaths, stalls and facilities, and flat rides — merry-go-round, ferris wheel, dodgems, haunted house and the rest, each a single track piece it can drop whole, with its entrance and exit. Tracked rides (roller coasters, transport rides) are still hands-off: they are built segment by segment, and there is no tool for that.

How it fits together

Claude Code (WSL)
      │ stdio (MCP)
      ▼
MCP server  ── dist/server.mjs, run by Windows node.exe
      │ TCP 127.0.0.1:7860  (newline-delimited JSON)
      ▼
Plugin (intransient) ── loaded inside OpenRCT2, LISTENS on the port
      │
      ▼
   OpenRCT2 game

The server runs under Windows node.exe (there is no node in WSL), so both the server and the plugin sit on the same Windows localhost — no WSL↔Windows networking involved. The plugin listens (it's intransient, so it stays alive across park loads); the server dials in and reconnects as needed.

Related MCP server: Roblox Executor MCP

Requirements

  • OpenRCT2 0.5.4+ (QuickJS engine — needed for Promises/ES2023).

  • Windows Node.js (found here at C:\Program Files\nodejs\node.exe).

Build & install

From WSL, using the Windows toolchain:

# helper wrappers (or just call node.exe / npm-cli.js directly)
NODE="/mnt/c/Program Files/nodejs/node.exe"
NPM=("$NODE" "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js")

"${NPM[@]}" install
"${NPM[@]}" run build          # builds dist/rct2-agent.plugin.js + dist/server.mjs
"${NPM[@]}" run install:plugin # copies the plugin into your OpenRCT2 plugin folder

install:plugin copies to C:\Users\casey\OneDrive\Documents\OpenRCT2\plugin\rct2-agent.plugin.js (override with RCT2_PLUGIN_DIR).

Wire up the MCP server

A ready-to-use project config is in .mcp.json. In Claude Code, from the project directory, it will be picked up automatically (approve it when prompted), or add it explicitly:

claude mcp add rct2-agent --scope project \
  -- "/mnt/c/Program Files/nodejs/node.exe" \
     "C:\\Users\\casey\\OneDrive\\Desktop\\dead code projects\\rct2-agent\\dist\\server.mjs"

Run

  1. Launch OpenRCT2 and load a scenario. Confirm the plugin loaded — the in-game console shows [rct2-agent] listening on 127.0.0.1:7860.

  2. Start Claude Code in this folder. The MCP server connects on first tool call.

  3. Ask the agent to manage the park. The core loop is measure → act → wait → measure: read baselines, make changes, advance_days(7), re-measure.

Tools

Read (eyes): get_park_summary, get_finance_report, list_rides, get_ride, list_shops, get_guest_overview, sample_guest_thoughts, list_staff, get_scenario

Act (hands): set_ride_price, set_shop_price, set_park_entry_fee, set_park_open, open_ride, close_ride, set_inspection_interval, start_marketing_campaign, set_research_funding, hire_staff, fire_staff, set_staff_patrol, set_loan

Build — survey: inspect_area, get_tile_detail, check_ride_access

Build — footpaths: list_path_styles, place_path, remove_path

Build — shops & facilities: list_shop_types, build_shop, remove_shop

Build — flat rides: list_ride_types, build_ride, build_ride_entrance, remove_ride

See (vision): capture_view, capture_ride, find_location, find_park_entrance

Time: get_clock, set_game_speed, pause, resume, advance_days

Save: snapshot, list_snapshots

Notes on behavior

  • Money is in plain dollars at the tool boundary; internally OpenRCT2 stores tenths, converted in src/shared/protocol.ts (MONEY_FACTOR). If a value looks off by 10×, that's the one knob to check.

  • Coordinates in capture_view / set_staff_patrol / find_location are in tiles (converted to map units internally).

  • advance_days unpauses, runs at max speed, and auto-pauses when the target date is reached — then returns the new clock. It blocks until done.

  • Snapshots save to save/agent__<play>__<label>.park, flat in the save root so they appear in the in-game Load Game menu — context.saveGame silently refuses any filename with a path separator, so subfolders are not possible. Restoring is a manual Load in-game — the plugin API can save but not load a park.

  • Writes go through game actions, so the game's own limits apply (e.g. the ride price cap). A rejected action comes back as a tool error.

  • Paths are one tile per call. place_path reports what the new tile actually connected to and, more usefully, which neighbours it missed and by how much — a path one land level off its neighbour looks like a finished route from above and is not walkable.

  • Shops are entered from one side. A stall or facility has no entrance element; guests use it from the single neighbouring tile it faces. build_shop turns the shop toward an adjacent path automatically, so building the path first is the easy order. Build it the other way round and you must pass direction yourself — there is no rotate action, so a shop facing the wrong way has to be removed and rebuilt. check_ride_access reports the facing side for shops and all four neighbours, so a mistake is visible.

  • Facilities are shops here. Toilets, first aid, the cash machine and the information kiosk build exactly like a food stall and go through the same list_shop_types / build_shop / remove_shop tools. The information kiosk is the one that sits on a different track piece and can be entered from all four sides, so its facing does not matter (usableFromAnySide in check_ride_access).

  • A new shop opens closed and priced at 0build_shop says so in its result. Follow up with set_shop_price and open_ride.

  • Flat rides cover a block, not a tile. list_ride_types gives each type's footprint and originOffset; the x,y passed to build_ride is the piece's origin, which for every 3x3 ride is the centre tile, so a merry-go-round at (50,50) covers (49,49)–(51,51). All of it must be owned, clear and level.

  • Only some sides of a ride will hold an entrance. Which ones is fixed by the track piece. The game accepts a rideentranceexitplace on any tile and then silently deletes an entrance on a disallowed side the next time it validates the ride, so build_ride picks from the legal sides only — preferring one a footpath already reaches — and check_ride_access reports them as entranceSites when a piece is missing. The table of legal sides per piece lives in FLAT_PIECES in the plugin, mirroring OpenRCT2's own TED.FlatRide.h.

  • A new ride opens closed and priced at 0, same as a shop, and open_ride refuses until the entrance and exit are both built and reachable. Guests reach the entrance over its connectAt tile, and that wants a queue line (place_path with queue: true); a plain path at the exit's connectAt is enough.

  • There is no rotate action for rides either — remove_ride and build again with a different direction.

Dev

  • npm run typecheck — tsc, no emit.

  • node scripts/smoke.mjs — loads the built plugin into a stubbed game and exercises the handlers over a real socket.

  • node scripts/mcp-smoke.mjs — spawns the built MCP server and drives it with real MCP JSON-RPC against a fake plugin.

Layout

src/shared/protocol.ts   wire protocol + money conversion + method names
src/plugin/main.ts       the intransient in-game plugin (TCP listener + handlers)
src/server/rct-client.ts reconnecting TCP client to the plugin
src/server/index.ts      MCP server: tool definitions -> plugin methods
esbuild.mjs              builds both bundles
scripts/                 install + smoke tests

Related MCP Connectors

Related MCP Servers