# godot-mcp > Bridge an AI agent and a live Godot editor over MCP — inspect, mutate, run, and verify a real project. 194 tools across 29 gated toolsets; the server owns safety and typing, the Godot addon owns the editor. godot-mcp is a generic, game-agnostic MCP server: the FastMCP server owns safety and gating, the Godot addon owns editor API calls, and a versioned JSON envelope joins them. Start with concepts (how it works and why), then getting-started for setup. Every mutation result carries honesty fields (persisted/undoable/aborted_at/rescan_pending) that read as ground truth; errors are a stable enum with recovery hints. Each page below also serves a clean markdown variant at its URL with `.md` appended (e.g. `architecture/bridge.html.md`), plus `llms-full.txt` for the whole site in one fetch. ## Home ## See it catch a mistake Ask your assistant to add a node inside an instanced scene: ```text > add a marker node under Relic/Cold { "node_path": "Relic/Cold/Marker", "created": true, "persisted": false, "reason": "instanced_child_not_editable", "hint": "'Relic/Cold' is inside the instanced scene 'Relic', which doesn't have Editable Children enabled — the change shows in the editor but won't be saved. Enable Editable Children on 'Relic', or target a node the scene owns." } ``` A file-blind agent would just say *"done."* godot-mcp tells you **what will actually be saved** — and exactly how to fix it. That honesty is the whole point. ## Start in about two minutes 1. **Add the addon** — install [Godot Editor MCP - Addon](https://store.godotengine.org/asset/john-d-studios/godot-editor-mcp-addon/) from the Godot Asset Store, then switch it on in *Project → Project Settings → Plugins*. 2. **Point your AI assistant at it** with one line in its MCP config (for Claude Code: `claude mcp add godot -- uvx godot-editor-mcp`). 3. **Tell it what you want** — *"add a player that jumps"*, *"make the jump feel snappier"*, *"why does it crash on start?"*. New to all this? **[Start with your AI agent](./getting-started-with-an-agent)** walks it through step by step — your assistant does the installing. Prefer to do it yourself? **[Manual setup](./getting-started-install)**. ## Two ways in - **Here to make a game?** Hand the setup to your assistant and build by describing what you want. → **[Start with your AI agent](./getting-started-with-an-agent)** - **Here for the tool itself?** (you know MCP — and maybe want to contribute.) → **[Concepts](./concepts)** · **[Under the hood](./architecture/)** · **[Reference](./reference)** · **[Contributing](https://github.com/hybridindie/godot-mcp/blob/main/CONTRIBUTING.md)** ## Where to go next - **[Get started](./getting-started)** — install, connect, and drive your first session - **[Guides](./guides)** — build a scene, play-test & debug, verify your work, ship - **[Concepts](./concepts)** — how it works, and why it's built this way - **[Reference](./reference)** — every tool, safety class, error code, and setting - **[Under the hood](./architecture/)** — the bridge, the envelope, gating, and safety --- > **For AI agents.** This site ships [`llms.txt`](https://hybridindie.github.io/godot-mcp/llms.txt) (a page index) and [`llms-full.txt`](https://hybridindie.github.io/godot-mcp/llms-full.txt) (the whole site in one fetch). ## Get started # Get started Two ways to get running. Pick whichever fits. ## Option 1 — Let your AI assistant do it (easiest) You use an AI assistant (Claude, Codex, ChatGPT…) and would rather not touch a terminal. Hand it the setup: → **[Start with your AI agent](getting-started-with-an-agent.md)** — one copy-paste prompt installs the server, the addon, and the skill; you do one click and start building. ## Option 2 — Set it up yourself You're comfortable with installs and MCP. Five steps: 1. **[Install the MCP server](getting-started-install.md)** — PyPI, Docker, or from source. 2. **[Install the Godot addon](getting-started-addon.md)** — from the [Godot Asset Store](https://store.godotengine.org/asset/john-d-studios/godot-editor-mcp-addon/) (**Godot Editor MCP - Addon**), or copy it in; then enable it. 3. **[Configure your client](getting-started-clients.md)** — OpenCode, Claude, or any MCP host. 4. **[Install the skills](reference-skills.md)** — this is what makes your assistant enable toolsets on its own, read the honesty fields, and avoid the common Godot pitfalls. Do it before your first session. 5. **[Run your first session](getting-started-first-session.md)** — the orient → enable → preview → act → verify rhythm. Running several clients against one editor, or driving Godot remotely? See **[Remote & multi-client](getting-started-openwebui.md)**. ## Requirements | Component | Minimum | Recommended | |-----------|---------|-------------| | **Godot** | 4.4 | 4.7 (validated target) | | **Python** | 3.11 | 3.13 | | **Package manager** | [uv](https://docs.astral.sh/uv/) | uv | | **OS** | macOS / Linux / Windows | Any desktop | ::: info Godot 4.4 is the floor The addon checks the editor version on enable and warns if it's older. A few toolsets (`scene_edit`, `input_map`, `tilemap`, `scene_3d`) are version-gated and refuse to enable on older editors. ::: New to the ideas behind any of this? **[Concepts](concepts.md)** explains the mental model in five short ideas. # Set up with your AI agent **You don't need a terminal, and you don't need to know what MCP is.** This page is for people who make things in Godot and use an AI assistant (Claude, Codex, ChatGPT…). The assistant does the setup; you make the game. > Already comfortable with terminals and MCP? The [technical setup](getting-started-install.md) > and the [architecture](architecture/index.md) / [reference](reference.md) sections are > for you — this page is the friendly on-ramp for everyone else. ## What this gives you Once it's connected, your assistant works **directly inside your real Godot project**: - create and arrange scenes and nodes, - write and fix GDScript, - run the game and read the errors, - and check its own work — with every change landing on Godot's own **undo** stack, so you can Ctrl+Z anything it does. You stay in Godot; the assistant works through the editor you already have open. ## What you need - **Godot 4.4 or newer**, with your project open. - An **AI assistant that can run commands and edit files** — for example Claude Code, OpenAI Codex, or OpenCode. A plain chat window can't install things for you (see [Not using a coding assistant?](#not-using-a-coding-assistant) below). - That's everything — the assistant installs godot-mcp itself. ## Step 1 — Open your project in Godot Have your Godot project open in the editor. Any project works; a brand-new empty one is fine to start. ## Step 2 — Ask your assistant to set it up Open your assistant **in your project's folder** and paste this: ```text Set up godot-mcp for my Godot project so you can drive the editor. 1. Install the server: run `pip install godot-editor-mcp`. (If pip is unavailable, don't install anything else — `uvx godot-editor-mcp` runs it on demand and is enough.) 2. Register godot-mcp as an MCP server for yourself, launching it with the command `uvx godot-editor-mcp` over stdio. Use the correct MCP-config method for your client (for Claude Code that is: `claude mcp add godot -- uvx godot-editor-mcp`). 3. Install the Godot addon into my project: download the latest `godot_mcp_addon.zip` from https://github.com/hybridindie/godot-mcp/releases/latest and unzip it into my project folder (it adds `addons/godot_mcp/`). 4. Install the godot-mcp AI skills into your own skills directory so you know how to work here — they teach you to enable toolsets on your own, read the result verdicts, and avoid the common Godot pitfalls. Clone https://github.com/hybridindie/godot-mcp and run `./scripts/install-skills.sh --copy`, or copy the `skills/*` folders into your skills directory. Reload your skills when done. 5. When that's done, tell me to enable the addon: in Godot, Project → Project Settings → Plugins → enable "Godot Editor MCP - Addon". 6. Once I confirm, verify the connection by calling `godot_health_check` and `godot_get_server_info`, and tell me the version it reports. Ask me for my project folder path if you're not sure where it is. ``` ## Step 3 — Enable the addon (the one click that's yours) In Godot: **Project → Project Settings → Plugins**, and switch on **Godot Editor MCP - Addon**. A small status dock appears at the bottom of the editor. When its dot turns **green**, the assistant is connected and ready. ![The godot-mcp status dock: a green Connected dot, the Godot version and bridge URL, the active scene and selected node, the last undoable action, the enabled toolsets, and a recent-command log showing what the assistant has been doing.](/dock.svg) *A green dot means connected. The **recent-command log** at the bottom shows exactly what the assistant has been doing, and the **Last action** row is undoable with Ctrl+Z.* ## Step 4 — Check it worked Tell your assistant: ```text Run a health check on godot-mcp and tell me whether the Godot editor is connected. ``` If it reports *connected* (and its version matches what it installed), you're set. ## Now make something Just describe what you want, in plain language: - *"Create a new scene with a player that moves with the arrow keys."* - *"Add a HUD showing the player's health, top-left."* - *"The jump feels too floaty — make it snappier."* - *"Run the game and tell me about any errors."* - *"Set up a main menu with Play and Quit buttons."* The assistant turns that into editor actions; you watch them happen in Godot and undo anything you don't like. ### What the assistant can do | You say… | What happens in Godot | |----------|-----------------------| | "build / change a scene" | nodes, properties, and signals — with undo | | "write / fix this script" | GDScript written into your project, with a parse check | | "run it and tell me what breaks" | the game runs; errors and warnings come back | | "test that X works" | live checks and screenshots | | "export a build" | Web / desktop builds | ### Things worth knowing - **It asks before destroying.** Deleting or overwriting needs your OK first; previews are free. - **Everything is undoable.** The assistant's changes sit on the same undo stack as yours — Ctrl+Z works. - **It can't see *your* screen** — but a vision-capable assistant can screenshot the editor or the running game to *look at* the scene. It still can't watch you, so answer its questions and keep an eye on the editor. - **Better assistants go further.** Claude Code and Codex can run the setup and chain many steps; a small local model may need more back-and-forth. ## If something's off | What you see | What to do | |--------------|------------| | "Godot bridge is not connected" | Make sure Godot is open with the addon enabled and the dock dot green. | | The dock dot stays grey or red | Restart Godot, or toggle the plugin off and on again. | | "unknown tool" | Ask the assistant to *"enable the toolset it needs first."* | | The assistant can't find Godot | Tell it where your Godot app and project folder are. | | It broke after an update | Ask the assistant to reinstall the addon from the latest release. | Still stuck? Point the assistant at the [workflow prompts](reference-prompts.md) (including `troubleshoot`) — they're written for exactly this. ## Not using a coding assistant? The setup above needs an assistant that can run commands and edit files. If you only have a **chat window** (no agent), ask a technical friend to do the [technical install](getting-started-install.md) once — after that, any MCP-capable assistant can use it. --- **Technical readers.** If you want the internals — or to contribute — the rest of these docs are for you: start at [Concepts](concepts.md) or [Under the hood](architecture/index.md), and see [`CONTRIBUTING.md`](https://github.com/hybridindie/godot-mcp/blob/main/CONTRIBUTING.md). # Install the MCP server ::: info For readers who know MCP This is the manual / technical path. Rather not touch a terminal? → **[Set up with your AI agent](getting-started-with-an-agent.md)** has your assistant do it. ::: ## Option A: PyPI (recommended) ```bash uv tool install godot-editor-mcp # or: pip install godot-editor-mcp godot-editor-mcp # starts the MCP server over stdio ``` ## Option B: Docker ```bash docker run -d -p 9090:9090 -p 9080:9080 \ -e GODOT_MCP_AUTH_TOKEN=$(openssl rand -hex 32) \ ghcr.io/hybridindie/godot-mcp:latest ``` Docker binds `0.0.0.0` inside the container, so a **bearer token is required** (#226). The server refuses to start on a non-loopback bind without one. ## Option C: From source (development) ```bash git clone https://github.com/hybridindie/godot-mcp cd godot-mcp uv sync uv run godot-editor-mcp # stdio mode (default) ``` ## Choose a transport | Transport | Use case | How | |-----------|----------|-----| | `stdio` (default) | A single local agent | the client spawns `godot-editor-mcp` itself | | `http` | Shared service; several clients on one editor; remote/web UIs | `GODOT_MCP_TRANSPORT=http` or `scripts/serve-http.sh` | ::: tip Service mode for multiple clients The bridge to the editor is a single connection — one server process owns it at a time. To give **several** clients (OpenCode, Claude, OpenWebUI, an eval harness) the same live editor, run **one** HTTP service and point every client at `http://:9090/mcp`: ```bash GODOT_MCP_TRANSPORT=http GODOT_MCP_AUTH_TOKEN= \ uv run godot-editor-mcp ``` Non-loopback HTTP binds **require** a `GODOT_MCP_AUTH_TOKEN` — the server refuses to start without one (fail-fast, #226). ::: ## Verify ```bash GODOT_MCP_BRIDGE_URL=ws://127.0.0.1:9080 uv run godot-editor-mcp # logs are JSON on stderr; the MCP protocol is on stdout ``` The server binds the bridge listener immediately — before Godot exists. Next: [install the addon](getting-started-addon.md). # Install the Godot addon The addon is the only part of the system that lives inside Godot. It is an `EditorPlugin` (`@tool`) that: - dials the server's bridge listener (`ws://127.0.0.1:9080` by default) and **reconnects with backoff** — launch order never matters - routes the server's `{id, command}` envelopes to `cmd_*` handlers that call the Godot Editor API (one per operation, grouped by domain — see [The Godot addon](architecture/addon.md)) - shows a **read-only status dock** (connection state, project/scene/selected node, recent command outcomes with timing) - registers the debugger plugin that powers live play-session inspection ::: info Under the hood The addon **dials out** to the server and reconnects, instead of hosting a server the MCP side connects to. The editor is the app that comes and goes, so this way launch order never matters. See [Bridge & transport](architecture/bridge.md). ::: ## Option A: Godot Asset Store (easiest) The addon is published as **Godot Editor MCP - Addon** on the [Godot Asset Store](https://store.godotengine.org/asset/john-d-studios/godot-editor-mcp-addon/). Install it into your project from there — or, inside Godot, open the **AssetLib** tab, search for **Godot Editor MCP - Addon**, and **Install**. ## Option B: copy into your own project (from source) ```bash cp -r godot-mcp/godot/addons/godot_mcp /path/to/your/game/addons/ ``` ## Option C: use the bundled project Open the `godot/` folder of the godot-mcp checkout as a project in Godot 4.4+. This is a minimal project that exists so the addon is loadable and testable. --- After installing by any method, enable the plugin: **Project Settings → Plugins → Godot Editor MCP - Addon**. The addon checks the editor version on enable and warns below 4.4. ## Point the addon at your server Same env var both sides — set it only if you deviate from the default: | Setting | Default | When to set | |---------|---------|-------------| | `GODOT_MCP_BRIDGE_URL` | `ws://127.0.0.1:9080` | Docker, remote server, or a non-default port | In the editor's environment (or via the project settings override), e.g. for Docker: ```bash GODOT_MCP_BRIDGE_URL=ws://127.0.0.1:9080 godot --path /path/to/your/game ``` ## Verify 1. The **status dock** appears at the bottom of the editor (alongside Output/Debug): connection dot, server/Godot version, bridge URL, active scene (marked `main.tscn ●` while the editor holds unsaved edits — #591), selected node, a **Last action** row (the most recent undoable change — "create_node 'Player' (Ctrl+Z to undo)" — #590), a **Playing** row (the running scene + whether the agent's runtime probe is attached — #591), and the **enabled toolsets** (fed by the server, #592), and an outcome-bearing recent-command log (✓ success, ✗ with the error code — #589) with timing, a per-session count, and a **Copy recent commands** button (#594). Reconnect attempts and server notices (auth refusal, a peer replacement) share the same log (#593), so a flapping or taken-over link explains itself. 2. Green dot = the addon connected out to the server. The server and editor can start in **either** order — the addon reconnects automatically. If a second editor takes over the bridge, the first shows a distinct **"Replaced by another editor"** state (purple dot) instead of a misleading green — its commands now go to the other editor. 3. `godot_health_check()` from your agent returns `{"bridge_connected": true, "version": "…"}`. ## Auto-refresh for external edits (optional) The editor's own external-file detection is **window-focus-driven** — files changed by an agent, git, or any other tool while Godot is unfocused stay invisible until you click into the editor. All files written *through* godot-mcp tools already refresh the editor's filesystem view automatically; for edits made outside the MCP path, enable the opt-in auto-refresh: ```bash GODOT_MCP_AUTO_REFRESH=1 GODOT_MCP_AUTO_REFRESH_INTERVAL=10 godot --editor ``` The **MCP dock** (bottom panel) has an **Auto-refresh files** checkbox — the runtime override of the env default. When on, the addon runs a periodic `EditorFileSystem.scan()` (never stacking a scan on an in-flight one) so external edits are picked up within one interval without focus. Next: [configure your MCP client](getting-started-clients.md). # Configure your MCP client ## OpenCode (stdio — the default path) `opencode.json` in the project root (or `~/.config/opencode/opencode.json`): ```json { "mcp": { "godot": { "type": "local", "command": ["uv", "run", "--directory", "/path/to/godot-mcp", "godot-editor-mcp"] } } } ``` Or with a PyPI install: ```json { "mcp": { "godot": { "type": "local", "command": ["godot-editor-mcp"] } } } ``` ## OpenCode (HTTP — shared service) When the server runs as a long-lived HTTP service (several clients, one editor): ```json { "mcp": { "godot": { "type": "remote", "url": "http://127.0.0.1:9090/mcp" } } } ``` With a token (non-loopback / Docker): ```json { "mcp": { "godot": { "type": "remote", "url": "http://:9090/mcp", "headers": { "Authorization": "Bearer " } } } } ``` ## Other clients Any MCP client works the same way — the server speaks standard MCP over stdio or Streamable HTTP: - **Any MCP client or agent harness**: register the server with the same `command` (stdio) or `url` (HTTP) shape in its MCP config. godot-mcp speaks standard MCP, so any client that drives MCP tools works — a coding agent, an IDE assistant, or your own orchestrator. ::: info Sessions are stateless godot-mcp serves the sessionless MCP `2026-07-28` protocol shape: no per-session state, and toolset grants are server-global. A reconnect keeps the grant — nothing to re-establish. ::: ::: warning Most clients don't refresh their tool list after a toggle `godot_enable_toolset` opens the gate server-side, but the server can only *announce* the change: `notifications/tools/list_changed` is a standard MCP notification, and whether the client re-runs `tools/list` is a **client choice** — the spec never makes re-listing a `MUST`. In practice **most MCP hosts cache the tool list at session start and ignore the notification**, so the newly-enabled tools never reach the model. Treat the gate as server state the agent *pulls*, not a push the client must honor. **If your client hits this: pre-enable the toolsets at server startup**, so the *initial* `tools/list` already contains them and no refresh is needed: ```jsonc // same MCP entry — add an env block to the server command { "mcp": { "godot": { "type": "local", "command": ["godot-editor-mcp"], "env": { "GODOT_MCP_DEFAULT_TOOLSETS": "scene_edit,scripts,physics,runtime" } } } } ``` `GODOT_MCP_DEFAULT_TOOLSETS` takes a comma-separated category list, or `all` for the full surface. It seeds the enabled set before the client's first `tools/list`, so the toolsets are present from the start. `enable_toolset` still works normally at runtime. ::: ## Which clients refresh their tool list? ::: info Under the hood On the sessionless `2026-07-28` protocol there's no standing server→client channel: a change notification reaches a client only if it opened a `subscriptions/listen` stream. Whether yours does is the reason this table exists — see [Toolset gating](architecture/gating.md) for the mechanism. ::: Surveyed October 2026. **Supports** = receives the change and re-runs `tools/list` without a reconnect; **partial** = a mechanism exists but is capability-gated or only some code paths use it; **does not** = caches `tools/list` at session start. | Client | Verdict | |--------|---------| | OpenCode | **supports** — re-lists every LLM step | | Google Gemini CLI | **supports** (buggy refresh path) | | Block Goose | **supports** | | Cline (VS Code extension) | **supports** | | VS Code + Copilot Chat | **supports** (can lag to the next turn) | | Claude Code (CLI) | **partial** — only subscribes when the server advertises `tools.listChanged: true` (see below) | | Zed | **partial / conflicting** | | Claude Code Desktop app | **does not** | | OpenAI Codex CLI | **does not** | | Cursor (IDE + CLI) | **does not** | | Continue | **does not** | | Roo Code | **does not** | | Cline (CLI) | **does not** | | Qwen Code | **does not** | | LibreChat | **does not** | | Claude Desktop · Windsurf/Devin Desktop · Open WebUI · Jan · Msty · Chatbox | **unknown** | | Python / TypeScript SDK · FastMCP client | **partial** — surface the event; re-listing is the caller's job | Transport compounds this: **stdio is the best-tested path**, while Streamable-HTTP has seen more refresh regressions. The local stdio default is the safer configuration. ### The advertisement gotcha A capable client (Claude Code's CLI) subscribes only when the server's handshake advertises `capabilities.tools.listChanged: true`. On the `2026-07-28` sessionless protocol MCP derives that flag from whether the server serves a `subscriptions/listen` stream. godot-mcp now serves it — it registers the mcp SDK's `ListenHandler` on FastMCP's low-level server, the same wiring mcp's own `MCPServer` uses — so the modern `server/discover` advertises `tools.listChanged: true` and a subscribing client receives `notifications/tools/list_changed` after `godot_enable_toolset` ([#620](https://github.com/hybridindie/godot-mcp/issues/620)). Clients that ignore the notification or never open a listen stream still need the pre-load workaround above. Beyond the push, `godot_enable_toolset` returns the enabled tools' names, descriptions, and input schemas ([#621](https://github.com/hybridindie/godot-mcp/issues/621)), so the agent can use them even when its registry never refreshes. ## Sanity check Just ask your assistant, in plain language: *"is Godot connected, and what's available?"* It calls `godot_health_check` and `godot_get_server_info` for you — **you never type these yourself.** If it reports `bridge_connected: false`, open Godot with the addon enabled, check the status dock, then ask again. Next: [your first session](getting-started-first-session.md). # Your first session godot-mcp is driven **through an AI assistant**, not by typing tool names. So the single most useful thing you can do first is **install the skill** — a small bundle of knowledge your assistant loads automatically. It teaches it to: - check the bridge and version before it starts, - **enable the toolset it needs, by itself** (you never manage gates), and - read the "what actually happened" fields and avoid the Godot pitfalls that trip agents up. → **[Install the skills](reference-skills.md)** (`./scripts/install-skills.sh`), then restart your assistant. From then on it just knows how to work in Godot. With the skill in place you talk in plain language and supervise in Godot. The rhythm is always the same: **orient → enable → preview → act → verify.** ::: warning No skill installed? The workflow degrades gracefully without it, but the assistant won't know to enable toolsets on its own or to read the honesty fields — you'll be prompting it about "unknown tool" errors and stale-save verdicts yourself. Install the skill. ::: ## 1. Orient — ask who's connected You: *"Check that Godot is connected and tell me what's available."* The assistant confirms the editor is reachable and tells you what it can do. If it says the bridge isn't connected, open Godot with the [addon enabled](getting-started-addon.md), check the status dock (bottom panel), and ask again — its report usually names the fix. ## 2. Enable what you need — the assistant handles it Ask for anything that needs a gated toolset and the assistant turns it on itself — the part the getting-started skill teaches, so you don't have to think about it. You: *"Add a Player node to the main scene."* If your assistant instead stalls on an "unknown tool" error, it's missing the skill (or hasn't enabled the toolset) — prompt it: *"enable the toolset you need first"*, or [install the skill](reference-skills.md). ## 3. Inspect before mutating — the assistant reads, then edits You: *"What's in the scene right now?"* The assistant reads the scene tree, your current selection, and the project setup (autoloads, input actions, main scene). Node paths come back scene-relative (`"."`, `"Player/Weapon"`) in exactly the form the editing tools expect — and you can see the same thing live in Godot's Scene dock. ## 4. Preview, then act — you approve, the assistant mutates You: *"Create the Player node — preview it first."* Ask for a preview and the assistant shows what *would* happen, running the same checks the real run will — including whether a save would keep it — before anything changes. When it goes ahead, the change lands in **your** editor: watch it appear in the Scene dock, and press **Ctrl+Z** to undo it. Every assistant change is on the editor undo stack — as revertible as your own edits. ## 5. Verify — ask the assistant to prove it worked You: *"Check the script parses and the scene runs without errors."* A clean parse is **not** full verification — runtime mistakes only show up when the game actually runs. Ask for the verdict, not just the action: *"does it run clean?"* is a different question from *"did you save it?"* ## Reading the results the assistant reports back ::: info Under the hood These fields are the server's **honesty contract** — what a save will *really* keep, what a batch actually ran, whether a check raced a rescan. See [Persistence truth](architecture/persistence.md) for how they're produced. ::: The assistant's results carry **honesty fields** that read as ground truth — check them (or ask the assistant to explain them): - `persisted: true` → the save will keep the change. - `persisted: false, reason: instanced_child_not_editable` → the edit shows in the editor but **won't be saved**; act on the hint (ask your assistant to enable Editable Children on the instanced scene, or to target a node the scene owns). - `rescan_pending: true` on a parse check → the editor's file scan hadn't flushed; have the assistant re-check once before "fixing" correct code. - `aborted_at` on a batch → trailing commands did **not** run. A well-behaved assistant reads these and acts; ask it to when it doesn't. ## When something fails | What you see | What it means | What to tell the assistant | |--------------|---------------|----------------------------| | "unknown tool" | the toolset isn't enabled | "enable the toolset first" | | a "no scene open" precondition | nothing is open to edit | "open or create a scene" | | a destructive action asks to confirm | it needs your OK first | review it, then "confirm and retry" | | "Godot is not connected" | the editor isn't reachable | open Godot + addon; check the status dock | | a typo'd key or path is rejected | a "did you mean…?" is offered | "use the suggested key" | The assistant's own report (and the `troubleshoot` prompt) usually names the fix. ## Making it smoother - **Reach for prompts.** The server's [workflow prompts](reference-prompts.md) are step-numbered recipes the assistant can follow for a whole task — a skill points it at the right one. - **Start from an example.** The two games under [examples](guides-examples.md) give the assistant a real project to work against. Next: [Build a scene](guides-build-scene.md) walks the full loop, or [Play-test & debug](guides-playtest-debug.md) starts from the runtime side. # Remote & multi-client godot-mcp's HTTP transport is built for remote/web front-ends: run the server next to your editor, expose it with a bearer token, and drive it from a phone or laptop via [OpenWebUI](https://openwebui.com) — or any MCP Streamable HTTP client. ## 1. Run the server over HTTP with a token ```bash GODOT_MCP_TRANSPORT=http GODOT_MCP_HTTP_HOST=0.0.0.0 \ GODOT_MCP_AUTH_TOKEN= uv run godot-editor-mcp ``` Non-loopback binds **require** a token (fail-fast, #226). The addon still connects to the bridge on `ws://:9080`. ## 2. Godot + addon Open your project with the addon enabled; the MCP dock should show *connected* (green). For a separate machine, point `GODOT_MCP_BRIDGE_URL=ws://:9080` at the server. ## 3. Register in OpenWebUI OpenWebUI ≥ 0.11 speaks MCP Streamable HTTP. 1. **Settings → Tools → Integrations → External tool servers → +** 2. Switch the type from OpenAPI to **MCP Streamable HTTP**: - **URL**: `http://:9090/mcp` - **Auth**: Bearer → paste the same `GODOT_MCP_AUTH_TOKEN` - **ID**: a short, simple name (e.g. `godot`) — see the caveat below 3. Test the connection (the round-trip arrows) and save. ## 4. Enable function calling per model Settings → Models → edit the model → Advanced Params → **Function calling = `native`**. OpenWebUI's `default` mode never emits tool calls — this is the most common "tools don't work" cause. Verified with small local models from the `qwen2.5-coder`, `gemma`, and `llama3.1` families. ## 5. Try it In a chat, enable the `godot` tool (wrench icon) and ask: > *"show the scene tree from the current connected project"* ## Caveats - **Tool-name prefixing.** OpenWebUI prefixes every tool with the integration ID (`godot_inspection_get_scene_tree` → `_godot_inspection_get_scene_tree`). Keep the ID short, or instruct the model to ignore the prefix — small local models otherwise try to call tools under their literal prefixed names. - **Toolset gating.** Only `core` + `inspection` are exposed by default; for scene editing the agent must first call `godot_enable_toolset("scene_edit")` — or prompt it: *"enable the scene_edit toolset, then add a button below the Quit button"*. ## Security The token is the whole transport gate: an unauthenticated network endpoint could write files, run arbitrary GDScript, and export builds through your editor. The server validates it via `StaticTokenVerifier`; generate any random string (`openssl rand -hex 32`) and keep it out of the repo. ## Guides # Guides Task-by-task recipes that show **what to ask** — and, **under the hood**, what your assistant does at each step. You prompt in plain language and watch it happen in Godot; the `godot_*` calls are the assistant's, never yours. - **[Build a scene](guides-build-scene.md)** — scaffold → nodes → script → collision → save, with previews and honest save verdicts along the way. - **[Play-test & debug](guides-playtest-debug.md)** — the live loop: play the game, inspect it, simulate input, assert state, then break and step through the debugger. - **[Verify your work](guides-verify.md)** — the verification stack: what each check catches, what it misses, and what to compose for real confidence. - **[Ship: export, tests & CI](guides-ship.md)** — headless export, running the project's test suite, and the CI-shaped verification loop. - **[Examples](guides-examples.md)** — two complete games under `examples/`, including the debugger demo, to run the toolsets against. New here? Start with **[your first session](getting-started-first-session.md)**. # Build a scene The canonical build loop. You prompt in plain language; the assistant makes the editor calls and hands you the honest verdicts. ## 1. Scaffold You: *"Scaffold a main scene."* The assistant enables scene editing and creates a root scene. The root must be a **built-in** node type (e.g. `Node2D`) — a custom `class_name` is refused, because the editor can't instantiate script classes. To root a scene on a custom class, have the assistant create it with the base built-in type, then attach the script to the root. ## 2. Compose the tree You: *"Add a Player with a visible Body shape."* The assistant adds the nodes and sets their properties — node types, names, and values — all in your live scene. For several independent operations, ask it to **batch** them: many commands run in a single editor round-trip (the editor drains each in about a frame), which is far faster than one call per node. ## 3. Attach behavior You: *"Give the Player a movement script."* Script writes flush straight to disk (no scene save needed), so the assistant writes the script, attaches it, and the behavior is live in your editor immediately. Scene *edits*, by contrast, stay in the editor's memory until the scene is saved (step 5). ## 4. Physics You: *"Give the Player collision so it stands on floors."* The assistant sets the collision shape, body, and layers. Godot types arrive as plain JSON — a `Vector2` size is `{"x": 32, "y": 48}` — and it dimension-checks the shape against the body up front: a 2D shape on a 3D body is refused with a naming hint, instead of silently shipping collisionless levels. ## 5. Verify, then save You: *"Check the script parses, then save the scene."* A clean parse is necessary but **not sufficient** — runtime API misuse only shows up when the game actually runs, so ask for a headless run too (see [Verify your work](guides-verify.md)). Then save. ## Reading the persistence verdicts Every mutation result carries a **persistence verdict** — whether the save will really keep the change: - `persisted: true` → the save keeps it. - `persisted: false, reason: instanced_child_not_editable` → the target is inside an instanced scene. Fix: ask the assistant to enable Editable Children on that scene (then re-apply), or to target a node the scene owns. - `persisted: false, reason: node_not_owned` → the node has no owner (e.g. added by a `@tool` script); re-create it through the tools so the owner lands. - Batch results: read `persistence[]` per target, and `undoable`. ## Groups & signals You: *"Put the Player in the `player` group and connect the Enemy's `died` signal."* Both are undoable, and a signal connection that already exists reports `already_connected` instead of failing on the duplicate. ## Undo Ask the assistant to undo and it steps back the last editor action. Large batches above 20 nodes bypass the undo stack (the result says `undoable: false`) — plan those. Next: [Play-test & debug](guides-playtest-debug.md), or [Verify your work](guides-verify.md). # Play-test & debug The live loop against a real running game. You ask in plain language; the assistant does the rest. Requires the `runtime`, `input`, `testing`, and `debugger` toolsets plus the `MCPRuntimeProbe` autoload in the game's project. ## Two run modes — pick the right one ::: info Under the hood A **headless** run spawns a separate Godot process — a quick "does it even boot" smoke, no editor, no probe. A **live play session** drives the game *inside* the running editor, so you can inspect and steer it. The rest of this page is the live loop. ::: | Mode | Toolset | Answers | |------|---------|---------| | Headless smoke test | `runtime` | "does it run at all" — no editor play session, no probe | | Live play session | `runtime` + `input` + `testing` + `debugger` | interactive inspection, input, assertions, breakpoints | For a one-call report that covers the headless side, just ask the assistant to "run the debug workflow" — that needs no extra toolset. ## 1. Register the runtime probe You: *"Set up live runtime inspection — add the probe autoload if it's missing."* The assistant checks the project's autoloads and, if the probe isn't registered, adds it. The probe runs **in the game** (not the editor) and no-ops in exported builds. ## 2. Play and inspect You: *"Play the main scene and show me the Player's position over time."* The assistant starts a live play session, then can read the running game's real hierarchy (not the editor's copy), sample a property over frames, find UI by its text, and read performance monitors. Pending reads carry a `reason` token (e.g. `capture_pending`) — the assistant keeps polling rather than re-issuing. ## 3. Simulate input You: *"Hold right, release, then press Space."* The assistant synthesizes key, mouse, and action input into the running game, can replay a recorded sequence or record a macro, and reports how many events the game acknowledged. While the game is **paused at a debugger break, input is refused** — the assistant continues execution first. A frozen injected count is the pause signal, not a bug. ## 4. Assert state You: *"Assert the Player is visible, then fuzz it with random input."* The assistant asserts live node state, runs a scripted scenario (a sequence of events plus assertions), fuzzes with random input, and compares screenshots — so behavior is checked, not just "it didn't crash". ## 5. Break and step You: *"Set a breakpoint on player.gd line 42 and step through it."* The assistant sets a breakpoint; breakpoints fire on their own line. A forced break lands on the probe's next frame (no game cooperation needed). Once stopped, it can step over/into/out, evaluate an expression in the running game (`evaluate_expression` — global scope, not frame-scoped), and continue. The paused-frame stack / variable dump (`get_stack_frames`, `get_frame_variables`) is **not available on Godot 4.7** (#677). ## See the scene (screenshots) You: *"Take a screenshot so you can see the current layout."* The assistant can **look at** the scene, not just read it — for clients that accept images. Two captures, and you pick by what you want to see: | Tool | Shows | Needs | |------|-------|-------| | `godot_editor_capture_screenshot` | the **editor viewport** — the scene tab, layout, and gizmos you're looking at | the `editor` toolset; the editor must be drawing a frame | | `godot_runtime_capture_game_screenshot` | the **running game's rendered frame** — the actual pixels the player sees | a live play session (`runtime`) + the probe | Both come back as a base64 PNG image block over the *same* bridge round-trip: the addon grabs `get_viewport().get_texture().get_image()` and returns it, and the server hands it to the model as an image. Honest caveats — if the editor window isn't drawing (minimized or unfocused) the editor capture reports `editor_not_drawing`; a capture taken while the game is **paused at a breakpoint shows the frozen frame**, not a fresh one. Captures are `read_only`. Use it to answer *"does this actually look right?"* — layout, HUD placement, a sprite that isn't showing — then diff two frames with `godot_testing_compare_screenshots` to catch a regression. ## Debugging failures | Symptom | First move | |---------|-----------| | Crash on play | ask for a headless run and read the errors/warnings | | `timed_out: true` | not a crash — games that never self-quit always time out; tell the assistant the timeout is expected | | Script "won't parse" | ask for a parse check; if it flags `rescan_pending`, re-check before "fixing" | | Input does nothing | is the game playing? is the probe registered? a full-screen overlay eating input? paused at a break? | | Stuck | ask the assistant for the server info's `next_steps`, or use the `troubleshoot` prompt | Ask the assistant to stop the play session when you're done. # Verify your work Ask the assistant to *prove* an action landed, not just perform it. "Verified" means different things at different layers — this page is the honest map: what each check covers, what it **misses**, and what to compose for real confidence. ## The stack | Check | Tool (class) | Catches | Misses | |-------|--------------|---------|--------| | Parse check | `godot_scripts_get_parse_errors` (`read_only`) | GDScript **syntax** errors, with line numbers | class-API misuse (`_rng.get_noise_1d` parses clean, fails at runtime); a stale cache is flagged with `rescan_pending` | | Shader compile | `godot_shader_validate` (`read_only`) | everything the engine's compile rejects: invalid uniform hints, bad `render_mode` | runtime shader behavior (the visual output) | | Headless run | `godot_runtime_run_and_capture` (`runtime`) | runtime errors, warnings, exit codes — the **full** verification for scripts | input-dependent behavior (no play session) | | One-call report | `godot_debug_workflow` (`core`) | parse + tree + run + bridge in one result | the same misses as its parts | | Persistence verdicts | every mutation | what the save will keep (`persisted`/`reason`) | — | | Play session | `godot_runtime_play_scene` + assertions | actual gameplay, input, live state | — | ## The recommended pattern 1. **Write** via `scripts_write`/`patch_script` — it flushes to disk immediately. 2. **Parse** — fix syntax errors first. 3. **Compile-check** shaders — e.g. `uniform float edge_width : float` parses and byte-reads clean; only the engine's compile catches it. 4. **Run** the scene headlessly and read the errors/warnings. A `timed_out: true` is **not** a crash (games that never self-quit). 5. **Play** for interactive behavior — input simulation plus assertions. ## Why "verification" is stated this narrowly The advertised verification used to give false confidence (#423): a VFX subagent verified a shader via byte-equality and scripts via empty parse errors — then 2292 runtime errors appeared at first boot. The fix: the shader check closes the shader half, and the parse check documents its scope honestly — the runtime half is what actually caught everything. ## The one-call diagnostics You: *"Run the full diagnostics on the main scene."* ::: info Under the hood The assistant runs one call that returns parse + tree + run + bridge together, plus a `findings` list that reads as ground truth — `RUNTIME ERRORS: n`, `RUN TIMED OUT`, `NON-ZERO EXIT` (only for a real crash), `PARSE ERRORS: n`. For a game that legitimately runs until killed, tell the assistant the timeout is expected. ::: ## Honesty fields as verification - `persisted` — will the save keep it? (act on the hint when false) - `undoable` — can the batch be undone? - `aborted_at` / `skipped_count` — did the trailing commands run? - `rescan_pending` — was this check racing the importer scan? - `scanning` — is the import status provisional? ## CI integration The full verification loop runs headless (no editor): run + test + export are the CI-shaped steps; the live-editor e2e suite exercises the rest on a real Godot binary. The addon's own logic is pinned the same way — headless `SceneTree` exercisers under `godot/tests/` run via pytest whenever a Godot binary is present (issue #524). # Ship: export, tests & CI The end of the loop: verification runs headless (no editor needed), so the whole build-test-ship path works in CI. You ask in plain language; the assistant does the rest. ## Export a build You: *"Export a Web build."* The assistant lists your project's export presets, checks the one you want, and runs it. Export is headless (Godot binary + export templates installed) and reports the exit code — a non-zero exit is a finding, not a hang. Requires the `export` toolset. ## Run the project's tests You: *"Run the project's unit tests."* The assistant runs the suite and parses the output — it understands GUT, including GUT 9.7's quirk of omitting the "Failing Tests" line when everything passes — and reports the pass/fail counts (or `framework_absent: true` when no test framework is installed). GUT-specific gotchas (from the expert skill): don't `await` `node.ready` on simple nodes, don't instantiate heavy scenes in `before_each`, reset autoload state between tests. ## CI-shaped verification loop ::: info Under the hood Every step returns structured results an agent or a script can gate on — no human reading logs. The one-call debug workflow collapses the first three. ::: The full headless loop — no live editor, no probe: 1. **Parse** every changed `.gd` for syntax errors. 2. **Compile-check** every changed `.gdshader`. 3. **Run** the scene headlessly and read its errors and warnings — a timeout is not a crash. 4. **Test** — run the project's own suite. 5. **Export** — produce the build artifact, with an exit-code verdict. Each step returns structured results an agent (or a script) can gate on; ask the assistant to "run the debug workflow" to collapse the first three into one findings report. ## Transport notes for CI - **stdio** (default): each CI job spawns its own server — nothing to share. - **http**: one service can carry several concurrent jobs' verification runs, but the editor bridge is single-connection — for CI prefer stdio per job. - Headless runs need a Godot binary: `GODOT_BIN` (or `godot` on PATH). The parse/compile checks use the same binary; version 4.4+ (4.7 validated). - A headless run is a detached subprocess — it never blocks the bridge, so a long-running scene times out on its own timer, not the bridge's. # Examples godot-mcp ships two complete Godot 4.x games under [`examples/`](https://github.com/hybridindie/godot-mcp/tree/main/examples). They are the reference testbeds: real projects, built through the MCP tools, that exercise most of the surface end-to-end — and double as worked examples of the build → verify loop the [guides](guides.md) describe in the abstract. Both are **geometry-and-color only** (no external art assets), so they are small, deterministic, and safe to diff. ## `examples/survivors/` — the from-scratch testbed A survivors-style game built **entirely through the MCP tools** — the canonical example of the whole authoring path. 60 GUT tests across 8 files. **Game:** player (WASD movement, health/XP/leveling), enemies (chase AI, per-wave scaling), projectiles (auto-fire at nearest enemy), XP gems (magnet pickup), a 40×40 checkerboard world with deterministic scattered obstacles, and a HUD. **Demonstrates:** | Feature | Toolset(s) | |---------|-----------| | Scene tree, nodes, properties | `scene_edit` | | GDScript authoring + attach | `scripts` | | Character bodies, collision layers/masks | `physics` | | Checkerboard grid + obstacle pool | `tilemap` / `scene_edit` | | HUD bars, labels, overlays | `theme_ui` | | Autoload game manager + state machine | `resources_edit`, `scripts` | | GUT unit tests (60) | `testing` | Run its tests — You: *"Run the survivors unit tests."* The assistant runs the suite and reports the pass/fail counts (the `testing` toolset detects GUT automatically — `framework_absent: true` when it isn't installed). Or run it yourself: ```bash godot --headless --path examples/survivors \ -s addons/gut/gut_cmdln.gd -gexit ``` ## `examples/vampire/` — the comprehensive coverage demo The original Vampire Survivors-style demo, kept as the broadest single project for toolset coverage — including the **debugger** demonstration. **Game:** player + health/XP/leveling, projectile and area weapons, enemies with health bars and wave scaling, an upgrade menu, particles (blood burst, pickup sparkle), and a `DebuggerDemo` node that trips a real `breakpoint` on input. **Debugger showcase:** press `Space` (or click in the game window) to hit the built-in breakpoint, then drive the `debugger` toolset while paused — `godot_debugger_evaluate_expression(expression=…)` (in the running game's global scope; the paused-frame stack/vars are not exposed on 4.7, #677), `godot_debugger_step_into()`, then `godot_debugger_continue_execution()`. See [Play-test & debug](guides-playtest-debug.md) for the full loop. **Toolset coverage** (from its README): `scene_edit`, `physics`, `scripts`, `theme_ui`, `particles`, `tilemap`, `runtime`, and `debugger`. ## How to use an example Open either project folder in Godot 4.4+ with the godot-mcp addon installed, then treat it as a target. You prompt in plain language; the assistant makes the calls — the rhythm from [Your first session](getting-started-first-session.md). **A few first things to ask:** - **"Read the scene before you change it."** — the assistant fetches the scene tree and the survivors player's properties; every path it sees is call-ready. - **"Set the Player's speed to 300 — but preview it first."** — the preview carries the same save verdict the real run will. - **"Break into the debugger and step through it."** — on `vampire`, trigger the debugger demo and read the stack, exactly as in [Play-test & debug](guides-playtest-debug.md). - **"Run the survivors tests."** — the suite is a live GUT fixture for the `testing` toolset, including the pass/fail parsing. ## Why these exist They are the **drift catchers**. Because they are real projects driving real tools, running the toolsets against them surfaces integration failures a unit test can't: a renamed parameter, a broken `.tscn` round-trip, a physics-layer regression. A consumer's live eval suites can drive the same projects as its integration checks. ## Concepts # Why & how it works Two minutes here and the rest of the docs will click. ## The problem An AI agent can already edit Godot files on disk — but it edits **blind**: - it can't *see* the live scene (instanced scenes, `owner` relationships, the open tab, the selection, unsaved edits); - it can't *run* anything, so "does this work?" is unanswerable without a human pressing play; - its file writes race the editor's importer, so its next check can read a stale class cache; - a mistake has no undo unless the editor's own stack was involved. godot-mcp closes that gap: the agent talks to the **live editor** over MCP, so every action lands on the project you're actually looking at — with undo, verification, and honest reporting. ## Why it's built this way (the short version) - **Editor-first, not file-first.** The editor owns state the disk doesn't — unsaved changes, `owner`, editable-instance flags, the selection, the importer cache. Every mutation goes through the editor's own APIs (`EditorUndoRedoManager`, `EditorInterface`, `ProjectSettings`), so *your* undo stack covers the agent's actions and the editor's own rules apply. - **The server listens; the editor connects out.** The editor is the app that comes and goes, so it's the one that dials and reconnects (with backoff). The server can start before Godot, before the agent's first call, or in Docker — launch order never matters. The direction of commands is unchanged: the server still initiates every one. - **Safety lives in the server, not the addon.** The addon only executes; it never decides whether an action is allowed. Safety classes, previews, `confirm` gates, and preconditions all live in `mcp_server/safety.py`, so a misbehaving addon can't bypass the confirm gate. - **Honest results over optimistic ones.** A result that *reads* like success but isn't is the worst failure mode for an agent with no human watching. So nothing ever says "done" without saying what happened to disk (see the honesty fields below). - **Game-agnostic by design.** No game vocabulary ships here. A game-specific project layers its own tools and prompts on top; anything that only makes sense for one game belongs there, not here. ## The ideas every page assumes ### 1. The toolset (the gating unit) Every tool carries **exactly one category tag**. `core` (diagnostics and toolset management) is always on; everything else ships **gated off**. The agent switches one on with `godot_enable_toolset("scene_edit")`, which updates a server-global enabled set and announces the change (`notifications/tools/list_changed`, plus a `subscriptions/listen` stream on the `2026-07-28` protocol). Because many clients ignore the notification, the enable result also carries the enabled tools' names and input schemas, so even a client that never refreshes can call them. Why gate at all? 194 tools in the context window degrade tool-selection and burn tokens. The default surface (`core` + `inspection` — 24 tools) is enough to orient and inspect; the agent pulls in `scene_edit` only when it means to edit. `godot_list_toolsets()` is always authoritative. A few toolsets are version-gated (`scene_edit`, `input_map`, `tilemap`, `scene_3d` need Godot 4.4+); enabling one on an older editor returns a structured `PRECONDITION_FAILED … [required=godot_version]`. ### 2. The safety class Every tool is one of: | Class | Meaning | Extra params | |-------|---------|--------------| | `read_only` | Never mutates | none | | `mutating` | Reversible editor change (UndoRedo) | `dry_run: bool = False` | | `destructive` | Deletes/overwrites; maybe irreversible | `dry_run` **and** `confirm: bool = True` | | `runtime` | Controls game execution | varies | `dry_run=True` runs the checks and returns what *would* happen — nothing is sent. `confirm=True` is required for destructive tools; without it the tool returns `PRECONDITION_FAILED … [required=confirm]`. `godot_undo` covers every UndoRedo-wrapped change (batches above 20 nodes bypass the stack and say so via `undoable: false`). ### 3. The envelope (and why errors look like this) ```jsonc // server → addon (command) { "id": "42", "command": "cmd_create_node", "params": { "parent_path": ".", "node_type": "Node2D", "name": "Player" } } // addon → server (response) { "id": "42", "ok": true, "result": { "node_path": "Player", "created": true } } ``` Failures never cross as stack traces: ```jsonc { "id": "42", "ok": false, "error": "PRECONDITION_FAILED", "hint": "Open a scene before creating nodes.", "required": "active_scene" } ``` `error` is from a fixed enum; `hint` tells the agent what to do next; `required` names the precondition to satisfy. The server shapes these into FastMCP `ToolError`s, so the agent sees the same structured failure on every transport. Almost every response is JSON like the above. The one exception is a screenshot: the same round-trip returns PNG bytes that the server hands the model as an image, so the agent can *look at* the editor viewport or the running game — see [Seeing the scene](guides-playtest-debug.md#see-the-scene-screenshots). ### 4. Naming: `godot__` The exposed name is the gating toolset made visible: `create_node` in the `scene_edit` toolset is `godot_scene_edit_create_node`. The prefix *is* the gate — an agent can read the name and know what it must enable. Always-on `core`/meta tools are just `godot_` (`godot_health_check`, `godot_enable_toolset`). The mapping is applied centrally and enforced by a contract test. ### 5. Honesty fields Results carry structured self-reporting that reads as ground truth: | Field | On | Meaning when it matters | |-------|----|-------------------------| | `persisted` + `reason` + `hint` | most mutations | whether a scene save will keep the change; `instanced_child_not_editable`, `node_not_owned`, `embedded_in_other_resource`, `group_from_base_scene` are the reason tokens | | `undoable` | `batch_set_property` | `false` above 20 nodes — undo will **not** revert the batch | | `aborted_at`, `skipped_count`, `hint` | `run_commands` | which sub-command stopped the batch and how many never ran | | `may_have_applied` | any mutating/`destructive` tool, on `TIMEOUT` | the timed-out mutation may still run on the editor (it drains commands serially) — re-read state before retrying, don't double-apply (#644) | | `rescan_pending` | `get_parse_errors` | the editor's rescan hadn't flushed; a fresh-class_name error may be stale cache | | `reason` (readiness) | poll-based tools | why the tool is still pending (`capture_pending`, `scan_in_flight`, …) | | `scanning` | `asset_import_get_status`, `rescan_filesystem` | the editor's filesystem scan is in flight | ### 6. Contract versioning `godot_get_server_info` carries `contract_version` (a breaking-change counter, currently **1**) and `min_compatible_contract`. A client is compatible when `min ≤ client ≤ contract`. Additive changes (new tools, optional fields) never bump it; removals and renames do. The CalVer build version (`2026.10.15`) moves every release and is *not* the compatibility signal. --- Deep dive next: **[Under the hood](architecture/index.md)** — the four-layer chain, the bridge, and the full contract. ## Reference # Reference ::: info For readers who know MCP This is the technical track. New here and just want to make a game? → **[Set up with your AI agent](getting-started-with-an-agent.md)**. ::: Authoritative tables for the surface. The per-tool contract spec (full result schemas, every field) remains [`docs/tool-contracts.md`](https://github.com/hybridindie/godot-mcp/blob/main/docs/tool-contracts.md) in the repo — it is the source of truth; these pages are the readable map. - [Toolsets & tools](reference-toolsets.md) — every toolset, every tool - [Safety classes](reference-safety-classes.md) — class × tool matrix, `dry_run`/`confirm` semantics - [Value shapes](reference-value-shapes.md) — how Godot types arrive as JSON - [Errors & recovery](reference-errors.md) — the error-code enum + the recovery table - [Prompts & resources](reference-prompts.md) — the 9 workflow prompts + the `godot://` resources - [Skills](reference-skills.md) — the 3 installable AI skills + the expert reference guides - [Consumer integration](reference-consumers.md) — layering game-specific agents on top (skills, orchestrator pattern) - [Env vars](reference-env-vars.md) — every configuration knob - [Changelog](changelog.md) — what shipped in each release ::: info For LLM agents The site serves [`llms.txt`](https://hybridindie.github.io/godot-mcp/llms.txt) (page index) and [`llms-full.txt`](https://hybridindie.github.io/godot-mcp/llms-full.txt) (everything as one markdown document) — point agents at those instead of crawling HTML. ::: # Toolsets & tools 194 tools across 29 categories. Only `core` + `inspection` are enabled by default; enable the rest with `godot_enable_toolset(category)`, which returns the enabled tools' names and input schemas so a client that never refreshes its registry can still call them. `category` also accepts a bundle (`author` / `playtest` / `ship`) or a comma-separated list, so a whole workflow is one call (#653). Class meanings: [safety classes](reference-safety-classes.md). ## `analysis` — Static analysis: dependencies, signal flow, unused resources, circular deps, scene integrity. 9 tools. | Tool | Class | |------|-------| | `godot_analysis_analyze_dependencies` | read_only | | `godot_analysis_analyze_signal_flow` | read_only | | `godot_analysis_cross_scene_find_refs` | read_only | | `godot_analysis_detect_circular_dependencies` | read_only | | `godot_analysis_find_orphaned_resources` | read_only | | `godot_analysis_find_unused_resources` | read_only | | `godot_analysis_project_stats` | read_only | | `godot_analysis_project_structure` | read_only | | `godot_analysis_validate_scene_integrity` | read_only | ## `animation` — AnimationPlayers and AnimationTrees: author animations, tracks, keyframes, state machines. 8 tools. | Tool | Class | |------|-------| | `godot_animation_add_state_machine_state` | mutating | | `godot_animation_add_track` | mutating | | `godot_animation_create` | mutating | | `godot_animation_create_tree` | mutating | | `godot_animation_get` | read_only | | `godot_animation_insert_keyframe` | mutating | | `godot_animation_list_animations` | read_only | | `godot_animation_set_blend_tree_node` | mutating | ## `asset_import` — Import external assets (.png/.glb/.wav/…), create materials from textures, poll import status. 3 tools. | Tool | Class | |------|-------| | `godot_asset_import_asset` | mutating | | `godot_asset_import_create_material_from_textures` | mutating | | `godot_asset_import_get_status` | read_only | ## `audio` — Audio players and AudioServer buses: add players, route buses, effects — with a verifiable layout read. 6 tools. | Tool | Class | |------|-------| | `godot_audio_add_bus` | mutating | | `godot_audio_add_bus_effect` | mutating | | `godot_audio_add_player` | mutating | | `godot_audio_get_bus_layout` | read_only | | `godot_audio_remove_bus` | destructive | | `godot_audio_remove_bus_effect` | destructive | ## `batch` — Bulk operations: find nodes by type, batch property sets, cross-scene edits, dependency analysis. 4 tools. | Tool | Class | |------|-------| | `godot_batch_cross_scene_set_property` | destructive | | `godot_batch_find_nodes_by_type` | read_only | | `godot_batch_get_dependencies` | read_only | | `godot_batch_set_property` | mutating | ## `composite` — Macro round-trips: N editor commands in one bridge call, batch node creation, apply_node_edits. 4 tools. | Tool | Class | |------|-------| | `godot_composite_apply_node_edits` | mutating | | `godot_composite_batch_create_nodes` | mutating | | `godot_composite_compose_node` | mutating | | `godot_composite_run_commands` | mutating | ## `core` — Always-on: diagnostics, toolset management, undo, the debug_workflow recipe. 13 tools. | Tool | Class | |------|-------| | `godot_check_for_updates` | read_only | | `godot_debug_workflow` | read_only | | `godot_describe_class` | read_only | | `godot_disable_toolset` | read_only | | `godot_enable_toolset` | read_only | | `godot_get_server_info` | read_only | | `godot_health_check` | read_only | | `godot_list_history` | read_only | | `godot_list_tools_by_safety_class` | read_only | | `godot_list_toolsets` | read_only | | `godot_read_resource` | read_only | | `godot_redo` | mutating | | `godot_undo` | mutating | ## `debugger` — Breakpoints, force_break, stack frames/variables, expression evaluation, stepping. 11 tools. | Tool | Class | |------|-------| | `godot_debugger_clear_breakpoints` | runtime | | `godot_debugger_continue_execution` | runtime | | `godot_debugger_evaluate_expression` | runtime | | `godot_debugger_force_break` | runtime | | `godot_debugger_get_frame_variables` | runtime | | `godot_debugger_get_stack_frames` | runtime | | `godot_debugger_remove_breakpoint` | runtime | | `godot_debugger_set_breakpoint` | runtime | | `godot_debugger_step_into` | runtime | | `godot_debugger_step_out` | runtime | | `godot_debugger_step_over` | runtime | ## `editor` — Editor screenshots (base64 PNG) for vision-capable clients. 1 tools. | Tool | Class | |------|-------| | `godot_editor_capture_screenshot` | read_only | ## `export` — Export presets and headless export runs. 3 tools. | Tool | Class | |------|-------| | `godot_export_get_info` | read_only | | `godot_export_list_presets` | read_only | | `godot_export_project` | runtime | ## `input` — Input simulation and recording for a live play session. 7 tools. | Tool | Class | |------|-------| | `godot_input_get_stats` | read_only | | `godot_input_play_sequence` | runtime | | `godot_input_record` | runtime | | `godot_input_simulate_action` | runtime | | `godot_input_simulate_key` | runtime | | `godot_input_simulate_mouse` | runtime | | `godot_input_stop_recording` | runtime | ## `input_map` — Input Map actions in project settings: add/remove actions and events. 5 tools. | Tool | Class | |------|-------| | `godot_input_map_add_action` | mutating | | `godot_input_map_add_event` | mutating | | `godot_input_map_clear_action_events` | destructive | | `godot_input_map_get_action_events` | read_only | | `godot_input_map_remove_action` | destructive | ## `inspection` — Read-only project/scene/node inspection. 11 tools. | Tool | Class | |------|-------| | `godot_inspection_diff_snapshots` | read_only | | `godot_inspection_get_active_scene` | read_only | | `godot_inspection_get_node_groups` | read_only | | `godot_inspection_get_node_properties` | read_only | | `godot_inspection_get_node_property` | read_only | | `godot_inspection_get_node_property_list` | read_only | | `godot_inspection_get_project_info` | read_only | | `godot_inspection_get_scene_tree` | read_only | | `godot_inspection_get_selected_node` | read_only | | `godot_inspection_list_scenes` | read_only | | `godot_inspection_snapshot_subtree` | read_only | ## `navigation` — NavigationRegion/Agent setup, navmesh baking, navigation layers. 5 tools. | Tool | Class | |------|-------| | `godot_navigation_bake_mesh` | mutating | | `godot_navigation_get_region` | read_only | | `godot_navigation_set_layers` | mutating | | `godot_navigation_setup_agent` | mutating | | `godot_navigation_setup_region` | mutating | ## `particles` — GPUParticles2D/3D: create, configure materials, presets, color gradients. 5 tools. | Tool | Class | |------|-------| | `godot_particles_apply_preset` | mutating | | `godot_particles_create` | mutating | | `godot_particles_get_material` | read_only | | `godot_particles_set_color_gradient` | mutating | | `godot_particles_set_material` | mutating | ## `physics` — Physics bodies, collision shapes, raycasts, layer/mask management. 4 tools. | Tool | Class | |------|-------| | `godot_physics_add_raycast` | mutating | | `godot_physics_set_layers` | mutating | | `godot_physics_setup_body` | mutating | | `godot_physics_setup_collision` | mutating | ## `profiling` — Editor + running-game performance monitors (FPS, draw calls, memory). 2 tools. | Tool | Class | |------|-------| | `godot_profiling_get_editor_performance` | read_only | | `godot_profiling_get_performance_monitors` | read_only | ## `project` — Filesystem tree, file search, project settings, UID resolution. 7 tools. | Tool | Class | |------|-------| | `godot_project_delete_resource_file` | destructive | | `godot_project_get_filesystem_tree` | read_only | | `godot_project_get_setting` | read_only | | `godot_project_move_file` | mutating | | `godot_project_resolve_uid` | read_only | | `godot_project_search_files` | read_only | | `godot_project_set_setting` | destructive | ## `project_scaffold` — Scaffold a new project skeleton (directories, base scenes). 1 tools. | Tool | Class | |------|-------| | `godot_project_scaffold` | destructive | ## `resources_edit` — Author .tres resource files, register/unregister autoloads. 5 tools. | Tool | Class | |------|-------| | `godot_resources_edit_create_resource` | destructive | | `godot_resources_edit_read_resource_file` | read_only | | `godot_resources_edit_register_autoload` | destructive | | `godot_resources_edit_set_resource_property` | mutating | | `godot_resources_edit_unregister_autoload` | destructive | ## `runtime` — Headless run + output capture; live play session lifecycle. 11 tools. | Tool | Class | |------|-------| | `godot_runtime_capture_game_screenshot` | read_only | | `godot_runtime_find_ui_elements` | read_only | | `godot_runtime_get_game_output` | read_only | | `godot_runtime_get_game_scene_tree` | read_only | | `godot_runtime_get_property_samples` | read_only | | `godot_runtime_is_playing` | read_only | | `godot_runtime_monitor_property` | runtime | | `godot_runtime_play_scene` | runtime | | `godot_runtime_read_property` | read_only | | `godot_runtime_run_and_capture` | runtime | | `godot_runtime_stop_scene` | runtime | ## `scene_3d` — 3D scenes: meshes, cameras, lights, environments, GridMaps, MeshLibraries. 8 tools. | Tool | Class | |------|-------| | `godot_scene_3d_add_mesh_instance` | mutating | | `godot_scene_3d_add_mesh_library_item` | mutating | | `godot_scene_3d_create_mesh_library` | mutating | | `godot_scene_3d_gridmap_get_cell` | read_only | | `godot_scene_3d_gridmap_set_cell` | mutating | | `godot_scene_3d_setup_camera` | mutating | | `godot_scene_3d_setup_environment` | mutating | | `godot_scene_3d_setup_lighting` | mutating | ## `scene_edit` — Create/modify/delete nodes, scripts, signals, scenes — the core authoring surface. 24 tools. | Tool | Class | |------|-------| | `godot_scene_edit_add_to_group` | mutating | | `godot_scene_edit_attach_script` | mutating | | `godot_scene_edit_close_scene` | destructive | | `godot_scene_edit_connect_signal` | mutating | | `godot_scene_edit_create_node` | mutating | | `godot_scene_edit_create_scene` | mutating | | `godot_scene_edit_delete_node` | destructive | | `godot_scene_edit_disconnect_signal` | destructive | | `godot_scene_edit_duplicate_node` | mutating | | `godot_scene_edit_extract_scene` | mutating | | `godot_scene_edit_instance_scene` | mutating | | `godot_scene_edit_list_open_scenes` | read_only | | `godot_scene_edit_list_signal_connections` | read_only | | `godot_scene_edit_move_node` | mutating | | `godot_scene_edit_open_scene` | mutating | | `godot_scene_edit_reload_scene` | destructive | | `godot_scene_edit_remove_from_group` | mutating | | `godot_scene_edit_rename_node` | mutating | | `godot_scene_edit_rescan_filesystem` | runtime | | `godot_scene_edit_save_all_scenes` | mutating | | `godot_scene_edit_save_scene` | mutating | | `godot_scene_edit_select_nodes` | mutating | | `godot_scene_edit_set_editable_children` | mutating | | `godot_scene_edit_set_node_property` | mutating | ## `scripts` — Read/write/patch GDScript; parse checks. 6 tools. | Tool | Class | |------|-------| | `godot_scripts_get_for_node` | read_only | | `godot_scripts_get_parse_errors` | read_only | | `godot_scripts_list` | read_only | | `godot_scripts_patch` | mutating | | `godot_scripts_read` | read_only | | `godot_scripts_write` | mutating | ## `shader` — Author .gdshader files, assign materials, set/verify uniform params, compile-validate. 6 tools. | Tool | Class | |------|-------| | `godot_shader_assign_material` | mutating | | `godot_shader_create` | mutating | | `godot_shader_get_param` | read_only | | `godot_shader_read` | read_only | | `godot_shader_set_param` | mutating | | `godot_shader_validate` | read_only | ## `testing` — Assertions, test scenarios, stress fuzz, screenshot diffing. 5 tools. | Tool | Class | |------|-------| | `godot_testing_assert_node_state` | read_only | | `godot_testing_compare_screenshots` | read_only | | `godot_testing_run_stress_test` | runtime | | `godot_testing_run_test_scenario` | runtime | | `godot_testing_run_tests` | runtime | ## `theme_ui` — Theme authoring: create themes, set colors/fonts/styleboxes, UI element styling. 5 tools. | Tool | Class | |------|-------| | `godot_theme_ui_create` | mutating | | `godot_theme_ui_get_node_overrides` | read_only | | `godot_theme_ui_set_color` | mutating | | `godot_theme_ui_set_font_size` | mutating | | `godot_theme_ui_set_stylebox` | mutating | ## `tilemap` — TileMapLayers: create, set cells, fill rects, TileSet authoring. 9 tools. | Tool | Class | |------|-------| | `godot_tilemap_add_tileset_atlas_source` | mutating | | `godot_tilemap_clear` | mutating | | `godot_tilemap_create_tile` | mutating | | `godot_tilemap_create_tileset` | mutating | | `godot_tilemap_fill_rect` | mutating | | `godot_tilemap_get_cell` | read_only | | `godot_tilemap_get_used_cells` | read_only | | `godot_tilemap_layers` | read_only | | `godot_tilemap_set_cell` | mutating | ## `visual_shader` — VisualShader graphs: nodes, connections, constants. 6 tools. | Tool | Class | |------|-------| | `godot_visual_shader_add_node` | mutating | | `godot_visual_shader_connect_nodes` | mutating | | `godot_visual_shader_create` | mutating | | `godot_visual_shader_list_node_types` | read_only | | `godot_visual_shader_read` | read_only | | `godot_visual_shader_set_node_param` | mutating | # Safety classes Every tool carries exactly one safety class in `meta.safety_class`, and the class determines the required parameters. The authoritative rule text lives in [`.opencode/rules/mcp-tools.md`](https://github.com/hybridindie/godot-mcp/blob/main/.opencode/rules/mcp-tools.md); the machine-checked matrix is [`docs/tool-contracts.md`](https://github.com/hybridindie/godot-mcp/blob/main/docs/tool-contracts.md). ## The model | Class | Risk | Extra params | UndoRedo | |-------|------|--------------|----------| | `read_only` | Never mutates | none | n/a | | `mutating` | Reversible editor change | `dry_run: bool = False` | wrapped | | `destructive` | Deletes/overwrites; maybe irreversible | `dry_run` **and** `confirm: bool = True` | wrapped (captures prior state) | | `runtime` | Controls game execution | varies | varies | ## `dry_run` Runs preconditions and returns what *would* happen. Key properties: - Nothing is sent to the editor (asserted by contract tests — `"cmd_…"` absent from the bridge traffic). - The preview carries the **same persistence verdict** the real run will stamp (the `cmd_node_persistence` probe runs the identical rule). - Batch previews report the projected `undoable` flag. - A dry-run must never be a containment bypass: the same server-side path containment (`res://`-only) applies to the preview's existence probe. ## `confirm` Required for every destructive tool, without exception. The refusal is a structured precondition, so an agent can react: ```jsonc { "error": "PRECONDITION_FAILED", "hint": "This call discards unsaved changes. Set confirm=True to proceed.", "required": "confirm" } ``` The recommended flow: `dry_run=true` first (read what will be affected), then `confirm=true` when the preview is acceptable. Destructive tools that remove state capture that state for undo (e.g. `remove_audio_bus` restores the whole bus + effects on undo; `delete_resource_file` restores exact bytes). ## Where the classes come from The class is metadata on the tool registration; `mcp_server/safety.py` owns `MUTATING` / `READ_ONLY` / `DESTRUCTIVE` / `RUNTIME` constants and the `enforce_preconditions` decorator that checks them. The addon never decides whether an action is allowed — it honors the flags defensively (e.g. the add-on-side `confirm` check on `cmd_delete_node`). ## The matrix rules The full class × tool matrix is machine-checked (`docs/tool-contracts.md` + tests). Invariants: - No mutating/destructive tool lacks `dry_run`. - No destructive tool lacks `confirm`. - No tool merges classes (delete is never folded into a fat "node" tool — the gate would be buried). - Read-only tools never mutate — a `read_only` tool that writes is a bug the contract tests catch. ## `godot_undo` Every UndoRedo-wrapped mutation is reversible by the human's own undo stack; `godot_undo` (always-on `core`) steps back one action. The documented exception: batch sets above 20 nodes bypass the stack for performance — and the result says so via `undoable: false` (the scene is still marked dirty, #643). ## Approval & the fail-closed default `confirm` is an *accident guard* the agent sets itself; real human oversight is the opt-in **approval gate** (`GODOT_MCP_APPROVAL_WEBHOOK`, or the elicitation guard with `GODOT_MCP_APPROVAL_REQUIRE`). `mutating`/`destructive` calls are routed to it, plus `runtime` tools flagged `writes_disk`/`executes_code` (`export_project`, `evaluate_expression`). With a webhook configured an unreachable webhook now **fails closed** — it denies — by default (#638); set `GODOT_MCP_APPROVAL_FAIL_OPEN=true` to opt into fail-open. # Value shapes Godot values cross the bridge as JSON. The coercion lives in one addon helper (`type_coerce.gd`) — never inline in a handler — and the same shapes work for setting properties, batch values, and tool params. ## Accepted input shapes (JSON → Godot) | Godot type | Accepted JSON | |------------|---------------| | `Vector2` / `Vector3` | `{"x":1,"y":2}` · `[1,2]` · `"Vector2(100, 200)"` | | `Vector4` | `{"x":…,"y":…,"z":…,"w":…}` · `[1,2,3,4]` · `"Vector4(1, 2, 3, 4)"` | | `Color` | `{"r":1,"g":0,"b":0,"a":1}` · `"#ff0000"` | | `Rect2` | `{"position":{…},"size":{…}}` | | `Quaternion` | `{"x":…,"y":…,"z":…,"w":…}` · `"Quaternion(…)"` | | `AABB` | `{"position":{x,y,z},"size":{x,y,z}}` · `"AABB(…)"` | | `Plane` | `{"normal":{x,y,z},"d":…}` · `"Plane(…)"` | | `Basis` | `{"x":{x,y,z},"y":{…},"z":{…}}` · `"Basis(…)"` | | `Transform2D` | `{"x":{x,y},"y":{x,y},"origin":{x,y}}` · `"Transform2D(…)"` | | `Transform3D` | `{"basis":{…},"origin":{x,y,z}}` · `"Transform3D(…)"` | | `Projection` | `{"x":{x,y,z,w},"y":{…},"z":{…},"w":{…}}` · `"Projection(…)"` | | `PackedByteArray` | `[0,1,255]` | | `PackedVector2Array` / `PackedVector3Array` / `PackedColorArray` | `[{…}]` (element shape as above) | | `NodePath` / `StringName` | a string | | Primitives | as-is (`true`, `3`, `"text"`) | | Non-finite floats | `"__nan__"` · `"__inf__"` · `"__-inf__"` (see below) | | Object-typed properties | `"res://path"` or `"uid://…"` (loaded via `ResourceLoader`), or `null` to clear | Example: ```jsonc // set_node_property(node_path="Player", property="modulate", value="#ff8800") { "id": "7", "command": "cmd_set_node_property", "params": { "node_path": "Player", "property": "modulate", "value": "#ff8800" } } ``` ## Output shapes (Godot → JSON) The same coercion runs in reverse for every result: a `Vector2` comes back as `{"x":…, "y":…}`, a `Color` as `{"r":…, "g":…, "b":…, "a":…}`, a `Transform3D` as `{"basis":{…},"origin":{…}}`, and so on (the full set is the "Accepted JSON" table above). Everything that crosses the bridge is JSON-safe — no Godot objects, ever (serialized trees are `{name, type, path, script, children}` dicts; large trees support `max_depth`). The **runtime probe reuses this same module** (`type_coerce.gd`), so the editor and the running game emit identical shapes — there is no second coercion implementation. ### Non-finite floats JSON has no literal for `NaN`/`INF`, and `JSON.stringify` maps them lossily (`NaN`→`null`, `INF`→`1e99999`). They are therefore encoded as explicit string sentinels — `"__nan__"`, `"__inf__"`, `"__-inf__"` — in both directions, so a round-trip is faithful (#641). ### Writes are verified by read-back `set_node_property` coerces the value to the property's declared type, applies it, then compares the **value read back** to the coerced value; a mismatch fails with a structured `VALIDATION_ERROR` naming the expected type. A value the engine silently ignores (an unsupported type, a dimension mismatch) can no longer come back as the *old* value under `set: true` (#641). Group / category / subgroup headings are rejected as properties (they have no backing storage). ## Coercion rules the addon follows 1. **Coerce to the property's declared type** when it already exists (a vector dict becomes a `Vector2`, a string form is parsed). 2. **Unknown keys are refused, not defaulted** — a malformed value is a `VALIDATION_ERROR` naming the property, never a silent zero (which would target the wrong cell/property). 3. **Type-check up front for polymorphic setters**: `setup_collision` rejects a 2D shape on a 3D body (and vice versa) with a dimension-naming hint, instead of shipping collisionless levels behind `ok: true`. ## Where it lives - Addon: `godot/addons/godot_mcp/type_coerce.gd` (`to_json` / `from_json`) - Server: `mcp_server/coercion_middleware.py` (per-call param coercion) - Full per-property table: `docs/tool-contracts.md` § value-shapes The type-coercion logic is deliberately single-sourced — a copy-pasted coercion layer (or one living inline in a handler) is a drift bug the addon rules call out as blocking, and would drift from this table within weeks. It is pinned by a headless behavior test (`godot/tests/type_coerce_smoke.gd`, wired into pytest via `tests/integration/test_addon_read_smokes.py`) that round-trips every shape in this document against the real Godot runtime. # Errors & recovery Errors are structured data an agent can act on. Never a stack trace, never silent, and every failure names a fix. ## The error-code enum The eight bridge-envelope codes (the `ErrorCode` enum in `mcp_server/models/envelope.py` — what the addon can emit): | Code | Meaning | Typical cause | |------|---------|---------------| | `PRECONDITION_FAILED` | a required state is missing | no scene open, missing `confirm`, toolset version gate | | `RESOURCE_NOT_FOUND` | a node/scene/script/resource path does not exist | typo'd path; use `get_scene_tree` / `get_filesystem_tree` first | | `VALIDATION_ERROR` | parameters are malformed or refused | unknown ProjectSettings key, dimension mismatch, bad enum value | | `BRIDGE_DISCONNECTED` | Godot is not reachable | editor closed / addon disabled / bridge URL wrong | | `TIMEOUT` | the request outlived its timeout (may carry a readiness `reason`) | a capture that never landed; a stalled editor | | `INTERNAL_ERROR` | a handler failed without producing a response | a GDScript error inside the addon (see the editor's Output panel) | | `APPROVAL_DENIED` | the human declined an approval | n/a — retry with different intent | | `PEER_REPLACED` | a server **notice** to the editor that a second editor took over the bridge (not a tool failure) | two editors pointed at one server; the replaced dock shows purple and stops reconnecting (#593) | Two server-side codes appear in `ToolError` text but never cross the bridge (the server rejects before dispatching, or bounds the payload locally): | Code | Meaning | Typical cause | |------|---------|---------------| | `PARAM_ERROR` | a path parameter rejected server-side before the bridge call | a script/resource path that isn't `res://`-contained (`res://../…`, `res:///abs`) | | `OUTPUT_LIMIT` | a result payload would exceed the 25k character cap | an oversized `snapshot_subtree` — narrow with `max_depth` | ## The recovery table | Error / symptom | Fix | |-----------------|-----| | `unknown tool` | the toolset isn't enabled → `godot_enable_toolset(...)`; or you mixed a server-side tool with an editor tool | | `[required=active_scene]` | `godot_scene_edit_open_scene(path)` or `create_scene` | | `[required=confirm]` | destructive call → `dry_run` first, then `confirm=True` | | `[required=bridge_connected]` | open Godot with the addon; check the status dock | | `[required=runtime_probe]` | register `MCPRuntimeProbe` as an autoload | | `[required=game_not_breaked]` | the game is frozen at a debugger break → `godot_debugger_continue_execution` | | `[required=godot_version]` | the toolset needs Godot 4.4+ → upgrade the editor | | `VALIDATION_ERROR: Unknown ProjectSettings key … did you mean …` | use the suggested key (e.g. `application/run/main_scene`, not `application/config/main_scene`) | | `persisted: false` + `hint` | act on the hint (e.g. `godot_scene_edit_set_editable_children`) or retarget | | `rescan_pending: true` | re-run the parse check once; the editor's scan hadn't flushed | | `NON-ZERO EXIT` on a real crash | read `errors[]` from the run; fix and re-verify | | `RUN TIMED OUT` | if the game never self-quits, pass `expected_timeout=true` to `godot_debug_workflow` | ## Design rules 1. **Failures are data.** `{ok: false, error, hint}` crosses the envelope; the server shapes the same structure into a `ToolError` for the client. 2. **`hint` is for recovery** — written for an agent with no human in the loop. A hint that restates the code is a bug. 3. **Preconditions carry `required`** — the machine-checkable name of what to satisfy. 4. **No partial success.** A result never reads as success when the effect half-failed: `aborted_at`, `undoable`, `persisted`, and `rescan_pending` exist so the agent never has to guess. 5. **No invented codes.** Clients match on the enum; a new code is an additive contract change. The envelope-level rules are pinned by contract tests against a fake addon peer — a shape change without a contract-test update is a drift bug. # Environment variables All knobs are optional; the defaults serve a local single-user setup. ## Server | Variable | Default | Meaning | |----------|---------|---------| | `GODOT_MCP_TRANSPORT` | `stdio` | `stdio` or `http` | | `GODOT_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind host | | `GODOT_MCP_HTTP_PORT` | `9090` | HTTP bind port (`/mcp` is FastMCP's mount path) | | `GODOT_MCP_AUTH_TOKEN` | unset | Bearer token; **required** for non-loopback HTTP binds (fail-fast, #226) | | `GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTION` | `auto` | HTTP Host/Origin (DNS-rebinding) guard (#636): `auto` validates the `Host` header + browser `Origin` for a loopback-bound server (foreign `Host` → `421`, foreign `Origin` → `403`); `false` opts out; `true`/`strict` validates every request | | `GODOT_MCP_BRIDGE_URL` | `ws://127.0.0.1:9080` | bridge listen (server) / connect (addon) URL | | `GODOT_MCP_BRIDGE_TOKEN` | unset | opt-in shared secret for the bridge handshake (#538) — when set, the addon must send the matching token as its first message or the peer is refused at the handshake; never logged | | `GODOT_MCP_PROJECT_DIR` | unset | explicit project dir; else the connected editor's project is used | | `GODOT_MCP_DEFAULT_TOOLSETS` | unset (= `inspection`) | seed the initial enabled toolsets: `all` or a comma-separated list (e.g. `scene_edit,runtime`); unknown names are logged and ignored | | `GODOT_MCP_GODOT_BIN` | `godot` on PATH | the Godot binary the *server* shells out to for headless runs and parse/compile checks (distinct from the addon's editor; also settable as `GODOT_BIN` on CI) | | `GODOT_MCP_LOG_LEVEL` | `INFO` | structured stderr logging level (JSON per line) | | `GODOT_MCP_APPROVAL_WEBHOOK` | unset | human-in-the-loop approval endpoint for `destructive` tools (#153); unset → auto-approve | | `GODOT_MCP_APPROVAL_TIMEOUT` | `30.0` | per-approval-request timeout, seconds | | `GODOT_MCP_APPROVAL_FAIL_OPEN` | `false` | on an unreachable/slow webhook: `false` **denies** (fail closed, #638), `true` approves | | `GODOT_MCP_CHECK_UPDATES` | unset (= off) | opt-in update check (#629): when set (`1`/`true`/`yes`) the server may query the release feed so `godot_check_for_updates` can report a newer release; unset = no network | ## Addon (read in the editor's environment) | Variable | Default | Meaning | |----------|---------|---------| | `GODOT_MCP_BRIDGE_URL` | `ws://127.0.0.1:9080` | where the addon dials the server (same var as the server) | | `GODOT_MCP_BRIDGE_TOKEN` | unset | the same opt-in secret as the server — the addon authenticates with it as its first message after connecting; never logged | | `GODOT_MCP_AUTO_REFRESH` | unset (= off) | opt-in timer-based filesystem auto-refresh (#561): `1`/`true`/`yes` enables a periodic `EditorFileSystem.scan()` so external edits (agents, git, other tools) show up without editor focus; the dock checkbox is the runtime override | | `GODOT_MCP_AUTO_REFRESH_INTERVAL` | `10` | seconds between auto-refresh scans (floor 2s; only read when `GODOT_MCP_AUTO_REFRESH` is on) | | `GODOT_BIN` | `godot` on PATH | where a *CI workflow* points the server's binary lookup (`e2e.yml` Actions var) — the server itself reads `GODOT_MCP_GODOT_BIN`, then `godot`/`Godot` on PATH, then the macOS app bundle | ## Security model | Mode | Bind | Auth | Why | |------|------|------|-----| | `stdio` (default) | N/A (local subprocess) | none | only the spawning client can talk to it | | `http` on loopback | `127.0.0.1` | none | only local processes reach it | | `http` non-loopback | `0.0.0.0` / external IP | **`GODOT_MCP_AUTH_TOKEN` required** | the server refuses to start otherwise (#226) — without a token, anyone on the network could write scripts, run the game, or export builds through your editor | ```bash # Generate + set a token TOKEN=$(openssl rand -hex 32) GODOT_MCP_TRANSPORT=http GODOT_MCP_HTTP_HOST=0.0.0.0 GODOT_MCP_AUTH_TOKEN=$TOKEN \ uv run godot-editor-mcp ``` Clients pass the same token (HTTP `Authorization: Bearer …` header). ### Bridge handshake auth (#538) The editor↔server WebSocket bridge is localhost-only; the bridge token adds a shared secret for shared/self-hosted runners and remote-editor setups. It is **opt-in on each side** — unset means no auth (zero-config local dev, unchanged path). Both sides read `GODOT_MCP_BRIDGE_TOKEN`. | Server token | Addon token | Behavior | |--------------|-------------|----------| | unset | unset | today's path — no auth exchange (byte-identical) | | set | set, matching | peer authenticated, everything serves | | set | set, **mismatched** | structured refusal at the handshake; the addon drops into its reconnect/backoff loop and the dock log shows the refusal reason each attempt until the tokens agree | | set | **unset** | refused at the handshake (the addon never authenticates) — same visible reconnect loop | | **unset** | set | the server consumes the auth envelope as a control message and serves anyway (forward-compatible: a newer addon against an older server) | A refusal happens **at the handshake** — never as per-command errors, never as a silent drop. Tokens are compared in constant time and are **never logged** on either side. ```bash # Shared/self-hosted runner: pin the same token on both sides TOKEN=$(openssl rand -hex 32) GODOT_MCP_BRIDGE_TOKEN=$TOKEN uv run godot-editor-mcp # server GODOT_MCP_BRIDGE_TOKEN=$TOKEN godot --editor --path ./game # the editor's env ``` ### Listener hardening (#636) Browsers do **not** apply the same-origin policy to WebSockets, and DNS rebinding can make a page the user visits resolve to `127.0.0.1`. Both listeners are hardened by default: - **Bridge WebSocket**: the listener accepts only Origin-less handshakes (`origins=[None]`) — the Godot addon's `WebSocketPeer` sends no `Origin`, so a browser page is rejected at the handshake (`403`) before it can reach the bridge. A bad-or-missing-token peer is also refused *before* it can displace the authenticated editor (#635). - **HTTP transport**: FastMCP's `HostOriginGuardMiddleware` is enabled by default (`GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTION=auto`), so a non-loopback `Host` header is refused (`421`) and a foreign browser `Origin` is refused (`403`) before any code-executing tool (`write_script`, `run_and_capture`, `export_project`) is reachable. Opt out with `GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTION=false`. Neither guard replaces the auth token — a non-loopback HTTP bind still requires `GODOT_MCP_AUTH_TOKEN`. ## Logging Logs are one JSON object per line on **stderr** (stdout is the stdio protocol channel). Every log line carries the command `id` where applicable; errors include exception info. No secrets or full file contents are logged. # Prompts & resources Beyond the 194 tools, the server exposes two more MCP surfaces: **prompts** (step-numbered workflow templates the agent renders for guidance) and **resources** (read-only `godot://` snapshots the host can fetch). ## Workflow prompts (9) Prompts are recipes, not actions: each one is a step-numbered template naming *which tools to call in what order*. Clients that surface prompts show them as slash commands (e.g. `/mcp__godot-mcp__build_scene`). Render with `list_prompts()` / `render_prompt(name, arguments)`. | Prompt | Purpose | Key tools it orchestrates | |--------|---------|---------------------------| | `toolset_discovery` | The gating protocol — **use first in every session**; teaches enable-before-use | `godot_get_server_info`, `godot_enable_toolset`, representative gated tools | | `build_scene` | Create a scene end-to-end: nodes, scripts, collision | `create_scene`, `create_node`, `attach_script`, `physics_setup_collision` | | `play_test` | Live play-testing: probe autoload, play, inspect, simulate input | `play_scene`, `input_simulate_*`, `runtime_find_ui_elements` | | `script_edit` | Author GDScript: write, attach, parse-check, patch | `scripts_write`, `scripts_patch`, `scripts_get_parse_errors` | | `debug_scene` | Systematic debugging: health → workflow → signal flow → unused resources | `godot_debug_workflow`, `analysis_*`, `get_node_properties` | | `troubleshoot` | Diagnostic playbook for common failures (bridge, toolsets, probe) | `godot_health_check`, `get_server_info`, `list_toolsets` | | `author_resource` | Author a resource (.tres/tileset) with the right toolset | `tilemap_create_tileset`, `add_tileset_atlas_source`, `create_tile` | | `export_build` | Ship a build: presets → export → verify | `export_list_presets`, `export_get_info`, `export_project` | | `batch_refactor` | Safe bulk property changes: find → preview → apply | `batch_find_nodes_by_type`, `batch_set_property`, `cross_scene_set_property` | Every prompt composes from the shared toolset-protocol text (single source of truth in `mcp_server/toolset_protocol.py`), so the gating instructions cannot drift between the server instructions and the prompts. ## Read-only resources (4) Resources are addressable, refreshed-on-access snapshots — JSON strings, no side effects ever (mutations always go through tools). On a bridge failure the resource returns valid JSON carrying the structured error. Clients without resource support can read them through the `read_resource` fallback tool. | URI | Returns | |-----|---------| | `godot://project/info` | Project metadata: name, Godot version, main scene, autoloads, input actions | | `godot://scene/current` | The currently open scene: `is_open`, `path`, `name` | | `godot://scene/tree` | The full active scene tree `{name, type, script, children}` (may be large) | | `godot://scene/tree/{max_depth}` | The tree limited to N child levels (resource template) | | `godot://node/selected` | The currently selected node snapshot, or `{"selected": null}` | These mirror the always-on inspection tools (`get_project_info`, `get_active_scene`, `get_scene_tree`, `get_selected_node`) — read-only context is exposed both ways by design; which one a client uses depends on whether it implements the resource protocol. ## Discovery Both surfaces appear in `godot_get_server_info` (`prompts[]`) and via the standard MCP `prompts/list` and `resources/list` calls. # Skills Beyond tools, prompts, and resources, godot-mcp ships **three installable AI skills** under [`skills/`](https://github.com/hybridindie/godot-mcp/tree/main/skills). A skill is client-side knowledge — not an action — that the agent **auto-triggers** when it recognises a matching task, then routes to the right prompts and tools. ## Skills vs prompts — different jobs | | Prompts (`/mcp__godot-mcp__build_scene`) | Skills (`godot-getting-started`) | |---|---|---| | Who invokes | The user picks a slash command | The **agent** auto-loads on a matching task | | Shape | A step-numbered recipe naming tools in order | Prose knowledge + rules + a map | | Lives | In the server (rendered via `render_prompt`) | On disk, installed into the client's skill dir | | Use for | "Do this specific procedure" | "Know how to work here at all" | They compose: a skill teaches the model *how the surface works*, and points it at the prompts for *specific procedures*. ## The three skills ### `godot-getting-started` — the MCP workflow Read once at the start of any Godot session. Teaches: - the bridge/version check (`godot_health_check`, `godot_get_server_info`) - the **toolset-gating model** — enable a toolset before its tools exist - the safety convention (`read_only` / `mutating` + `dry_run` / `destructive` + `confirm` / `runtime`) - reading the honesty fields (`persisted`, `undoable`, `aborted_at`, `rescan_pending`) - the `godot://` resource surface and the `read_resource` fallback - round-trip economy (`run_commands`, batch setters) Triggers on *"use godot-mcp", "drive Godot", "unknown tool from godot"*. ### `godot-playtest-and-debug` — the runtime loop The two run modes (headless smoke vs live play session), registering the runtime probe, playing and inspecting the live tree, simulating input, assertions/stress/screenshots, and the debugger (breakpoints, stepping, frame variables, expression evaluation). Triggers on *"play-test in Godot", "why does my game crash", "debug the running scene"*. ### `godot-expert` — engine knowledge The Godot 4.x rules and pitfalls an expert knows — Control vs Node2D rendering, z-ordering, the autoload-children trap, input interception, collision layers/masks, GDScript type inference, `.tscn` format, and the common-bug list. Triggers on *"build a Godot game", "sprite not showing", "input not working"*. ## The `godot-expert` reference guides The expert skill loads detail on demand from `references/` — 12 guides, each "rule → pitfall → the MCP tool shape that applies it": | Guide | Covers | |-------|--------| | `ui-hud.md` | CanvasLayer vs Node2D, anchors/presets, the input-blocking trap | | `physics-collision.md` | body types, layer/mask convention, collision shapes | | `autoload-architecture.md` | autoload lifecycle, the children-rendering trap, state machines | | `scene-authoring.md` | `.tscn` format, disk vs bridge editing | | `scene-templates.md` | ready-to-use `.tscn` templates | | `testing-gut.md` | GUT setup, patterns, gotchas | | `tileset-gridmap.md` | TileSet chain, GridMap + MeshLibrary authoring | | `visual-shader.md` | VisualShader graph authoring (node ids, port wiring) | | `shader-authoring.md` | text `.gdshader`, validate-before-assign | | `theme-styling.md` | theme vs local override, per-state styleboxes | | `particles-animation.md` | particles, Animation tracks/keyframes, Navigation | | `common-bugs.md` | documented bugs — symptom, root cause, fix | ## Install ```bash # opencode (default — symlinks into ~/.config/opencode/skills/): ./scripts/install-skills.sh # Claude / other clients: ./scripts/install-skills.sh --target ~/.claude/skills # Standalone copy (packaging a release, no repo dependency): ./scripts/install-skills.sh --copy ``` Symlink (the default) is best during development — repo updates flow through. Restart the client after installing; skills activate automatically on their triggers. ## These skills stay honest The skills are part of the product surface and are **pinned to the live toolsets** by `tests/unit/test_skills_metadata.py`: - every `godot_*()` call a skill shows must resolve to a real tool - the getting-started quick-map must name every registered toolset - every registered prompt must appear in a workflow skill - every `references/*.md` guide must be linked from its `SKILL.md` - the skills and their README must name the current server version So a surface change without a skills update fails the test suite — the skills cannot silently drift from the tools they teach. # Consumer integration godot-mcp is deliberately game-agnostic. This page maps the integration surface for consumers that layer game-specific behavior on top. ## The integration surface A consumer pins four things (all versioned, all additive since contract 1): | Pinned surface | Where it lives | How to track drift | |----------------|----------------|--------------------| | Tool names + param keys | `_TOOL_NAMES` in the consumer's MCP client | mirror verbatim; tool-accuracy evals double as contract checks | | Toolset categories | `enable_toolset(category)` before gated calls | server-global set — a grant survives reconnects | | Envelope/result shapes | normalized `MCPResult(ok, data, error, required)` | `structuredContent` preferred when present | | Contract version | `godot_get_server_info.contract_version` | client compatible when `min ≤ client ≤ contract` | ## The companion skills (in this repo) Three installable AI skills ship under [`skills/`](https://github.com/hybridindie/godot-mcp/tree/main/skills) — install into any client via `scripts/install-skills.sh`: - `godot-getting-started` — bridge connection, toolset gating, safety classes, honesty fields, the `godot://` resources, round-trip economy - `godot-playtest-and-debug` — runtime play-test, input simulation, break-state gate, timeout semantics - `godot-expert` — Godot 4.x engine knowledge: 10 sections + 12 reference guides (rendering, physics, UI, authoring, shaders, themes, …) + a documented-bug list See [Skills](reference-skills.md) for what each teaches and the full guide index. They are pinned to the live surface by `tests/unit/test_skills_metadata.py` (every tool reference must resolve, the toolset map must cover all toolsets, every guide must be linked), so a surface change without a skills update fails the suite. ## The orchestrator pattern A consumer that drives godot-mcp as one component of a larger agent system **built specifically against this server** (not a generic MCP client) demonstrates the pattern: - Typed client wrappers mirroring godot-mcp's tools verbatim, with the same param keys and `_ensure_or_return_error` gating - Honesty-field awareness: `persisted`, `undoable`, `aborted_at`, `rescan_pending`, `expected_timeout`, `game_not_breaked` - `notifications/tools/list_changed` handling (`toolset_generation`) - A live eval harness whose suites drive a real editor through the bridge — the drift catcher across the integration seam ## Layering your own game vocabulary The recommended shape for a game-specific consumer: 1. **Keep godot-mcp untouched** — it stays generic; game vocabulary ("spawn wave", "upgrade tower") lives in your project. 2. **Wrap, don't fork** — typed client wrappers per tool group, with your domain models built from godot-mcp's `structuredContent` payloads. 3. **Add your own prompts/skills** — the recipe templates here ([prompts](reference-prompts.md)) are the pattern; your game's versions compose the same generic tools in game-shaped orders. 4. **Pin the contract** — assert `contract_version` at startup; the additive guarantee means unknown fields are safe to ignore. 5. **Eval the behavior** — the tool-accuracy eval pattern (score calls against an `expected_tools` spec) doubles as drift detection. ## Under the hood # Architecture ::: info For readers who know MCP This is the technical track. New here and just want to make a game? → **[Set up with your AI agent](../getting-started-with-an-agent.md)**. ::: godot-mcp is two halves joined by one seam. Keep the seam clean; keep logic out of the handlers. ```mermaid flowchart LR subgraph client["Agent side"] AI["AI client (OpenCode / Claude / any MCP client)"] end subgraph server["godot-mcp server (Python)"] SRV["FastMCP server 194 tools · safety · Pydantic models"] BR["WebSocket listener ws://127.0.0.1:9080"] end subgraph addon["godot-mcp addon (GDScript)"] WS["WebSocketPeer client connects out · reconnects"] RT["Command router cmd_* handlers"] end ED[("Live Godot project")] AI -->|"stdio (MCP)"| SRV SRV --- BR WS ==>|"dials out, reconnects"| BR BR -.->|"{id, command, params}"| RT RT -->|"Godot Editor API"| ED RT -.->|"{id, ok, result, error}"| BR ``` The bold arrow is the **transport** — the editor dials out and reconnects. Once connected, the **server still initiates every command**; the addon responds. The direction of control never changed, only who dials. ## Pages in this section - [The four-layer transport chain](index.md) — this page - [The Godot addon](addon.md) — the GDScript half: dial-out client, command router, status dock, undo - [Bridge & transport](bridge.md) — lifecycle, backoff, timeouts, config - [The JSON envelope](envelope.md) — command/response shapes, error codes, id correlation - [Toolset gating](gating.md) — the enabled set, list_changed, version gates - [The safety model](safety.md) — classes, preconditions, dry_run/confirm, where safety lives - [Persistence truth](persistence.md) — the owner chain, parent rule, per-target verdicts - [The readiness envelope](readiness.md) — reason tokens, poll_ready, why nothing stalls silently ## The layers | Layer | Where | Owns | Never touches | |-------|-------|------|---------------| | AI client | any MCP host | tool selection, prompt context | Godot, the bridge | | MCP server | `mcp_server/` (Python 3.11+, FastMCP) | safety, preconditions, Pydantic models, tool surface, the WebSocket **listener** | the Godot editor API | | Godot addon | `godot/addons/godot_mcp/` (GDScript, `@tool`) | the Godot Editor API, UndoRedo, serialization, reconnection | safety decisions, permissions | | Godot | the running editor + project | game state | — | Two rules keep the seam clean: 1. **Library-first**: tool/resource/prompt handlers are *delegation only* — validate, call a service or the bridge, return a typed model. Zero domain logic in the handler body. Reusable logic lives in `bridge.py`, `safety.py`, and `mcp_server/models/`. 2. **Service isolation**: the bridge client is the only way Python talks to Godot; the server owns all safety; the addon owns all Godot API calls. Neither crosses into the other. ## Why four layers at all? A thinner design (client → addon directly) would skip the MCP server, but then safety, gating, and typing would live in GDScript, version-locked to the editor, and unshareable across stdio/HTTP clients. A thinner one (client → Godot over a socket, no Python) loses typed models and the eval/skill ecosystem that runs on the Python side. The four-layer shape lets each half evolve on its own Cadver/contract terms: the addon pins Godot APIs, the server pins the MCP protocol, and the envelope is the versioned glue. The bridge seam is also the test boundary: the entire suite runs against a fake addon connection — no sockets, no editor, deterministic — and a separate live-e2e suite exercises the real editor on a self-hosted runner (run it on a PR touching the addon/server, or dispatch it manually). ## The data path, end to end 1. The agent calls `godot_scene_edit_create_node(parent_path=".", node_type="Node2D", node_name="Player")` over stdio. 2. FastMCP resolves the tool (transform maps the exposed name to the handler), runs safety: `active_scene` precondition, safety-class params. 3. The server writes `{id, command: "cmd_create_node", params}` to the WebSocket. 4. The addon's router dispatches to the `cmd_create_node` handler: resolves the parent, probes the persistence verdict, registers the create with `EditorUndoRedoManager`, commits. 5. The handler returns `{id, ok, result: {node_path, created, persisted, …}}`. 6. The server builds a typed Pydantic model (`CreateNodeResult`), FastMCP serializes it as `structuredContent` on the MCP response. 7. The agent reads `persisted: true` and proceeds — or reads `persisted: false, reason: instanced_child_not_editable` and acts on the hint (enable Editable Children, retarget, …). Each numbered hop has a contract test pinning its shape. # The Godot addon The addon (`godot/addons/godot_mcp/`) is the **only** part of the system that lives inside Godot and the only code that touches the Godot Editor API. It is a plain `EditorPlugin` (`@tool`) that installs under `res://addons/godot_mcp/` and joins the editor to the MCP server. Everything else — the reasoning, the per-tool contract, and the safety model — lives on the [server](index.md) side. See [Install the Godot addon](../getting-started-addon.md) for setup; this page is the architecture of what you installed. ## What it is, in one picture ```mermaid flowchart LR SRV["MCP server
WebSocket listener"] WS["WebSocketPeer client
(dials out · reconnects)"] RT["Command router"] H["cmd_* handlers"] API["Godot Editor API"] DOCK["Status dock
(read-only)"] DBG["Debugger plugin"] SRV -.->|"{id, command, params}"| WS WS --> RT --> H --> API API --> H -.->|"{id, ok, result, error}"| WS -.-> SRV RT --> DOCK RT --> DBG ``` The server **initiates every command**; the addon responds. The addon never acts unprompted. ## The connection: it dials out The addon embeds a `WebSocketPeer` **client** that connects **out** to the server's bridge listener (default `ws://127.0.0.1:9080`, configurable via `GODOT_MCP_BRIDGE_URL` on both sides). This inversion (#276) is deliberate: - **Start order never matters.** Editor first, server first — whoever comes second connects. - **Restarts don't wedge.** The addon reconnects with exponential backoff (≈500 ms, doubling, capped), so a bounced server or a restarted editor recovers on its own. - **A second editor is handled honestly.** When another instance takes over the bridge, the replaced editor gets a `PEER_REPLACED` notice and shows a distinct **"Replaced by another editor"** state (purple dot) instead of a misleading green, and stops reconnecting. The transport details (auth, buffer sizes, the control-message pushes) are in [Bridge & transport](bridge.md) and the [JSON envelope](envelope.md). ## Command routing Every incoming envelope is one JSON object; a single **command router** (`command_router.gd`) dispatches it to a `cmd__` handler that calls the Godot Editor API and returns a correlated `{id, ok, result, error}` response. - **Handlers are per-domain**, registered data-driven from a table (32 domain handler scripts under `handlers/`), plus the router's own core commands (undo/redo/history, batch, handshake). One handler does one thing. - **A handler that dies on a GDScript error** answers `INTERNAL_ERROR` rather than leaving the server to time out (#466). - **Nothing is guessed.** The router is the only place command strings resolve; the server's handshake surface mirrors its registered-command list. ::: info Why the addon holds no safety logic Preconditions, `dry_run`/`confirm`, gating, and permission decisions all live in the server (see [The safety model](safety.md)). The addon performs the editor operation it is asked to, undo-tracked and serialized. This keeps it game-agnostic — it knows Godot, not your game. ::: ## Mutations are undoable Every create, rename, delete, or property set registers with **`EditorUndoRedoManager`**, so a human can revert any agent action with **Ctrl+Z**. Destructive handlers honor the `confirm` flag the server enforces. Batches above the 20-node undo threshold report `undoable: false` with a hint rather than pretending the undo stack covers them. ## Everything returned is JSON-safe Scene trees, node data, and properties serialize to plain JSON. Godot types (`Vector2/3`, `Color`, `Rect2`, `NodePath`, resources) are coerced by a single shared helper, `type_coerce.gd` — never inline, never duplicated. A handler error becomes an `ok: false` envelope with a stable code and an actionable hint, never a raw engine trace. ## The status dock Enabling the plugin adds a **read-only** status panel at the bottom of the editor (alongside Output and Debug). It is a dumb `Control` fed by the plugin — it holds no Editor API knowledge and never mutates the project, which is what makes it verifiable headlessly. It shows: | Row | Shows | |-----|-------| | Connection | colour dot + state — Connected / Connecting / Disconnected / **Replaced** | | Server / Godot | the server's CalVer once the handshake lands, and the editor version | | Bridge | the bridge URL | | Scene | the active scene, marked `main.tscn ●` when the editor holds unsaved edits | | Selected | the selected node | | Last action | the most recent undoable change — *"create_node 'Player' (Ctrl+Z to undo)"* | | Playing | the running scene + whether the runtime probe is attached | | Toolsets | the enabled set, pushed by the server (honestly `(unknown)` before the first push) | | Recent commands | an outcome-bearing log (✓ success, ✗ **error code**) with a per-session count and a **Copy** button | Reconnect attempts and server notices (auth refusal, a peer replacement) share the same log, so a flapping or taken-over link explains itself. The dock reflects state; it never drives it. ## Live runtime: the debugger plugin + probe Two optional pieces let the agent see a **running** game, not just the editor: - A **debugger plugin** (`mcp_debugger.gd`) registers with the editor's debugger and captures the `godot_mcp:` channel — the live scene tree, game output, and the break/step/stack surface. - A **runtime probe** (`mcp_runtime_probe.gd`) that you add as an autoload **in your game's project**. It runs in the game, no-ops outside a debug session, and is safe to leave enabled. With it, live input simulation, property monitoring, and UI-element discovery work; without it, those calls return a clear precondition failure — never a silent no-op. See [Play-test & debug](../guides-playtest-debug.md) for the loop. ## What the addon owns vs. the server | The addon owns | The server owns | |----------------|-----------------| | the Godot Editor API, `EditorUndoRedoManager` | safety classes, preconditions, `dry_run`/`confirm` | | `WebSocketPeer` client, reconnection/backoff | the WebSocket **listener**, one active peer | | JSON-safe serialization (`type_coerce.gd`) | Pydantic result models, the tool surface | | the status dock, the debugger plugin | toolset gating, contract versioning | | `cmd_*` handlers | MCP tools / resources / prompts | The seam is the versioned JSON envelope. Keep it clean: **only the addon touches Godot; only the server owns safety.** # Bridge & transport ## Topology The **server listens**, the **editor connects out** (#276). Both are configurable via the same env var, `GODOT_MCP_BRIDGE_URL` (default `ws://127.0.0.1:9080`) — never hard-coded in library code. ```mermaid sequenceDiagram participant E as Godot addon participant S as MCP server (listener) Note over S: serve() binds :9080 — before, during, or after Godot launches E->>S: WebSocket connect (dials out) Note over E,S: transport up; server initiates everything from here S->>E: {id, command: "cmd_ping", params: {}} S-->>E: {id, ok: true, result: {pong: true}} Note over E,S: editor closes / restarts E-->>S: (connection lost) Note over E: exponential backoff ~500 ms → capped E->>S: reconnects ``` Why the editor dials: - The editor is the party that comes and goes; the server is a headless service (often spawned per session by the MCP client, or a long-lived Docker service). - Launch order never matters: the server may bind the port before Godot exists. - Reconnection with exponential backoff (start ~500 ms, capped) lives on the addon side — the only party with enough knowledge to survive restarts. ## Framing & lifecycle - **Framing:** one JSON object per WebSocket text message, UTF-8. No headers. - **One active peer:** a new addon connection replaces the old one (a stale editor can't ghost the bridge). - **Auth is opt-in (#538)**: localhost-only by default — the zero-config path has no handshake. Set `GODOT_MCP_BRIDGE_TOKEN` on both sides for shared/remote-editor setups: the addon authenticates as its first message, a mismatch is refused at the handshake, tokens are never logged. Separately, a non-loopback HTTP bind requires `GODOT_MCP_AUTH_TOKEN` (#226) — a fail-fast guard refuses to start otherwise. - **`cmd_ping` → `{pong: true}`** is the health check, both directions. - **Server→addon pushes:** besides commands the addon answers, the server sends a few fire-and-forget control messages the addon consumes without a waiter — `cmd_server_hello` (server version, #521) and `cmd_toolsets_update` (the enabled toolset set, #592, so the dock's *Toolsets* row mirrors the server). A replaced peer also receives a `PEER_REPLACED` notice before its link closes (#593). ## Threat model Loopback is reachable by more than the editor, so the listeners are hardened by default (#636): - **A browser page.** WebSockets are exempt from the same-origin policy, and DNS rebinding lets an attacker's hostname resolve to `127.0.0.1`. The bridge therefore accepts only **Origin-less** handshakes (`origins=[None]`) — the Godot addon's `WebSocketPeer` sends no `Origin`, so a page is refused at the handshake; the HTTP transport's Host/Origin guard is likewise on by default (`GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTION=auto`; foreign `Host` → `421`, foreign `Origin` → `403`). - **Another local process.** Any local process can connect. The bridge is localhost-only; for shared/remote setups the opt-in token (#538) is the gate, and a bad/missing token is refused **before** it can displace the authenticated editor (#635). A non-loopback HTTP bind still requires `GODOT_MCP_AUTH_TOKEN` (#226). ## Timeouts, everywhere A request that outlives its timeout resolves to a `TIMEOUT` envelope — it does not hang the agent. Concretely: | Timeout | Where | Value | |---------|-------|-------| | Bridge request | `Bridge.send(command, timeout=…)` | per-call; tool defaults vary (3–30 s) | | Toolset enable's version fetch | `_fetch_godot_version` | 3 s, best-effort | | HTTP transport | `GODOT_MCP_HTTP_*` via httpx client | tool-call bound (#422) | | Readiness polls | `poll_ready(…, timeout_ms)` | per-tool (`timeout_ms` param) | | Reconnect backoff | addon `mcp_bridge.gd` | ~500 ms start, capped | A send with **no connected peer** resolves to `BRIDGE_DISCONNECTED` immediately — it does not wait for an editor that isn't coming. ## Concurrency model - **Id correlation is concurrency-safe**: many in-flight commands, each resolved to its own waiter keyed by `id`. No shared mutable state across requests. - **Single-writer semantics**: one editor per bridge. A second connection *replaces* the first (documented; the server is single-user local by design). - The server is **sessionless** by design (MCP `2026-07-28` shape): no per-session state, works on every transport, and `enable_toolset` writes a server-global set shared by all clients (see [gating](gating.md)). - No blocking sync I/O inside async handlers; no import-time sockets. ## Configuration | Env var | Default | Meaning | |---------|---------|---------| | `GODOT_MCP_BRIDGE_URL` | `ws://127.0.0.1:9080` | bridge listen/connect URL (both sides) | | `GODOT_MCP_TRANSPORT` | `stdio` | `stdio` or `http` | | `GODOT_MCP_HTTP_HOST` / `_PORT` | `127.0.0.1:9090` | HTTP bind | | `GODOT_MCP_AUTH_TOKEN` | unset | Bearer token — **required** for non-loopback HTTP binds (#226) | | `GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTION` | `auto` | HTTP Host/Origin (DNS-rebinding) guard (#636) — `auto` protects loopback, `false` opts out | | `GODOT_MCP_BRIDGE_TOKEN` | unset | opt-in shared secret for the bridge handshake (#538) — set on both sides; the addon authenticates as its first message; a mismatch is refused at the handshake; never logged | | `GODOT_MCP_PROJECT_DIR` | unset | explicit project dir (else the connected editor's) | The full table lives in [env-vars](../reference-env-vars.md). ## Testing the seam The bridge is the seam most worth testing, so it gets two layers: 1. **Fake addon connection** (`tests/fakes.py`): an in-memory transport that validates real envelope shapes, correlating `id`s, and canned responses — the entire contract suite runs with no sockets and no editor. 2. **Live e2e** (`tests/integration/test_*_e2e.py`): a real editor on a self-hosted runner; ephemeral per-run bridge ports (#444) so concurrent jobs can't contend; the addon's reconnect behavior is exercised by dedicated tests (#276). The bridge inversion was itself landed with a red-first e2e: the old "addon-hosts" topology hung on every server restart; the "server-listens" topology survives any restart order. # The JSON envelope Every message across the bridge is a versioned JSON envelope. Failures are structured data an agent can act on — never a stack trace, never silent. ## Shapes ```jsonc // server → addon (command) { "id": "42", "command": "cmd_create_node", "params": { "parent_path": ".", "node_type": "Node2D", "name": "Player" } } // addon → server (success) { "id": "42", "ok": true, "result": { "node_path": "Player", "created": true, "persisted": true } } // addon → server (failure) { "id": "42", "ok": false, "error": "RESOURCE_NOT_FOUND", "hint": "No node at 'Player/Gun'." } // precondition failure — the richer form the agent can act on { "id": "42", "ok": false, "error": "PRECONDITION_FAILED", "hint": "Open a scene before creating nodes.", "required": "active_scene" } ``` ## Rules 1. **Correlate by `id`.** Every response carries the command's `id`; the server keeps one waiter per in-flight command, so many commands can be in flight concurrently without cross-talk. 2. **Stable error codes.** Failures use the enumerated `ErrorCode` set: `PRECONDITION_FAILED`, `RESOURCE_NOT_FOUND`, `VALIDATION_ERROR`, `BRIDGE_DISCONNECTED`, `TIMEOUT`, `INTERNAL_ERROR`, `APPROVAL_DENIED`, `PEER_REPLACED` (a server→addon notice, `ok: false` with no `command`). No ad-hoc strings — clients match on the enum, not on prose. 3. **`hint` is for recovery**, written for an agent with no human in the loop: "Enable Editable Children on 'Relic', or target a node the scene owns" — not a restatement of the code. 4. **Preconditions carry `required`** — the machine-checkable name of what to satisfy: `active_scene`, `confirm`, `bridge_connected`, `runtime_probe`, `game_not_breaked`, `godot_version`, `project_dir`. 5. **Catch at the boundary, both sides.** A GDScript runtime error inside a handler becomes `{ok: false, error: INTERNAL_ERROR, hint: "…failed inside the addon…"}`; a Python exception becomes a FastMCP `ToolError` with the same discipline. Raw traces never reach the client. 6. **Never a partial success** — `{ok: true}` when the effect half-failed is the anti-pattern the persistence and readiness systems exist to prevent. ## Where the shapes live | Side | Module | |------|--------| | Server models | `mcp_server/models/envelope.py` (`CommandEnvelope`, `ResponseEnvelope`, `ErrorCode`) | | Addon builders | `command_router.gd` `_ok()` / `_fail(code, hint, required?)` | | Tool shaping | `mcp_server/tools/_route.py` (`route()`, `run_or_preview()`, `poll_ready()`) | The envelope shape is pinned by contract tests (a fake addon peer asserts the wire bytes); a shape change without a contract-test update is a drift bug by the repo's own rules. ## From envelope to MCP result ```mermaid flowchart LR A["tool call (MCP)"] --> B["safety: preconditions, dry_run/confirm"] B --> C{"dry_run?"} C -->|yes| D["preview + probe (cmd_node_persistence)"] C -->|no| E["Bridge.send {id, command, params}"] E --> F["addon handler (UndoRedo, Editor API)"] F --> G["{id, ok, result}"] G --> H["typed Pydantic model → structuredContent"] B --> H2["ToolError {error, hint, required}"] G --> H3["ok:false envelope → ToolError"] ``` Every tool returns a **typed Pydantic model** — never a raw dict. FastMCP serializes the model into `structuredContent` on the MCP response, so a client gets machine-parseable JSON (with the text fallback for other servers). ## Versioning the envelope The envelope is versioned from day one and has been shape-stable since the first PR: additive keys (`persisted`, `reason`, `hint`, `required`, `scanning`, …) never break a client, because clients ignore unknown fields. That is why `contract_version` has stayed at **1** through every release while the surface grew 194 tools. A *breaking* change (removed/renamed key, changed required param) would bump it — and `godot_get_server_info` carries the counter so clients can negotiate. # Toolset gating A large flat tool surface degrades agent tool-selection and burns context. So the *live* surface stays small: `core` + `inspection` are exposed by default (24 tools), and 27 further toolsets are **gated off** until the agent enables them. ## The enabled set `enable_toolset` / `disable_toolset` write into a **single server-global enabled set** (issue #364). godot-mcp is a single-user, locally-run server — per-session isolation was removed because it was dead code on the sessionless MCP `2026-07-28` protocol (a fresh `session_id` per call made it useless) and violated the no-server-initiated-hook rule. Consequences, which are features: - A grant issued on one connection **persists to a fresh connection** against the same server process — a reconnect between enable and mutate keeps the grant. - All clients see the same surface. There is no per-session isolation to reason about. - The set is seeded at startup from `GODOT_MCP_DEFAULT_TOOLSETS` (`all` or a comma-separated list; unknown names are logged and ignored). The agent-facing protocol text derives its wording from the effective default so it never claims "everything is hidden" when the operator seeded more (#425). ## `tools/list_changed` Since 2026.09.17 (#491/#485), `enable_toolset`/`disable_toolset` emit `notifications/tools/list_changed` after mutating the set. Clients that cache the tool list at session start refresh on the notification. The send is fire-and-forget by design — the toggle already succeeded, so a delivery failure must not fail the tool call — but it is logged, never swallowed silently. On the sessionless `2026-07-28` protocol the spec forbids *unsolicited* change notifications: they reach a client only on a `subscriptions/listen` stream it opens. Since #620 the server registers a `ListenHandler` (backed by an in-memory `SubscriptionBus`, the same wiring mcp's own `MCPServer` uses) on FastMCP's low-level server, so it both **advertises** `tools.listChanged: true` in `server/discover` and **fans** each toggle out to every open listen stream. Registering the method is what flips the advertised flag — the mcp SDK derives it from whether `subscriptions/listen` is served, not from FastMCP's legacy notification options. **Pull-based fallback (#621).** Because many clients still ignore the notification, `enable_toolset`'s result also carries the enabled tools' exposed names, descriptions, and input schemas — so an agent can call them immediately even when its registry never refreshes. The push that keeps the editor dock's Toolsets row honest is fire-and-forget (#624) and never stalls the toggle. The typed tools remain authoritative either way: calling a gated tool whose toolset is disabled fails with a structured gating error, and `_ensure_or_return_error`-style client helpers re-enable idempotently. ## Why the gate is worth its friction 1. **Tool selection sharpness.** With 194 tools exposed, models confuse similar tools and waste calls. The default 24-tool surface fits in context with room to reason. 2. **Safety surface minimization.** Destructive tools (`delete_node`, `reload_scene`, `close_scene`, bus removers, file deleters) are *invisible* until the agent deliberately asks for a toolset containing them. 3. **Version gating** rides the same flag: `scene_edit`, `input_map`, `tilemap`, `scene_3d` require Godot 4.4+ and refuse to enable on older editors with a structured error naming the requirement. ## The toolsets | Domain | Toolsets | |--------|----------| | Scene building | `scene_edit` · `composite` (macro round-trips) · `scene_3d` · `theme_ui` · `tilemap` · `particles` · `navigation` · `physics` · `animation` · `audio` · `shader` · `visual_shader` | | Code & data | `scripts` · `resources_edit` · `project` · `project_scaffold` | | Run & verify | `runtime` · `input` · `testing` · `debugger` · `profiling` | | Bulk & ship | `batch` · `analysis` · `export` · `asset_import` · `editor` | Plus always-on `core` (diagnostics, toolset management, `godot_undo`, `godot_debug_workflow`) and `inspection`. The full per-toolset table (with tool names and counts) lives in the [toolsets reference](../reference-toolsets.md); the authoritative per-tool spec is [`docs/tool-contracts.md`](https://github.com/hybridindie/godot-mcp/blob/main/docs/tool-contracts.md). ## Design guardrails - **New categories register gated off** — added to `TOOLSETS`, never to `DEFAULT_ENABLED`. Growth of the catalog does not grow the default surface. - **Fewer, richer tools over many micro-tools.** Create-with-config (`create_node(…, properties?, script?)`) and batch setters replace one-tool-per-field. One tool per noun with a few clear verbs. - **Never merge across safety classes.** Folding `delete` into a fat `node` tool would bury the `confirm` gate; the class split is deliberate. - **Never a single `do(action, params)` dispatcher** — that discards the per-tool schemas and descriptions that guide the agent. # The safety model Every tool carries a **safety class** that determines its risk and required parameters. The class is metadata on the tool (`meta.safety_class`), and every class change is pinned by contract tests. ## The classes | Class | Risk | Extra params | Examples | |-------|------|--------------|----------| | `read_only` | None | none | `get_scene_tree`, `scripts_read`, `shader_validate` | | `mutating` | Reversible/additive change (UndoRedo) | `dry_run: bool = False` | `create_node`, `set_node_property`, `attach_script`, `set_resource_property` | | `destructive` | Removes/overwrites existing data, or an irreversible disk/project write | `dry_run` **and** `confirm: bool = True` | `delete_node`, `reload_scene`, `close_scene`, `remove_audio_bus`, `delete_resource_file`, `create_resource`, `set_setting`, `cross_scene_set_property`, `register_autoload`/`unregister_autoload`, `disconnect_signal` | | `runtime` | Controls/side-effects game execution | varies | `play_scene`, `run_and_capture`, `export_project`, `monitor_property`, `stop_recording`, `rescan_filesystem`, `evaluate_expression` | ### Classification rule The class follows the effect, not the verb (issue #638): - **Reversible, additive editor edit** (UndoRedo restores it) → `mutating`. - **Removes/overwrites existing data, or writes disk/project state the undo stack does not cover** → `destructive`. That includes resource/scene file writes (`create_resource`, `cross_scene_set_property`), `ProjectSettings` persistence (`set_setting`, `register_autoload`/`unregister_autoload`), and breaking a signal connection (`disconnect_signal`) — even where the editor happens to undo-track the step, the agent must opt in. - **Side-effecting observation** (starts/stops a recorder, starts sampling, triggers a reimport) → `runtime`, not `read_only`. A contract test (`tests/contract/test_safety_classification.py`) pins the class of every tool the audit touched, so a misclassification fails CI. Two knobs do the work: - **`dry_run=True`** runs preconditions and returns what *would* happen. For most mutations the preview carries the same persistence verdict the real run will stamp — the tool sends the read-only `cmd_node_persistence` probe, and the addon answers with the same rule the real handler applies (preview and real run always agree). - **`confirm=True`** is required for destructive tools. Without it: `PRECONDITION_FAILED: This call discards unsaved changes. Set confirm=True to proceed. [required=confirm]`. The server enforces this; the addon honors the flag defensively too. **What `confirm` does and does not protect against.** `confirm` is an *accident guard*: the agent sets it itself, so it stops a tool being called by reflex without deliberation — it is **not** human oversight. Real human-in-the-loop oversight is the opt-in **approval gate** (`GODOT_MCP_APPROVAL_WEBHOOK`, or the elicitation guard with `GODOT_MCP_APPROVAL_REQUIRE`): `mutating`/`destructive` calls — and `runtime` tools flagged `writes_disk`/`executes_code` (`export_project`, `evaluate_expression`) — are routed to it. With a webhook configured, an **unreachable** webhook now fails **closed** (denies) by default (issue #638); set `GODOT_MCP_APPROVAL_FAIL_OPEN=true` to opt into fail-open. ## Preconditions Checked **before** any side effect, and failing with a structured error — never a Python exception: | Precondition | Required token | Recovered by | |--------------|----------------|--------------| | `require_bridge_connected` | `bridge_connected` | open Godot + enable the addon | | `require_active_scene` | `active_scene` | `open_scene` / `create_scene` | | `require_node_exists(path)` | — (`RESOURCE_NOT_FOUND`) | `get_scene_tree` to find real paths | | `require_godot_binary` | `godot_binary` | install Godot / set `GODOT_BIN` | | toolset version gate | `godot_version` | upgrade the editor | | game-not-break gate (input sim) | `game_not_breaked` | `godot_debugger_continue_execution` | ## Where safety lives, and why All safety logic lives in `mcp_server/safety.py` — **never** in the addon. Reasoning: - The addon is the only layer that touches Godot, but it is the *least* trustworthy layer: it runs inside the editor process, next to user code, and is version-coupled to Godot. Keeping it a dumb, auditable executor means a corrupted addon cannot bypass the `confirm` gate. - The server owns the classes/params once, and the same rules apply across stdio, HTTP, and Docker — a safety decision made in GDScript would only protect Godot clients. - The split makes the surface reviewable: `safety.py` (one file) is where an auditor reads the whole permission model. The classes apply **in every transport mode**; transport-level auth (the `GODOT_MCP_AUTH_TOKEN` gate on non-loopback HTTP binds) is a second, separate layer that gates *who can connect at all*. Listener hardening (#636) is the third: the bridge refuses browser-`Origin` WebSocket handshakes and the HTTP transport's Host/Origin guard is on by default, so a visited web page (WebSocket same-origin exemption, DNS rebinding) can't reach the code-executing tools — while a *local* process still only needs the bridge/token, since loopback is single-user by design. ### Batch tools don't bypass the gates A macro/batch tool is a convenience, not a privilege escalation. `run_commands` resolves each sub-command back to the tool that backs it and enforces that tool's policy per entry (#634): a sub-command from a disabled toolset is refused, a destructive entry needs `confirm=True`, params go through the path preflight, and an unknown `cmd_*` is refused — a batch containing a delete is presented to the approval gate as `destructive`, not as the outer tool's `mutating` class. ### Path containment Every file-path parameter is a `ResPath` (a contained `res://` path) — the server rejects a missing scheme, an absolute filesystem path, or a `res://../` escape at the schema boundary, before the bridge round-trip (#637). The addon's own `begins_with("res://")` check is not trusted for this. `run_commands` sub-commands, which bypass the argument schema, are re-checked by the same preflight predicate. Node paths (`Player/Gun`) are not `res://` paths and are exempt. `export_project`'s `output_path` is confined to the project dir: a target outside it is refused unless `confirm=True`. ## Honesty as safety The most dangerous failure mode is a result that reads like success but isn't. The persistence system exists for exactly that: - Structural edits stamp `persisted` with a `reason` + actionable `hint` (see [persistence](persistence.md)). - Batch sets above 20 nodes report `undoable: false`. - `run_commands` reports `aborted_at` / `skipped_count` / an unsaved-scene hint. - Parse checks report `rescan_pending` when they raced an importer scan. - Timeout kills are distinguished from crashes. - A `TIMEOUT` on a **mutating** command carries `may_have_applied: true` + a hint to re-read state: the editor drains commands serially, so a timed-out mutation can still run after the agent gave up (#644). These fields are contract-pinned; skills and evals treat them as ground truth. The rule of thumb in the codebase: **a result never reads as success when the effect half-failed.** ## UndoRedo Every create/rename/delete/set operation registers with `EditorUndoRedoManager` — the human's undo stack covers agent actions. The documented exceptions report themselves: batch sets above 20 nodes bypass the stack for performance (`undoable: false` in the result), and `set_setting`/file-only resource authors are not undo-tracked (their contracts say so). # Persistence truth The engine's save rules are subtle: Godot packs a node only when its `owner` is the edited root or an editable instance, and **a skipped node's subtree is never visited**. So a change that applies live in the editor can silently vanish on save — the worst outcome for an agent that trusts `ok: true`. The persistence system stamps every mutation result with what a save will keep: ```jsonc { "node_path": "Relic/Cold/Extra", "created": true, // the change applied live "persisted": false, // but the save will drop it "reason": "instanced_child_not_editable", "hint": "'Relic/Cold' is inside the instanced scene 'Relic', which does not have Editable Children enabled — the change shows in the editor but will not be saved. Enable Editable Children on 'Relic', or target a node the scene owns." } ``` ## The rule `_persistent_target(node)` (in `command_router.gd`) mirrors 4.7's `SceneState::_parse_node`: ```mermaid flowchart TD N["target node"] --> W{"walk the owner chain to the root"} W -->|"owner == null"| X["not persisted node_not_owned"] W -->|"owner == root"| OK["persisted"] W -->|"owner != root"| E{"is_editable_instance (owner)?"} E -->|yes| OK E -->|no| Y["not persisted instanced_child_not_editable"] ``` Reasons are stable tokens the agent can match on: | Token | Cause | |-------|-------| | `node_not_owned` | no owner in the edited scene (e.g. added by a `@tool` script) | | `instanced_child_not_editable` | inside an instanced scene without Editable Children | | `embedded_in_other_resource` | the edit reaches through a resource embedded in another scene, never re-saved | | `group_from_base_scene` | the group an instanced/inherited scene gives the node is back after reload | ## The deciding rule per mutation family A change is saved only when the packer *visits* the changed node, so the verdict keys on the **nearest ancestor the packer actually writes**: | Family | Keys on | Why | |--------|---------|-----| | creates / instances / duplicates / compose | the **parent** | the target does not exist yet; if the parent is skipped, the whole new node is lost | | `move_node` | the **destination parent** | a node moved into a non-editable instance is lost; the removal from an owned source saves fine | | `delete_node` | the **node itself** | an instance-owned node is restored by the instance on reload; an unowned node was never packed | | property edits / resources | the node, then the resource chain | a sub-resource saves with its node; a resource file saves itself; a sub-resource of another scene is never re-saved | ## Previews agree with reality A `dry_run` preview carries the same verdict as the real run. The tool sends the read-only `cmd_node_persistence` probe (target path, plus the `resource_properties` the real handler would follow, or `probe_parent` / `probe_parent_of` for structural edits), and the addon runs the *same* rule. The probe and the real handler always agree — the e2e suite asserts preview == real == saved file for every family. ## Per-target verdicts for batches `batch_set_property` and `apply_node_edits` target resolution **descends into instanced children**, so their `applied[]`/`edited[]` entries are not uniformly persistent. The batch result carries a `persistence[]` array — one `{node_path, persisted, reason?, hint?}` entry per applied target. A batch is never "ok" in a way that hides a lost target. ## The dead end, closed `instanced_child_not_editable` hints used to say *"Enable Editable Children on '<instance>'"* with no tool to do it. [2026.09.19](../changelog.md) added `godot_scene_edit_set_editable_children` — the action the verdict names — and the scene tree now marks instanced nodes with `owner` / `editable_children: true` so the agent can *see* the situation before acting. ## Where it is enforced - **Addon**: `_persistent_target`, `_resource_persistence`, `_group_removal_persistence` in `command_router.gd`; every Tier-1/2/3 handler stamps via `_with_persistence`. - **Previews**: `mcp_server/tools/_persistence.py` builders (`node_probe`, `animation_probe`, `parent_of_probe`) — the single source of truth for the probe the tool sends. - **e2e**: `test_persistence_e2e.py` — preview == real == saved file for ~40 operations across instanced, inherited, and resource-chain cases. - **Contract version**: additive fields only; `contract_version` stays 1. # The readiness envelope Some handlers cannot complete on the first call: an editor screenshot needs a rendered frame, a property monitor needs the probe's next sample batch, a full-Control UI scan takes a frame or two. The naive design — the handler blocks until the data exists — stalls the bridge, blocks the editor's main thread, or times out with a generic error pointing at the wrong layer. Instead, poll-and-cache handlers **always answer within one round-trip**: ```jsonc // not yet — and here is why { "ready": false, "pending": true, "reason": "capture_pending" } // ready { "ready": true, "samples": [...] } ``` ## The token table | Token | Handler | Meaning | |-------|---------|---------| | `recording_pending` | `godot_input_stop_recording` | the stop push hasn't landed from the probe yet | | `capture_pending` | `godot_runtime_get_property_samples`, game-frame capture | the request is dispatched; the probe answers on a later frame | | `read_pending` | `godot_runtime_read_property` | the one-shot read is dispatched; the probe replies on a later frame (#571) | | `output_pending` | `godot_runtime_get_game_output` (cold start) | the first probe pull is in flight; the ring is empty until it lands | | `scan_in_flight` | `godot_runtime_find_ui_elements` | the full-Control scan runs over the next frames | | `probe_pending` | `godot_profiling_get_performance_monitors` | the probe hasn't answered the first monitor pull | | `rescan_in_flight` | `godot_asset_import_get_status` | the editor's filesystem scan is running; the status read is provisional | | `editor_not_drawing` | `godot_editor_capture_screenshot` | the editor hasn't rendered a frame yet (e.g. minimized/unfocused) | `godot_asset_import_get_status` also reports `scanning: bool` — the same `EditorFileSystem.is_scanning()` read the parse-check gate keys on, so an agent can distinguish "not imported yet" from "the scan hasn't flushed". ## `poll_ready` and the no-silent-stall rule The server-side loop is `poll_ready(bridge, command, params, timeout_ms)`: 1. Make at least one attempt (a fast tool never returns a pending answer). 2. Poll on a fixed cadence until the result is `ready` or the deadline hits. 3. On expiry: if the last result carried a `reason`, the tool raises a `TIMEOUT` error **relaying it** — the agent sees `TIMEOUT: … did not become ready within 2000ms (last reason: capture_pending)` and can act on the cause instead of guessing. The design rule: **no bridge call can stall silently, and every `ready: false` beyond a grace period names its cause.** A handler that can't complete should never hang the transport; the addon is the only layer that knows *why* Godot isn't cooperating, so it says so. ## Grace periods Not every first-poll miss is a failure — captures legitimately defer by a frame. So the pattern is: - **Captures** (screenshots, frames): 2 polls (one frame deferral + slack). - **Probe-based polls**: the standard poll cadence (`DEFAULT_POLL_INTERVAL_SECONDS`), bounded by the tool's `timeout_ms`. A handler may defer normally (one empty `ready: false`); only a *still-pending* handler past its grace window carries the token. In practice every handler listed above emits the token on every non-progress poll — simpler to reason about, and `poll_ready`'s deadline is the real bound. ## Why agents care Without the token, a pending poll and a broken probe are indistinguishable — the agent burns its retry budget on a capture that will never land, or gives up on one that needs one more frame. With it: - `recording_pending` → the game is still flushing the input buffer; wait, or check `input_get_stats`. - `read_pending` → the one-shot read is in flight; poll once more (#571). - `output_pending` → the first output pull hasn't landed; poll once more. - `scan_in_flight` → the UI scan is running; poll once more. - `probe_pending` → the probe autoload is present but slow; check `runtime_is_playing`. - `rescan_in_flight` → the import is *progressing*; keep polling rather than re-importing. - `editor_not_drawing` → the editor isn't producing frames (unfocused window); focus the editor or wait for the next frame. ## Optional # Changelog Full release notes live on [GitHub Releases](https://github.com/hybridindie/godot-mcp/releases). This page summarizes the recent ones. ## 2026.10.15 — Security hardening, honest results, and a leaner agent surface A deep review pass across the whole server and addon: the safety/permission model is now enforced end to end, partial failures are reported honestly instead of as success, the debugger tells the truth about what the engine cannot do, and the tool surface is easier for an agent to select from. Surface: **194 tools** (unchanged). Shape is additive — `contract_version` stays 1 — with one behavioural note below. - **Security (#634–#638, #649, #650)** — `run_commands` now enforces each sub-command's own safety class; the bridge authenticates a new peer *before* evicting the active one; the WebSocket/HTTP listeners check `Origin`/`Host`; every file-path parameter is contained to `res://`; the safety-class audit reclassified six tools (`create_resource`, `set_setting`, `register_autoload`/`unregister_autoload`, `cross_scene_set_property`, `disconnect_signal`) as **`destructive`** and made code-execution/disk-write tools approval-gated with a **fail-closed** default; the publish workflow builds the tagged commit, gates on tests, and pins actions by SHA; every CI job guards against fork PRs. - **Honest results (#640–#645, #670/#671)** — asset edits land in the scene undo history; `type_coerce` covers the full shape set and property writes are verified by read-back; `own_recursive` no longer descends into instanced sub-scenes; the over-threshold batch path marks the scene dirty; a timed-out mutation is flagged `may_have_applied`; `cross_scene_set_property` validates tabs and ownership; runtime and debugger reads are request-scoped so a stale reply cannot be mistaken for the current one. - **Debugger & runtime (#677, #648)** — `evaluate_expression` runs in-game through the probe; `get_stack_frames`/`get_frame_variables` return a structured `UNSUPPORTED` on Godot 4.7 instead of a wrong answer; the probe stays parseable on the 4.4 floor. - **Reliability & lifecycle (#655, #675, #647, #646, #683)** — bridge/runtime lifecycle leaks closed; the leaked runtime-probe autoload is removed from the project; addon scan re-entrancy, oversize replies, and property type names fixed; autoload names validated and save errors surfaced; visual-shader create is redoable. - **Agent experience (#652, #653, #654, #691)** — every tool reference in hints and docstrings resolves; tool schemas gained description budgets and enums; known-shape `dict` params are typed; `enable_toolset` accepts a bundle (`author`/`playtest`/`ship`) or a comma-separated list, collapsing the setup loop to one call. - **CI & docs (#651, #672, #656)** — live-editor e2e coverage closed and the play-session e2e runs under a virtual display; a portable hook plus a version-consistency test keep the CalVer copies in lockstep; docs, skills, and examples refreshed against the final surface. > **Behavioural note:** the six tools moved to `destructive` now require `confirm=True` > (default `False`). Calls that omitted it and previously succeeded must now pass it. ## 2026.10.12 — Version awareness, a resilient addon, and a friendlier site The server can now tell you when it — or the addon — is out of date, the addon can no longer silently drop an import, and the docs are rebuilt around getting you to a first working session fast. Surface: **194 tools** (one new). Additive — `contract_version` stays 1. - **Version drift & update check (#629)** — `godot_health_check` and `godot_get_server_info` report the connected addon's version and flag a server↔addon mismatch; a new `godot_check_for_updates` tool (opt-in via `GODOT_MCP_CHECK_UPDATES`, off by default — no silent phone-home) reports whether a newer release exists, and a known update is surfaced in `get_server_info` and the editor dock. - **Reliable asset import (#628)** — `godot_asset_import_asset` no longer silently drops its filesystem scan when one is already in flight: the addon defers and re-issues it, so a freshly copied asset actually imports (and reports `scan_deferred` when it queued one). - **The addon is "Godot Editor MCP - Addon"** — its in-editor plugin name and its Godot Asset Store listing now match. - **A friendlier site** — a creator-first home, a getting-started path that leads with the **skill** (so your assistant enables toolsets itself), and a clearer split between the hands-on guides and the deep "Under the hood" reference. The `godot_*` calls are the assistant's; the docs no longer ask you to type them. ## 2026.10.10 — Gating that works on the sessionless protocol Toolset gating now works end to end on the `2026-07-28` sessionless protocol, and the server no longer trusts the client to refresh its tool list. Surface: **193 tools** (unchanged). Additive — `contract_version` stays 1. - **`subscriptions/listen` served and advertised (#620)** — the server registers a `ListenHandler` (backed by an in-memory bus) on FastMCP's low-level server, so the modern handshake advertises `tools.listChanged: true` and a subscribing client receives `notifications/tools/list_changed` after a toggle. Previously the flag was advertised as `false` on the modern era, so capable clients (Claude Code CLI) never subscribed. - **Self-sufficient enable result (#621)** — `godot_enable_toolset` returns each enabled tool's name, description, and input schema, so a client that ignores the notification (most of them) can still call the tools straight from the result. - **Fire-and-forget dock push (#624)** — the editor dock's toolset row is pushed with the new `Bridge.notify` (no awaited reply), so a silent addon no longer stalls `enable_toolset`/`disable_toolset` for the 10s request timeout. - **MCP stack upgrade (#622)** — FastMCP `4.1.0` / `mcp` `2.3.0`, with `ResultValidationMiddleware` adapted to FastMCP 4.1's `_ToolBodyError` body-error tag so a result-shape mismatch stays a structured `INTERNAL_ERROR`. - **Front-door README + cleanup** — the README is now a short landing pointing at this docs site; stale historical docs and non-OpenCode harness artifacts were dropped. ## 2026.09.30 — Dock feedback, peer-replacement notice, authoring docs The editor dock becomes a trustworthy supervisor's view, plus two additive bridge-contract changes and a documentation pass. Surface: **193 tools** (unchanged). Additive — `contract_version` stays 1. - **Dock logs command outcomes (#589)** — the recent-command log is fed from completion, not dispatch: `✓` on success, `✗ ` on failure, so a failed command reads differently from a successful one. - **Replaced-editor state (#593)** — when a second editor takes over the bridge, the server sends the replaced peer a `PEER_REPLACED` notice before closing it; the dock shows a distinct **"Replaced by another editor"** state (never a misleading green) and stops reconnecting. New additive error code `PEER_REPLACED`; reconnect attempts and auth refusals now land in the dock log too. - **Visible UndoRedo guarantee (#590)** — a "Last action" row shows the most recent undoable change and how to revert (Ctrl+Z). - **Dirty-scene + play/probe rows (#591)** — the scene row marks unsaved edits (`main.tscn ●`); a "Playing" row shows the running scene and probe state. - **Toolsets row fed from the server (#592)** — `cmd_toolsets_update` pushes the enabled-set on connect and after every toggle, so the dock no longer shows a permanent `(none)` (unknown until the first push, honestly). - **Dock log polish (#594)** — depth 10 → 50 (survives a 20-command batch), a *Copy recent commands* button, and a session-scoped command count. - **Skills: correctness + authoring guides (#611, #614)** — dropped stale hardcoded counts (defer to the live inventory), documented the `godot://` resource surface, fixed the per-project addon guidance, and added five authoring reference guides to `godot-expert` (TileSet/GridMap, VisualShader, text shaders, theme/UI, particles/animation/navigation). ## 2026.09.28 — One-shot runtime reads, screenshot transport, probe hardening Robustness and honesty pass over the runtime-session surface, plus one new tool. Surface: **193 tools** (was 192; +`godot_runtime_read_property`). All changes additive — `contract_version` stays 1. - **Dedicated one-shot property read (#571)** — `godot_runtime_read_property` (`read_only`): reads a live property once without disturbing the monitor capture; mid-capture reads and stale-`request_id` polls are safe (#577). - **Full-screenshot transport (#563)** — raised WebSocket buffer caps on both sides so full-resolution captures transfer instead of truncating. - **Probe runs while paused (#565)** — the runtime probe keeps processing frames while the SceneTree is paused, so captures don't stall behind a `get_tree().paused` game. - **Physical-keycode binding probe (#570)** — injected keys match physical-keycode (`KEY_`) bindings, not just keycode ones. - **Result models are JSON-strict (#580)** — addon-produced fields on result models are strictly typed; malformed bridge payloads surface as `INTERNAL_ERROR` instead of passing through (#567). - **`ui_elements` replies gated on the pending request_id (#577)** — a stale capture's late reply can no longer resolve the wrong waiter. - **CI hardening (#579, #583, #587)** — setup-uv pinned (no manifest fetch), the e2e suite bounded with `timeout --kill-after`, stray godot processes reaped; previously-unverified smokes wired into CI. - **Proactor `ConnectionResetError` demoted to DEBUG (#572)** — benign disconnect noise no longer logs at ERROR. - **Timer-based filesystem auto-refresh (opt-in)** — the editor can pick up external edits on a timer; off by default. - **Class info accepts Variant-shaped defaults (#564)** — `describe_class` no longer chokes on Variant-typed property defaults. ## 2026.09.23 — Prefab workflow, snapshot verification, C# authoring Closes the authoring gaps: the editor's inverse moves, one-round-trip verification, and C# authoring behind a capability gate. Surface: **192 tools** (was 184). All changes additive — `contract_version` stays 1. - **Extract subtree to PackedScene (#531)** — `godot_scene_edit_extract_scene` (`mutating`): the editor's "Save Branch as Scene" — packs a subtree into a reusable `.tscn`, optionally replacing it with an instance in one UndoRedo action; refuses non-owned/mixed-ownership subtrees (#477 family). - **Move/rename project file (#532)** — `godot_project_move_file` (`mutating`): `res://` move with project-wide dependency remap (path + uid forms), `.uid` sidecar relocation, uid re-point; structured refusals for missing source / existing destination / open-and-unsaved scene; honest `undoable: false` with a recovery hint. - **Snapshot + diff a subtree (#535)** — `godot_inspection_snapshot_subtree` + `godot_inspection_diff_snapshots` (both `read_only`): verify a batch mutation in one round-trip; server-side bounded LRU store; group changes surface as `property: "groups"`. - **Push-on-change property sampling (#536)** — `monitor_property` dedupes unchanged values by default (epsilon-tolerant floats, recursively through Vector2/Color shapes); `requested`/`dropped_duplicates`/`sampling_usec` honesty stats; `on_change_only=False` restores legacy sampling. - **Optional bridge auth token (#538)** — `GODOT_MCP_BRIDGE_TOKEN` on both sides; the addon authenticates as its first message; a mismatch is refused at the handshake with a structured envelope (reconnect loop shows the reason); unset = byte-identical no-auth path. Never logged. - **C# Phase 1 (#207)** — language-aware script authoring: read/write/list accept `.cs` (default GDScript, contracts unchanged); `list` gains `language=gd|cs`; a `.cs` write carries a validate-via-build hint (never a false parse-OK); parse/patch refuse `.cs` until Phase 2 (#560); `get_server_info().bridge.backend` surfaces `{csharp_supported, csharp_project, csharp_build}`. - **ClassDB discovery (#533)** — `godot_describe_class`: properties/methods/ signals for agent discovery before setting. - **Game console output (#534)** — `godot_runtime_get_game_output`: the running game's stdout/stderr/script errors from a bounded ring. - **Server↔addon handshake (#530/#521)** — `cmd_get_addon_info` cached per peer; `get_server_info` exposes `addon_version`/`addon_commands` + a drift warning; the dock labels `godot-mcp / Godot `. - **Peer identity (#537)** — the addon announces project path/Godot/addon versions at connect; peer replacement is logged naming both paths. - **Undo/redo parity (#529)** — `godot_redo` + `godot_list_history`. - Refactors: single-sourced add-child undo commits (#528), unified probe/break guards (#526), shared helpers extraction (#546), honest `command_completed` exec_ms (#520), prop-cache staleness fix (#540), dock status-icon texture cache (#539). ## 2026.09.19 — Persistence truth everywhere + verification tools Completes the persistence-truth family, formalizes the readiness/reason envelope, adds the verification tools the "advertised verification" trust gap needed. Surface: **184 tools** (was 181). All changes additive — `contract_version` stays 1. - **Persistence truth for structural edits + batches (#477)** — parent rule for 13 create-family handlers, destination rule for moves, node rule for deletes, per-target `persistence[]` verdicts for `batch_set_property` and `apply_node_edits`; live e2e asserts the saved file agrees with every verdict. - **`godot_scene_edit_set_editable_children` (#487)** — the action the `instanced_child_not_editable` hints name; the tree marks instanced nodes with `owner` / `editable_children`. - **Readiness/reason envelope (#456/#457/#459)** — every poll handler carries a stable `reason` token; `poll_ready` relays it in timeout errors. - **`godot_shader_validate` (#423)** — real headless engine compile check; `get_parse_errors` documented as syntax-level. - **Audio effect property echo (#427)** — `get_bus_layout` echoes per-effect `properties` (read/write parity). - **AudioStreamWAV loop settings (#418)** — `import_asset` accepts `loop_mode`/`loop_begin`/`loop_end` for `.wav`; `loop_applied` verdict. - **`godot_scene_edit_rescan_filesystem` (#486)** — pick up external file edits non-destructively. - Skills teach the honesty semantics + round-trip economy (#501). ## 2026.09.17 — Honest reporting & trust-class fixes - Timeout ≠ crash: no false `NON-ZERO EXIT`; `expected_timeout` param (#490). - `run_commands` reports `aborted_at`/`skipped_count` on early stops (#437). - `batch_set_property` reports `undoable` (false above 20 nodes) (#461). - `set_setting` refuses unknown keys with a did-you-mean hint (#462). - Input simulation refused while paused at a debugger break (#443). - `get_parse_errors` gates on the editor's filesystem scan (`rescan_pending`) (#453). - Addon RefCounted leak cycles fixed (#492, by Vincent Huang). - `tools/list_changed` emitted on enable/disable (#491). - Toolset-gating text reflects the effective `GODOT_MCP_DEFAULT_TOOLSETS` (#425). - OpenWebUI setup guide (#484, by Randall Lasini). ## 2026.09.10 — Game frame capture + silent-failure hardening - Live game screenshots/frames via the probe; silent-failure fixes across persistence previews (#458/#475/#476); debugger session hygiene (#454). ## Earlier See the [releases page](https://github.com/hybridindie/godot-mcp/releases) for the full history (2026.09.02 "First Stable", 2026.08.31b4, …).