The book · Chapter 10

Tools for agents

A coding agent is an AI that writes code by reading your files and running commands: it can't look at your window, and it can't watch a game play. A person often can't either, on a build server, or at the moment a test fails. So Shuyi Engine is built so that you and your agent can make, run, see, test and replay a game without a window and without guessing.

You have been using this all along. Every cargo shuyi command in this book is one set of operations, the tool contract, offered three ways: the shuyi command line, an MCP server, and Agent Skills that teach an agent to use them. A contract here means a promise of what goes in and what comes out, which a program can rely on.

The tool contract

Each operation takes a typed input and gives a typed answer, the same shape for every one:

You saw a limits in chapter 1 if you read to the end of what run printed:

limits:
  - no scene: the game's tools give no scene (the top screen is `title`)

That isn't an error. It's the tool being plain that there was something it couldn't tell you.

cargo shuyi <op> prints the answer for people, and with --json as one JSON document, a common text format for data, for a program, whose exit code, the number a program hands back when it stops, says its class: 0 when ok, 1 when it ran and found something, 2 for a wrong command, 3 for a setup to fix. The operations are new, check, lint, modules, add, run, scene, replay, playtest, autobalance, import, bake, doctor, explain and attach. Each is also a function of shuyi_cli::contract, which a game's own tests call:

    #[test]
    fn the_contract_checks_the_files_and_plays_a_script() {
        let tools = BasicTools::new(game());
        let answer = check(&tools, &CheckInput::default());
        assert!(answer.ok, "{answer:?}");

        let mut input = RunInput::default();
        input.script = keys("Enter Right Enter Backspace Enter Backspace");
        let answer = run(&tools, &input);
        let out = answer.result.as_ref().unwrap();
        assert_eq!(out.screens, ["title"]);
        assert!(out.events.iter().any(|e| matches!(e, Event::Quit { .. })));
    }

Rust: CheckInput::default() makes the input with every setting at its default, and let mut lets the next line change one. "{answer:?}" prints the whole answer in its debug form, if the test fails, as Debug was derived for it. answer.result.as_ref().unwrap() looks inside the Option without taking it out of answer. .iter().any(|e| matches!(e, Event::Quit { .. })) asks whether any event is a quit, whatever else it holds, which is what .. means.

The contract's page says what each operation answers, and every input and answer has a JSON schema, a description of a document's shape that a program can read: cargo shuyi schema run.output prints one.

A game gives the tools what they need of it in its tool program (chapter 1). On the short path that is BasicTools: its frame, its text, its files' errors and its controls. A game with a scene, records, a playtest or importers writes its own GameTools, adds those, and calls BasicTools for the rest; a grid game's stage has one to read.

One value of the game

A stage's scene carries its data: the stage's whole state, written by serde, the same derive that writes its state hash, so your game writes no code to show it. scene --path reads one value of it by a JSON Pointer, a path of keys from the top, and scene --shape answers its JSON Schema, every key it may hold:

cargo shuyi scene --script "Enter Right" --path /hero/x
cargo shuyi scene --script "Enter" --shape

The first answers your piece's column. A pointer that names nothing fails with SH0005, and its message lists the keys there are. An enum is written as serde writes it, its variant a key: /outcome/Won/turn is the turn a won stage ended on.

The stage's tools name the value with Scene::with_data. To show what lives on the screen and not in the state, such as the number of commands taken, a grid game's stage names a small struct of its own, StageData in its tools.rs: the stage's state, then its seed and its commands.

Rust: #[serde(flatten)] on StageData's stage field writes the state's own keys at the top, beside seed and commands, instead of under a stage key.

The rules' types derive JsonSchema, which describes their shape, under a schema feature that the game's tools feature turns on, so the game you ship builds no schema. A type of your own that the stage holds needs the same line, #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))].

The MCP server

MCP, the Model Context Protocol, is a common way for an agent to call tools. shuyi mcp serves the contract over MCP: each operation is a tool. Set it up once in the game's folder, and commit what it writes:

cargo shuyi mcp install
cargo shuyi mcp --ready

install shows what it would write for each agent it finds (Claude Code's .mcp.json always, and Cursor's, VS Code's and Gemini CLI's where it finds them) and asks first; --dry-run writes nothing. --ready builds the server and the game's tool program, so the first call doesn't wait for a build.

The server keeps one game running headless between calls, a session, so an agent can press a key, look, and press another, as you would:

ToolDoes
startbuilds the game's tool program and starts the game, with its start options
stepplays a script of keys, buttons, actions by name and waits, then idle frames, and answers as run does
scenethe scene on screen now, and the log; with path, one value of its data, with shape, its data's shape
frame_pnga picture of the frame now
eventswhat happened since the start, the sounds, and the sounds the game made at run time
perfthe frames' times and memory
resetstarts the game again as start did
stopstops it

Call start again after changing the game's code. With link, start attaches the session to a debug build running in its window instead, and frame_png then gives the real window's picture.

The skills

An agent that knows the tools still has to know what to do with them. The engine's skill pack teaches an agent each kind of task on a game: starting one, a screen, input, a grid stage, dialogue, tests, replays, bots, performance, shipping. Each skill says when to use it, its steps, the tools it calls, and what to do on each problem code, and each has a worked example that the engine's CI runs on a new game, so a skill that has gone wrong fails there first.

A game's own AGENTS.md holds the rules a game on the engine keeps (this book's chapters 3, 4 and 6, in short), for any agent that reads it. It also links the engine's rules and docs, those of the engine the game builds with: for a game on main, the site's llms.txt and llms-full.txt; for a game on a commit (--engine-rev), this book's Markdown at that commit; for a game on a checkout (--engine-path), the same files in that folder.

Problem codes

Every error the tools give has a stable code: SH and four digits, whose first says its class. An agent branches on the code, never on the message, as a message's words may improve over time and the code won't change. It's handy for you too: cargo shuyi explain lists every code, and cargo shuyi explain SH2003 prints one code's page, what it means and how to fix it. The same pages are on this site, in the problem codes.

check: error[SH1002]: shuyi.ron names no game tool program (`game`), and check runs the game
  help: fix shuyi.ron as the message says, then run the same command again
  see: shuyi explain SH1002

When it goes wrong

A command the tools don't know. It says so with a code, then lists every command, so this is also the way to find one:

cargo shuyi: error[SH0001]: unknown command: walk
  see: shuyi explain SH0001

A setup to fix. The SH1002 above is what you get from a folder whose project can't run the game. Its exit code is 3: nothing is wrong with your game, and the tools couldn't get as far as looking at it. Check that you are in the game's folder, then run cargo shuyi doctor, which lists what your machine has and how to get what it lacks:

doctor: host x86_64-pc-windows-gnu
  ok   rust: rustc 1.99.1 (58cf8f9c3 2026-10-13)
  ok   cargo: cargo 1.99.1 (5f94df478 2026-08-27)

A session showing old code. The server runs the game as it was at start, so after changing code the answers are of the old game. Call start again.

An ok that didn't look at everything. Read limits. lint on a game with no rules crate says so:

lint: OK
limits:
  - the rules crate's fence wasn't checked: shuyi.ron names no rules package (`rules`)