Chapter 8: mcp, a window that answers questions

The window is worked on by AI agents, and an agent cannot look at a screen. It can call tools, though. The Model Context Protocol (MCP) is a standard way for a program to offer tools to an agent client such as Claude Code: a list of named functions with JSON-schema arguments, called over JSON-RPC. So the window carries an MCP server, and an agent can ask the running game what it is doing instead of photographing it: state, frame (the picture as a PNG), read_wram, bot, jev_history, budget.

Who plays is not who watches. The bot plays; the client watches. So the tools come in two sets. The ones that only look (watching) are always there. The ones that change anything (driving: press, save_state, load_state, power_cycle, restart, hot_patch) exist only while dev mode is on, and it is off whenever the window opens. Turning it either way tells every client its tool list changed.

The console stays on its own thread. The emulator belongs to the window's UI thread. A tool call becomes a Request sent down a channel; the UI thread answers it at the next frame boundary and the reply comes back on a oneshot. Nothing in this crate touches the emulator, which is why it can be a library the window merely uses.

sequenceDiagram
  participant A as Agent (Claude Code)
  participant P as native mcp (stdio proxy)
  participant S as Server in the window (127.0.0.1:7637)
  participant U as UI thread (owns the console)
  A->>P: tools/call state
  P->>S: the same call, over streamable HTTP
  S->>U: Request::State
  U-->>S: at the next frame boundary
  S-->>P: result
  P-->>A: result

Aside: why a proxy in front? A client talking HTTP straight to the window loses its connection every time the window restarts, which during development is constantly. So a client starts native mcp instead, the same binary with no window, which speaks MCP on stdio and forwards to whichever window is running. It outlives any number of windows, and tells the client tools/list_changed when one appears, goes or is replaced. It knows a window went away because it holds a request to the window's /alive open, which is never answered and ends when that process does.

MCP has two lifecycles in use, and the server speaks both: sessions (the protocol before 2026-07-28), kept in a file so a client connected before a restart is still connected after it; and sessionless clients that ask to be told of changes with subscriptions/listen.

Try it. With a window running (chapter 12), from the repository root:

tools/watch/budget.sh

opens a session on http://127.0.0.1:7637/mcp, calls state, budget and bot, and prints one line. Chapter 26 explains it.

For the people who maintain it

In this folder

PathWhat
src/lib.rsThe server: ADDRESS (127.0.0.1:7637), the Request channel to the UI thread, the watching and driving tool routers, dev mode, tools/list_changed for both lifecycles, the /alive endpoint.
src/proxy.rsnative mcp: the stdio process a client starts, which outlives any number of windows.
src/sessions.rsThe file-backed SessionStore a restarted window restores clients from ($XDG_STATE_HOME/jev/mcp-sessions.json).
src/states.rsSnapshot names (Name) and the snapshot directory (States) kept beside the ROM, as <rom>.states/<name>.state.
BUCKThe rust_library and //packages/mcp:test. Deliberately without the hot-patch flags.

The always-there tools are state, frame, window, read_wram, states, bot, jev_history, budget, patch_info and dev_mode. Each one except budget (which reads the shared spend ledger itself, through jev-http), patch_info and dev_mode is a Request answered by the UI thread, which owns the console and the bot: bot gets the zbanks bot's own report, already JSON (built in the app, so this crate does not link the C), and jev_history gets decisions::goal_choice::Chooser's bounded history. Adding a watching tool is therefore two edits here (the Request variant and the #[tool] method) and one in apps/native (an arm in App::answer).

This crate was pulled out of apps/native on 2026-09-20 so that editing the window's own code (apps/native/src/app.rs, packages/panels) never makes rmcp's tool-router macros and schemars' derive expand again; CLAUDE.md has the reasoning. It is the one crate here that serves a socket.

← Previous: Chapter 7, panels/ · Up: packages · Next: Chapter 9, apps/ →