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 mcpinstead, 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 clienttools/list_changedwhen one appears, goes or is replaced. It knows a window went away because it holds a request to the window's/aliveopen, 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.shopens a session on
http://127.0.0.1:7637/mcp, callsstate,budgetandbot, and prints one line. Chapter 26 explains it.
For the people who maintain it
In this folder
| Path | What |
|---|---|
| src/lib.rs | The 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.rs | native mcp: the stdio process a client starts, which outlives any number of windows. |
| src/sessions.rs | The file-backed SessionStore a restarted window restores clients from ($XDG_STATE_HOME/jev/mcp-sessions.json). |
| src/states.rs | Snapshot names (Name) and the snapshot directory (States) kept beside the ROM, as <rom>.states/<name>.state. |
| BUCK | The 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/ →