jevsnes.git / packages / mcp / README.md
1# Chapter 8: mcp, a window that answers questions
2
3The window is worked on by AI agents, and an agent cannot look at a
4screen. It can call tools, though. The [Model Context Protocol](https://modelcontextprotocol.io)
5(MCP) is a standard way for a program to offer tools to an agent client such
6as Claude Code: a list of named functions with JSON-schema arguments, called
7over JSON-RPC. So the window carries an MCP server, and an agent can ask the
8running game what it is doing instead of photographing it: `state`, `frame`
9(the picture as a PNG), `read_wram`, `bot`, `jev_history`, `budget`.
10
11**Who plays is not who watches.** The bot plays; the client watches. So the
12tools come in two sets. The ones that only look (`watching`) are always
13there. The ones that change anything (`driving`: `press`, `save_state`,
14`load_state`, `power_cycle`, `restart`, `hot_patch`) exist only while *dev
15mode* is on, and it is off whenever the window opens. Turning it either way
16tells every client its tool list changed.
17
18**The console stays on its own thread.** The emulator belongs to the window's
19UI thread. A tool call becomes a `Request` sent down a channel; the UI thread
20answers it at the next frame boundary and the reply comes back on a oneshot.
21Nothing in this crate touches the emulator, which is why it can be a library
22the window merely uses.
23
24```mermaid
25sequenceDiagram
26  participant A as Agent (Claude Code)
27  participant P as native mcp (stdio proxy)
28  participant S as Server in the window (127.0.0.1:7637)
29  participant U as UI thread (owns the console)
30  A->>P: tools/call state
31  P->>S: the same call, over streamable HTTP
32  S->>U: Request::State
33  U-->>S: at the next frame boundary
34  S-->>P: result
35  P-->>A: result
36```
37
38> **Aside: why a proxy in front?** A client talking HTTP straight to the
39> window loses its connection every time the window restarts, which during
40> development is constantly. So a client starts `native mcp` instead, the
41> same binary with no window, which speaks MCP on stdio and forwards to
42> whichever window is running. It outlives any number of windows, and tells
43> the client `tools/list_changed` when one appears, goes or is replaced. It
44> knows a window went away because it holds a request to the window's
45> `/alive` open, which is never answered and ends when that process does.
46
47MCP has two lifecycles in use, and the server speaks both: sessions (the
48protocol before 2026-07-28), kept in a file so a client connected before a
49restart is still connected after it; and sessionless clients that ask to be
50told of changes with `subscriptions/listen`.
51
52> **Try it.** With a window running (chapter 12), from the repository root:
53>
54>     tools/watch/budget.sh
55>
56> opens a session on `http://127.0.0.1:7637/mcp`, calls `state`, `budget`
57> and `bot`, and prints one line. Chapter 26 explains it.
58
59## For the people who maintain it
60
61### In this folder
62
63| Path | What |
64| --- | --- |
65| [src/lib.rs](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. |
66| [src/proxy.rs](src/proxy.rs) | `native mcp`: the stdio process a client starts, which outlives any number of windows. |
67| [src/sessions.rs](src/sessions.rs) | The file-backed `SessionStore` a restarted window restores clients from (`$XDG_STATE_HOME/jev/mcp-sessions.json`). |
68| [src/states.rs](src/states.rs) | Snapshot names (`Name`) and the snapshot directory (`States`) kept beside the ROM, as `<rom>.states/<name>.state`. |
69| [BUCK](BUCK) | The `rust_library` and `//packages/mcp:test`. Deliberately without the hot-patch flags. |
70
71The always-there tools are `state`, `frame`, `window`, `read_wram`, `states`,
72`bot`, `jev_history`, `budget`, `patch_info` and `dev_mode`. Each one except
73`budget` (which reads the shared spend ledger itself, through `jev-http`),
74`patch_info` and `dev_mode` is a `Request` answered by the UI thread, which
75owns the console and the bot: `bot` gets the zbanks bot's own report,
76already JSON (built in the app, so this crate does not link the C), and
77`jev_history` gets `decisions::goal_choice::Chooser`'s bounded history.
78Adding a watching tool is therefore two edits here (the `Request` variant
79and the `#[tool]` method) and one in `apps/native` (an arm in `App::answer`).
80
81This crate was pulled out of `apps/native` on 2026-09-20 so that editing the
82window's own code (`apps/native/src/app.rs`, `packages/panels`) never makes
83rmcp's tool-router macros and schemars' derive expand again; CLAUDE.md has
84the reasoning. It is the one crate here that serves a socket.
85
86← Previous: [Chapter 7, panels/](../panels/) · Up: [packages](../) · Next: [Chapter 9, apps/](../../apps/) →