Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
20 KiB
Agent control: driving the launcher and the game
A player's own AI agent (Claude Code, Codex, Cursor, any MCP client) or a script can do everything a player does with the mouse and keyboard, in the launcher and in the game. This page covers developers: the surfaces, the full inventory, and the rules every new command follows. Players read Control Horizon with your AI agent (playhorizon.dev/docs/agents).
The command lists are generated, so they can't drift:
- launcher-commands.md: every
horizon-launchercommand, its arguments and its MCP tool. It comes from the CLI's clap definition (cli_tools::markdown), and the launcher testthe_launcher_reference_is_up_to_datefails when it is stale or a tool is missing. - horizon-ctl-commands.md: every
horizon-ctlcommand, its MCP tool and its arguments. It comes from the command table (horizon-ctl help --markdown), and the testthe_generated_reference_is_up_to_datefails when it is stale.
Surfaces
| Surface | Where | Transport |
|---|---|---|
| Game control socket | horizon-client --control / --control-socket PATH, or Agent Control on in Horizon Settings > AI Agent (crates/horizon-client/src/control.rs; commands in src/game/control.rs, agent.rs, ui_control.rs, saves_control.rs and friends) |
text line in, one JSON line out; Unix socket, or localhost TCP and a token where there is none |
horizon-ctl |
crates/horizon-client/src/bin/horizon-ctl/ |
CLI over the socket; one table of commands (commands.rs) gives the help, the argument checks, the MCP tools and the reference |
horizon-ctl mcp |
same | MCP over stdio, one tool per command of the table |
| Launcher CLI | launcher/src/cli.rs |
horizon-launcher <command> [--json] |
| Launcher MCP | launcher/src/mcp.rs, cli_tools.rs, game_tools.rs |
horizon-launcher mcp (stdio): every CLI command as a tool, the session tools, and the game's tools as game_* |
| Connecting | launcher/src/agents.rs, agents_page.rs; src/game/agent_settings.rs |
horizon-launcher agent-config; Settings > AI in the launcher; Horizon Settings > AI Agent in the game |
| No AI | crates/horizon-core/src/no_ai.rs |
noAi:true in <game home>/horizon-extras.txt, read by every surface |
One MCP server for everything
An agent adds one server, horizon-launcher mcp. It holds:
- The launcher's CLI, generated.
cli_tools.rswalks the clap definition. Each command is a tool named after its words (mods searchismods_search). Its arguments are the command's, in snake_case, with clap's help, choices and defaults. A call runs that command with--jsonas a child process and returns what it printed. A new CLI command is a new tool with no extra code. - Session tools, which keep something running between calls:
play(the game, under control:world,creative,join,offscreen,arg);server,server_command,server_log,server_stop;account_sign_in,account_list.
- The game's tools. It runs the selected version's
horizon-ctl mcp(shipped beside the game) as a child. It lists that server's tools asgame_<name>, all butlaunch, and passes calls and results through, images included.playstarts the game with a control socket in the launcher's data folder. While no game answers,game_*calls say to callplay.
Why one server rather than two: the game's tools stay defined once, in horizon-ctl's table. The
agent's client needs one entry and one approval. The version that plays is always the one the
launcher selected, so the game's tools always match the game. horizon-ctl mcp stays available
on its own, for a game started by hand or by a script.
Inventory, before and after
Before is main at 2026-10-23, before #1342, #1343, #1344, #1345 and #1353. After is with
them. CLI is horizon-launcher for the launcher and horizon-ctl for the game. MCP is
horizon-launcher mcp (the game's tools as game_*) and horizon-ctl mcp. part means only
through a generic command (ui and click by widget, or a raw socket line that horizon-ctl
passes on without checking it).
Launcher
| Capability | Window | CLI before | MCP before | CLI after | MCP after | Notes |
|---|---|---|---|---|---|---|
| Releases and installed versions | Play, version picker | yes | yes | yes | yes | versions [--installed] |
| Install, remove a version | Play | yes | yes | yes | yes | remove needs confirm in MCP |
| Pick the version that plays | Play | no | no | yes | yes | settings selected-version TAG |
| Prepare resources | Play | yes | yes | yes | yes | |
| Start the game | Play | yes | yes | yes | yes | play --world --creative --join --arg; MCP play adds offscreen |
| Join a friend by invite code | Play | yes | no | yes | yes | play --join CODE |
| Dedicated server here: start, console, log, stop | Servers | yes | yes | yes | yes | server; MCP server, server_command, server_log, server_stop |
| Hosted servers: list, create, set up, run a command, forget | Servers | yes | no | yes | yes | servers ...; servers_forget needs confirm |
| Accounts: list, sign in, sign out, select, offline | Settings | yes | part | yes | yes | names and UUIDs only; sign-in is a device code; account_offline was missing |
| Game folder, server folder, install new versions, opening animation | Settings | no | no | yes | yes | settings [KEY VALUE] |
| Extra game arguments | Settings | yes | no | yes | yes | game-args |
| Applications menu entry (Linux) | Settings | yes | no | yes | yes | desktop-entry |
| Updates: check and install | Settings | yes | part | yes | yes | MCP could only check |
| Patch notes, links | Patch notes, footer | yes | yes | yes | yes | |
| Game log, crash reports | Settings | yes | yes | yes | yes | logs [--crash] |
| Data collection: status, allow, deny, show, delete, send, bug, benchmark upload | Settings > Data | yes | part | yes | yes | sending was missing in MCP; data_delete needs confirm |
| Mod browser: search, check, install, update, remove, list (mods, resource packs, modpacks) | Mods | yes | no | yes | yes | mods ...; mods_remove needs confirm |
| Modpacks: import, port, play, remove, list | Modpacks | yes | no | yes | yes | modpack ...; modpack_remove needs confirm |
| Launcher benchmark | (none) | yes | no | yes | yes | |
| The game it started | (the game) | (horizon-ctl) | yes | (horizon-ctl) | yes | game_* |
| Connect an AI agent | Settings > AI | no | (none) | yes | (none) | agent-config [--client] [--test]; the agent is already connected, so there is no tool |
| No AI | Settings > AI | no | (none) | yes | (none) | no-ai [on|off]; the player's switch, never an agent's |
Game
| Capability | Screen | CLI before | MCP before | CLI after | MCP after | Notes |
|---|---|---|---|---|---|---|
| Start, wait until ready, save, quit, log | (all) | yes | yes | yes | yes | launch, wait-ready, save, quit, logs |
| Every widget of every screen, by id; click, set, type | (all) | yes | yes | yes | yes | ui, click ID, set ID VALUE, type; click-at, drag-at, scroll-at by pixel |
| Title menu, pause menu | Title, Pause | yes | yes | yes | yes | ui/click, pause, resume |
| Worlds: list | Select World | part | part | yes | yes | worlds |
| Worlds: create, open | Create New World | yes | yes | yes | yes | open WORLD [SEED] [creative] |
| Worlds: rename, back up | Edit World | part | part | yes | yes | world-rename, world-backup |
| Worlds: delete | Select World | part | part | yes | yes | world-delete FOLDER --confirm; it was possible through click without any confirm |
| Servers list | Multiplayer | part | part | yes | yes | servers (names and addresses; reads servers.dat) |
| Join a server or an invite code, leave | Multiplayer | part | no | yes | yes | connect, disconnect |
| Open to LAN | Pause > Open to LAN | part | no | yes | yes | lan open|close|status |
| Play with Friends: host, stop, status | Pause | part | no | yes | yes | friends host|stop|status |
| Vanilla options, every page | Options | yes | part | yes | yes | options-get, options-set (all of options.txt, typed); options PAGE was socket-only |
| Horizon Settings: Graphics, Controller, Extras, Mods, AI Agent | Horizon Settings | part | part | yes | yes | options horizon|graphics|extras|agent..., then ui/click/set |
| Agent control on or off, status, connection details | Horizon Settings > AI Agent | no | no | yes | yes | new page (#1353) |
| Key binds | Controls > Key Binds | yes | yes | yes | yes | options-set key_key.jump key.keyboard.j |
| Resource packs | Options > Resource Packs | part | part | yes | yes | options packs, then ui rows and click; options-set resourcePacks |
| Chat and commands | Chat | yes | yes | yes | yes | chat, command, messages, chat-log |
| Inventory, containers, stations | Inventory, Chest... | yes | part | yes | yes | inventory, screen, click-slot; container X Y Z was socket-only |
| Crafting, recipe book | Crafting | part | part | yes | yes | the grid by click-slot; the recipe book by ui/click |
| Trading, riding, shearing (right-click an entity) | Merchant, Horse | part | no | yes | yes | interact-entity ID; mouse was CLI-only |
| Horse, llama inventory | Horse | no | no | yes | yes | mount-menu |
| Books, lecterns, signs | Book, Sign | part | part | yes | yes | book [next|back|take|done], signedit X Y Z |
| Move, look, find a path | (playing) | yes | yes | yes | yes | move, goto, look, look-at, turn, jump |
| Mine, place, use, attack, hotbar, drop | (playing) | yes | yes | yes | yes | break, place, interact, use-item, attack, select, drop |
| Raw keys and mouse buttons | (all) | yes | part | yes | yes | key; mouse, release were CLI-only |
| Controllers | (all) | yes | no | yes | yes | pad |
| Respawn | Death | yes | yes | yes | yes | respawn |
| Screenshots | (all) | yes | yes | yes | yes | PNG of the next frame, --offscreen too |
| F3 data | F3 | yes | part | yes | yes | key f3; status (renderer, chunks, memory) was CLI-only |
| Waits and events | (all) | yes | yes | yes | yes | wait, wait-for, events |
| Effects, experience, enchantments, potions, fill | (cheats) | part | no | yes | yes | effect, effects, xp, enchant, potion, fill |
| Benchmarks and frame stats | (all) | yes | part | yes | yes | bench, stats were CLI-only; fly |
No AI
One switch hides and turns off every AI feature: noAi:true in <game home>/horizon-extras.txt.
The game home is HORIZON_HOME, else the data dir's horizon. The game reads its extras from
its working directory, which is that folder. horizon_core::no_ai reads and writes the switch:
is_on(home)andcheck(home)read it;set(home, on)keeps the file's other lines, and writes no file to turn off a switch that was never on;REFUSALis the one line every surface prints.
The switch is in three places: Settings > AI in the launcher, Horizon Settings in the game
(Item::NoAi), and horizon-launcher no-ai on|off. When it is on:
| Surface | What happens |
|---|---|
| Game | opens no control socket, even with --control (it logs the refusal); Horizon Settings hides AI Agent...; turning it on in a running game closes the socket at once |
horizon-ctl |
every command refuses: exit status 4, {"error": REFUSAL, "kind": "denied"} |
horizon-ctl mcp |
refuses to start (exit 4, REFUSAL on stderr); a session already running refuses each call |
horizon-launcher mcp |
refuses to start; a running session answers each tools/call with error -32000 and REFUSAL |
| Launcher CLI | mcp and agent-config are hidden from the help and refuse |
| Launcher window | Settings > AI shows only the No AI switch, without Connect an AI agent |
| Companion (#1068) | uses the same switch (see horizon-parity companion-notes.md) |
Every other launcher command stays usable: versions, install, remove, prepare, play, server,
notes, links, account, settings, game-args, logs, update, desktop-entry, data,
modpack, mods, servers, no-ai and benchmark. None of them drives the game for an agent. The
launcher itself, packagers, the docs and players' scripts need them, and no-ai has to stay to
turn the switch off.
Connecting
- Launcher, Settings > AI > Connect an AI agent:
- a client picker (Claude Code, Codex, Cursor, any MCP client) with its config and a Copy button;
- for clients with a CLI, the add command, shown and run only on Add to ...:
claude mcp add --scope user horizon -- <exe> mcp,codex mcp add horizon -- <exe> mcp; - Test the connection (
agents::self_test) starts<exe> mcp, lists its tools and says how many answered. horizon-launcher agent-config [--client ID] [--json] [--test]prints the same thing.
- Game, Horizon Settings > AI Agent:
- Agent Control (
agentControlin horizon-extras.txt, off by default) opens the default endpoint ($HORIZON_CONTROL, elsehorizon-control.sockin the runtime directory) in a game the player started. Turning it off closes the endpoint and stops its listener. - The page shows whether a client is connected (
Server::clients), when the last command came, and the socket's name. - Copy Connection Details copies the socket path, a
horizon-ctl --socket ... stateline and anmcpServersconfig for this game'shorizon-ctl mcp.
- Agent Control (
- The default: a game an agent starts (
playin MCP,horizon-ctl launch,--control) is under control. A game the player starts from the window is not, until they turn Agent Control on. An open socket lets any program of that user drive the game, so it opens only when an agent asked for the game, or when the player chose it.
Design rules for a new command
One vocabulary
-
A command has one name, kebab-case, the same on the socket, in
horizon-ctland in the launcher CLI. Its MCP tool is the name with_for-(click-slotisclick_slot). In the launcher, a subcommand's words are joined (mods searchismods_search). The launcher's MCP offers every game tool asgame_<name>, so none collides with its own (logsis on both sides). A launcher tool whose name starts withgame_(game_args) is matched first. -
Arguments: positional words, then
--flags.- The game: the
horizon-ctltable (Param) is the source of truth. It generates the MCPinputSchemaand the reference. A game command is not done until it is in that table. - The launcher: the clap definition is the source of truth. Give every argument a help line; it becomes the tool's description.
- The game: the
-
Replies are one JSON object. A query returns its data; an action returns
{"ok": true, ...}plus what changed (clicked,screen,drops...). -
Errors are
{"error": "<sentence an agent can act on>", "kind": "<reason>"}. The kinds:usage: a bad or missing argument;unknown: no such command, widget, option or entity;state: the game is not in a state that allows it (no world, wrong screen, disabled widget);busy: another action or a benchmark is running;timeout;connection: the game can't be reached;denied: No AI is on (exit status 4).
The message names what to do next ("no world open; use: open WORLD").
-
Shapes are shared:
- a position is
[x, y, z]; - a block is
{"pos", "block", "state", "loaded"}; - an item stack is
{"item", "count", "damage"?, "components"?}; - a slot is a stack plus
"slot": ID(the slot ids ofclick-slot); - bounds are
[x, y, w, h]in GUI pixels, the coordinates ofclick-at.
- a position is
Safety
- Local only. The socket is a Unix socket in the user's runtime directory, or a TCP port on
127.0.0.1 behind a 128-bit token in a file only the user can read. It is never
0.0.0.0and never a network listener. Both MCP servers use stdio. - Confirm what can't be undone. A command that deletes the player's things requires
confirmand carries the MCPdestructiveHint. In the game these areworld-delete,fillandsetblock(the table'sDESTRUCTIVE); in the launcher,remove,modpack remove,mods remove,servers forget,account sign-outanddata delete(cli_tools::DESTRUCTIVE). Withoutconfirm: truethe call is refused and the message says to ask the player. In the game CLI the flag is--confirm. The launcher CLI asks nothing: a person typingremovemeant it. - Ask before reaching out. A command that sends data or reaches other people carries
openWorldHint, and its description says "only when the player asks". In the game these areconnect,lan,friends,chatandcommand; in the launcher,data allow,data send,data bug,data benchmarkandservers create.play'sjoinargument says the same. Telemetry stays opt-in, and onlydata allowturns it on. - No secrets. No surface returns tokens, session files, keychain entries or invite secrets
the player didn't ask to share:
- accounts are names and UUIDs;
- sign-in is a device code the player enters;
--session-fileis read once and deleted;serversreads names and addresses only.
- Vanilla behaviour untouched. Control code reads state and synthesizes the input events a
player produces; registering widgets for the tree draws nothing. A change to game files runs
tools/parity.py --changed origin/main.
The widget tree
ui returns every interactive element the last frame drew, on every screen:
{"screen": "options", "gui_size": [427, 240], "frame": 3061,
"widgets": [
{"id": "fov", "kind": "slider", "label": "FOV: 70", "bounds": [40, 50, 150, 20], "enabled": true, "value": 0.5},
{"id": "done", "kind": "button", "label": "Done", "bounds": [...], "enabled": true}]}
-
Ids are stable and readable. An id is the label up to its
:, as a slug (FOV: 70isfov,AI Agent...isai-agent). A repeated slug gets-2,-3; unlabelled elements are<kind>-<n>. A slider's id doesn't change with its value. Slots areslot-<ID>with their stack. -
Kinds:
button,slider(value0..1),field,row(a world, server, pack or language),tab(selected),slot. -
Actions:
click ID [right|middle] [shift]clicks where the mouse would.set ID VALUEmoves a slider.typetypes into the focused field.
After a click, the next
ui,clickorsetwaits for a redrawn frame. -
No new code paths in screens. The tree is built from what the draw functions already register for controller navigation (
widgets.rs).
Waits and events
wait-for screen NAME|widget ID|loaded|chat TEXT|event KIND [--timeout S] [--since SEQ]replies when the condition holds, or withkind: timeout.events [--since SEQ]returns what happened since SEQ: screens, chat, deaths, respawns, items, dimensions and worlds (the newest 256). Takelastbefore an action, and wait with--since, so the action's event can't slip in before the wait.
Tests
- Unit tests:
- horizon-ctl: the table, the schemas, the reference, the No AI refusal, the destructive and network annotations;
- the launcher: generated tools,
confirm, the reference, the agent configs; horizon_core::no_ai;options_ui: the AI Agent page, and that No AI hides it.
tools/agent_control_e2e.pydrives a real game, offscreen on macOS and on a virtual screen on Linux, through the CLI and MCP:survival,ui,options,events,consume,pick,suggestions;manage: worlds, servers, effects, books, LAN, the options pages andconfirm;no-ai: every surface refuses;setting: Agent Control on opens the endpoint, the AI Agent page, Copy, and off closes it.
tools/launcher_mcp_e2e.pydriveshorizon-launcher mcp: the generated tools,playand thegame_*tools,confirm,agent-configand--test, and No AI.
Later
These are tracked in FEATURES.md (LATER):
- Horizon's own option files by key (
horizon-graphics.txt, controller, extras) next tooptions-getandoptions-set. - Adding and removing saved servers by command.
subscribe: events as a stream on one connection.- Launcher window screenshots in this page's PRs.
- No AI changed in the launcher while a game is open: the game rewrites its extras when it saves.
- A confirmation question in the launcher CLI for destructive commands.