gdcli¶
A CLI that lets an agent drive a running Godot editor through its own APIs, instead of
patching .tscn text or installing an MCP server that floods the prompt with tool schemas.
Why not the alternatives¶
MCP server¶
the tool schemas (dozens of them) sit in the conversation forever. The transport is not the problem; the surface is. A CLI keeps the surface in the shell.
Editing .tscn by hand¶
inheritance and instanced scenes are not representable as text patches without a lossless parser, and a bad patch makes the engine refuse the file.
Runtime PackedScene round-trip (instantiate() + pack())¶
flattens the inherited root (godot#27243) and loses instance ownership.
PTY/AT-SPI/xdotool automation¶
Godot's AccessKit tree exposes editor chrome only (buttons, tabs, dock shells) on current release (4.7.2) : 107 nodes, zero tree item, zero menu item, no EditableText. Scene content and menus are invisible to it.
PTY-free GUI process injection¶
the release binary is fully stripped (nm symtab 0), so gdb call needs a per-version C++ shim; and injection only reaches the same EditorInterface API a plugin already has.
A plugin is the only supported way in, and it is the cheapest of all of them.
Architecture¶
agent/human ──▶ gdcli (Rust) ──NDJSON/tcp 127.0.0.1 ──▶ EditorPlugin (GDScript)
├─ EditorInterface
├─ EditorUndoRedoManager
└─ EditorFileSystem
opt-in: ──gdcli:req/gdcli:res (debugger)──▶ GdcliRuntime (game autoload)
Layout¶
assets/addons/gdcli/ GDScript plugin, embedded into the binary
plugin.cfg name, version, entry script
plugin.gd lifecycle, socket, main-thread queue, deferred replies
debug_capture.gd EditorDebuggerPlugin: ring buffer and game-message routing
runtime/gdcli_runtime.gd in-game autoload: bridge, log capture, screenshots
commands/*.gd one module per command group (meta, editor, scene, node, prop,
signal, group, res, project, fs, script, game, input)
src/ CLI: clap surface, wire client, installers, renderers
docs/ design + protocol
tests/fixture/ throwaway Godot project (instanced scene, prints on run)
scripts/e2e.sh headless and xvfb end-to-end suite
scripts/install.{sh,ps1} release installers
What the editor API actually can and cannot do¶
By dumping the ClassDB, we can see what the editor API actually can and cannot do.
Available: scene open/close/save/save-as/reload, open + unsaved scene roots, node
add/remove/reparent/duplicate/rename/instance, property read/write/reset on any object (nodes
and res:// resources), signal connect/disconnect, groups, resource create/inspect/duplicate,
script check/open/save, project settings, autoloads, input map, plugin enable/disable,
filesystem scan/reimport, editor UI (main screen, selection, inspector, toasts, play/stop,
2d/3d/whole-window screenshots), writes to the command palette, and — through the opt-in game
runtime — the running game's scene tree, properties, console output and screenshots.
Not possible, and therefore not exposed:
- Editable children / editing inside an instance.
Nodeonly hasset_scene_instance_load_placeholder; the editable flag has no scripting API. - New inherited scene.
EditorSceneManageris C++-only;SceneStateis read-only. - Gizmo drags and viewport picking. Set transforms as properties instead.
- Editing a non-current open scene.
get_open_scene_roots()reads them; writing means switching the active scene, which is visible to the user. - Sending commands to the running game needs a project-side autoload, installed with
gdcli runtime install: the editor sendsgdcli:reqandGdcliRuntimeanswersgdcli:res. Without it everygame.*call isNO_GAME. - Reading a local game's stdout through the debugger. 4.7 only carries
game_view:*messages, so the runtime installs aLoggerand forwards the game's own output; that is whatgame.logsreads. Without the autoload it stays empty. - Switching built-in bottom panels. Only panels owned by the plugin are addressable via
EditorPlugin; the rest would need UI-tree spelunking that breaks across versions. - Listing or invoking someone else's command palette entries.
EditorCommandPaletteexposes onlyadd_command/remove_command, so the plugin registers and lists its own.
Side effects to document for users: saving through the engine rewrites the scene header
(adds unique_id, reorders uids), so diffs are noisy; the plugin's undo entries appear in
the user's history as gdcli: <cmd>.
Features¶
Scene editing — scene open/save/save-as/reload/roots/tree, node
add/rm/mv/dup/rename/instance, property get/set/list/reset on nodes and res://
resources, signal connect/disconnect, group add/remove, editor undo/redo/history, and
--dry-run on every mutation.
Project data — project settings, autoloads, the input map (actions plus key, mouse and joypad bindings), plugin enable/disable, filesystem scan/reimport, and script check/open/save.
Editor UI — selection, inspector, main screen switching, toasts, 2d/3d/whole-window
screenshots, and play/stop with --wait.
Running game (opt-in, gdcli runtime install) — game ping/tree/get/set/eval,
screenshot, and captured logs; requests time out after 3 s instead of blocking the
editor.
For agents — --json and --quiet on everything, batch for NDJSON pipelines, raw
for commands the CLI does not wrap, one undo action per request, and --private to keep the
plugin out of a shared repository.
For humans — a few gdcli: entries in the editor's command palette that trigger the same
actions from the editor side.
Testing¶
scripts/e2e.sh drives a real editor: a headless pass covers commands and asserts the saved
.tscn keeps instance=ExtResource and its overrides; E2E_GUI=1 adds an xvfb pass that
checks screenshots are real PNGs, the game runtime round-trips, and game output is captured.
CI runs it together with cargo fmt/clippy/test and cargo test on macOS and Windows.
Tooling note: the editor's command-line scene argument races the first filesystem scan, so a
client that connects as soon as endpoint.json exists may see no edited scene yet —
gdcli scene open (or a retry) is the reliable path.
Tooling note for GUI tests: run the editor with WAYLAND_DISPLAY unset and
--display-driver x11 under xvfb-run. With WAYLAND_DISPLAY inherited, the editor runs on
the real Wayland session and the game it spawns (--display-driver wayland --wid ...) never
initialises its scene tree, so the runtime autoload never runs and every game.* call times
out.
Conventions¶
- Every mutating request is one undoable action, validated before it mutates.
Node.get_path()is never used as an address (it returns editor-internal paths).- The plugin never blocks the main thread; slow work returns an
opid. - The plugin must survive bad input: validate arguments, return
BAD_ARGS, never crash the editor, because a plugin crash takes the user's unsaved work with it. - One version string,
assets/addons/gdcli/plugin.cfg.Cargo.tomlmust carry the same value andsrc/assets.rsfails the build if it does not;plugin.gdand the runtime read it from that file at runtime instead of declaring a constant, andscripts/e2e.shasserts CLI == installed plugin.cfg == live plugin == runtime version. - An exported game does not ship
plugin.cfg, sogame.ping.runtimereportsunknownthere. The bridge only matters when the game runs from the editor, so that is acceptable.