For agents, on top of README.md, which they read first.

  • This is the one crate under packages/ that is native-only and does NOT build for wasm32-unknown-unknown. ../CLAUDE.md's "no std::fs/std::net/threads/windowing" rule is about every OTHER crate here staying portable to the browser build; this one is sockets (tokio::net, axum), threads (its own MCP-server thread) and files (sessions.rs, states.rs) on purpose, because that is what serving MCP and remembering sessions/snapshots IS. It lives under packages/ rather than a new top-level directory because it is a plain buck2 rust_library like every other package, just not a portable one - apps/native is its only consumer and always will be, since a browser build has no local process for an agent to talk to.
  • Why this crate exists at all, measured 2026-09-20: before the split, mcp.rs/proxy.rs/sessions.rs/states.rs were part of apps/native's own tip crate. Every edit to apps/native or packages/panels forced buck2 to re-run that crate's ONE rustc invocation, which re-expanded rmcp's #[tool_router]/#[tool] proc-macros and schemars' JsonSchema derive on every single hot-patch cycle, whether or not anything in this code had changed. As its own buck2 target, this crate is recompiled only when ITS OWN source changes.
  • Deliberately not hot-patch-flagged. No -Csave-temps/-Clink-dead-code in BUCK. An edit here is not meant to be part of the fast iteration loop tools/hotpatch serves - it needs a restart. If that ever stops being true (e.g. wanting to hot-patch a tool's description text), add the two flags the same way packages/panels/BUCK does, and patch.sh's existing first-party-dependency scan will pick this crate up with no other change.
  • A new Request variant breaks apps/native until it is answered, on purpose. App::answer matches every variant with no wildcard arm, so the compiler names the one place the console is actually read. Add the variant and the tool here, then the arm there — never a _ => {} to make the build go quiet, which would leave the tool hanging on a reply that never comes (the caller waits on the oneshot until the window closes).
  • A reader that can be absent returns Option<T>, and the tool says so in words, not with a JSON null: null reads to a client as this tool failing, where the truth is about where the game is.
  • Request's SaveState/LoadState/HotPatch variants and the tool methods that build them are the actual channel contract with the app; the UI-thread handling of every Request variant stays in apps/native (app.rs's App::answer), not here - this crate only ASKS, it never touches the console.