Date: 2026-04-15
Exercise: Build a full cyberpunk HUD + pause menu in test_project/ using only MCP tools.
Purpose: Stress-test UI, theme, animation, input, autoload, and signal tools under real composition pressure.
Built a cyberpunk-styled HUD with health/shield bars, ability cooldowns, ammo counter, log feed, and a full pause menu with settings panel. All authored via MCP tools -- zero hand-edited .tscn/.tres/project.godot.
Approximate MCP calls by family:
node_*: ~15 (create, reparent, set_property, delete)ui_build_layout: 6 calls (TopLeft, TopRight, BottomLeft, BottomRight, LogFeed, PauseOverlay)theme_*: 12 calls (1 create + 7 styleboxes + 4 colors + 2 font sizes + 2 constants + 1 apply)animation_*: 12 calls (1 player_create + 4 create_simple + 2 create + 3 add_property_track + 1 add_method_track + 1 set_autoplay)input_map_*: 9 calls (3 add_action + 6 bind_event)script_*: 4 calls (2 create + 2 attach)autoload_*: 1 call (add)signal_*: 2 calls (both failed -- deliberate poke)scene_*: ~8 calls (create, open, save, get_hierarchy)batch_execute: 1 call (7 node_create ops)editor_*: ~6 calls (state, screenshot, logs)
Where composers held up: ui_build_layout handled all static layout including 13-node deeply nested PauseOverlay in one atomic call. animation_create_simple handled multi-target tweens (same node different properties) and multi-tween clips cleanly. theme_* covered all standard slots.
Where I fell back to low-level: animation_add_property_track for multi-keyframe shake animation (3+ keyframes per track). node_set_property for all theme_override_* properties. animation_add_method_track for method-call tracks.
- Tool:
theme_create - What I tried:
theme_create res://themes/cyberpunk.treswhenthemes/dir didn't exist - What happened: Error 19 (ERR_FILE_NOT_FOUND or ERR_FILE_CANT_WRITE)
- What I wanted: Auto-create parent directories
- Workaround: Call
filesystem_write_text res://themes/.gitkeepfirst to create the directory - Proposed tool change:
theme_create(and other file-creating tools) shouldDirAccess.make_dir_recursive_p()before saving
- Tool:
node_rename - What I tried: Rename
/cyberpunk_hud(scene root) toHUD - What happened:
INVALID_PARAMS: Cannot rename the scene root - What I wanted: Scene root renamed to
HUD - Workaround: Worked with the auto-generated name
cyberpunk_hud - Proposed tool change: Allow renaming the scene root -- Godot's editor UI allows this
- Tool: (missing)
- What I tried: Instance
cyberpunk_hud.tscnintomain.tscn - What happened:
node_createonly creates by type, not by scene reference. Noscene_instanceor similar tool exists. - What I wanted:
node_createwith ascene_pathparameter, or a dedicatedscene_instancetool - Workaround: Created a runtime loader script (
hud_loader.gd) thatpreloads andinstantiates the scene - Proposed tool change: Add a
scene_instancetool or extendnode_createwith an optionalscene_pathparameter
- Tool:
editor_screenshot - What I tried:
editor_screenshot source="game"while the project was running - What happened: Error:
'source'(KeyError-style) - What I wanted: Screenshot of the running game view
- Workaround: No workaround -- could only capture the editor viewport, which doesn't show the CanvasLayer HUD
- Proposed tool change: Fix the
source="game"path in the screenshot handler
- Tool:
theme_set_stylebox_flat - What I tried: Wanted thick bottom border + thin top border for a neon underline effect
- What happened: Only
border_width(uniform all sides) is available - What I wanted:
border_width_top,border_width_bottom,border_width_left,border_width_right - Workaround: Used uniform border width
- Proposed tool change: Add per-side border width parameters (StyleBoxFlat supports this natively via
border_width_top/bottom/left/right)
- Tool:
theme_apply - What I tried: Apply theme to the scene root (
CanvasLayer) - What happened:
INVALID_PARAMS: Node /cyberpunk_hud is not a Control or Window (got CanvasLayer) - What I wanted: Theme applied to cascade to all children
- Workaround: Created a
ThemeRoot(Control, full_rect) under the CanvasLayer, reparented all regions under it, applied theme there - Proposed tool change: Could auto-detect this pattern and suggest the workaround, or
ui_build_layoutcould accept athemeparameter that auto-wraps
- Tool:
ui_build_layout - What I tried:
"theme_override_colors/font_color": "#00eaff"in properties dict - What happened:
INVALID_PARAMS: Property 'theme_override_colors/font_color' not found on PanelContainer - What I wanted: Theme overrides settable in the layout spec
- Workaround: Build layout first, then call
node_set_propertyfor each override - Proposed tool change: Support
theme_override_*property paths in the layout builder's property coercion
- Tool:
signal_connect - What I tried: Both
/root/GameStateandGameStateas source path - What happened:
INVALID_PARAMS: Source node not foundfor both forms - What I wanted: Wire signals from an autoload to a scene node
- Workaround: Connected signals in GDScript code (
_ready()function) - Proposed tool change:
signal_connectcould resolve autoload paths by checking the ProjectSettings autoload registry, not just the edited scene tree
- Tool: (missing)
- What I tried: Recreate
damage_shakeanimation after node paths changed - What happened:
INVALID_PARAMS: Animation 'damage_shake' already exists. Delete it first or choose a different name. - What I wanted: Delete or overwrite an existing animation clip
- Workaround: Created new clips with incremented names (
dmg_shake2,dmg_shake3) -- left stale clips in the library - Proposed tool change: Add
animation_deletetool. Also consider anoverwriteparameter onanimation_create
- Tool:
scene_open - What I tried: Open a scene immediately after
project_stopreturned success - What happened:
EDITOR_NOT_READY: Editor is in play mode — stop the game firsteven thougheditor_stateshowedis_playing: false - What I wanted: Scene opens after stop completes
- Workaround: Wait 3-5 seconds and retry
- Proposed tool change: Readiness gating should wait for the play-state transition to fully settle, or
scene_openshould internally retry
- Tool:
theme_set_stylebox_flat - What I tried: Needed asymmetric content margins (more padding top for header areas)
- What happened: Only
content_margin(uniform) available, same forcorner_radiusandborder_width - What I wanted:
content_margin_top,content_margin_bottom,corner_radius_top_left, etc. - Workaround: Used uniform values
- Proposed tool change: Expose per-side parameters -- StyleBoxFlat supports all of these natively
- Tool:
animation_* - What I tried: Restructured node tree (moved HealthBar deeper), existing animation tracks still pointed at old paths
- What happened: Animations silently target non-existent paths at runtime (no error, just no effect)
- What I wanted: Either a way to update track paths, or a validation tool that flags broken track references
- Workaround: Recreated animations with new names (see #9) pointing at new paths
- Proposed tool change: Add
animation_update_track_pathor aanimation_validatetool that checks track paths resolve
| # | Poke | Result |
|---|---|---|
| 1 | ui_build_layout can't express theme_override_* |
Confirmed -- falls back to node_set_property |
| 2 | StyleBoxFlat uniform-side limitation | Confirmed -- no per-side border width |
| 3 | Tween coercion on self_modulate, custom_minimum_size |
Works -- hex strings for Color, dict for Vector2 both coerce correctly |
| 4 | animation_create_simple duplicate-target |
Partial -- same node + different property works fine; only same node + same property is rejected (better than expected) |
| 5 | Animating stylebox property inside theme resource | Not tested -- used self_modulate on ProgressBar as planned workaround |
| 6 | signal_connect target form for autoloads |
Both forms fail -- neither /root/GameState nor bare GameState resolves |
| 7 | input_map_bind_event modifiers + multiple bindings |
Works -- Shift+E and plain E coexist on ability_2, input_map_list shows both correctly |
| 8 | animation_create_simple with many concurrent tweens |
Works -- tested 2 tweens on same node (different properties) in one call |
| 9 | Atomic undo across batch_execute |
Works -- 7-node batch created atomically with undoable: true |
| 10 | Scene instancing gap | Confirmed -- no MCP tool for instancing a scene into another scene |
| 11 | Theme apply before vs after children | Moot -- theme applied to empty container first, children added after; cascade worked once children existed |
These gaps were surfaced by the exercise:
- Multi-track composer test:
animation_create_simplewith 2+ tweens targeting the same node but different properties - Theme cascade + override interplay: Apply theme to parent, then set
theme_override_*on a child, verify both apply signal_connectto autoload: Test and document the error clearly (currently generic "not found")- Realistic
batch_executeworkflow: batch that creates nodes + sets properties + applies anchor presets as sub-commands theme_createmissing directory: Test error path and verify error message is actionableeditor_screenshot source="game": Integration test for game-view capture- Deep
ui_build_layoutundo: 10+ node deep tree, verify single Ctrl+Z undoes the whole tree - Scene instancing workflow: Document the gap and test the runtime-loader workaround pattern
| File | Method | Description |
|---|---|---|
cyberpunk_hud.tscn |
scene_create + MCP tools |
HUD scene with all regions |
themes/cyberpunk.tres |
theme_create + theme_set_* |
Full cyberpunk theme |
autoload/game_state.gd |
script_create |
GameState singleton |
cyberpunk_hud.gd |
script_create |
HUD controller script |
hud_loader.gd |
script_create |
Runtime scene instancing workaround |
project.godot |
autoload_add + input_map_* |
Modified: autoload + 3 input actions |
Confirmed shipped since the original exercise: animation_delete, per-side
StyleBoxFlat params (border/corners/margins/shadow dicts),
editor_screenshot source="game", overwrite on animation_create*.
New friction during the polish:
- Tool: N/A (setup)
- What I tried: Auto-enable
godot_aion a fresh project so MCP tools work end-to-end without the user touching the UI. - What happened: Without the plugin enabled there's no MCP session, so we
can't call
EditorInterface.set_plugin_enabled. Writing[editor_plugins] enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg")intoproject.godotbefore first launch gets stripped because Godot hasn't indexed the addon yet on first boot. - Workaround: Launch Godot once (indexes addons), then add the entry and relaunch. Or have the user tick the checkbox once.
- Proposed tool change: Ship a
script/install-plugin <project-path>that symlinks + does the two-phase boot, or document the sequence.
- Tool:
node_create,ui_set_anchor_preset - What I tried: Corner-tick ColorRects pinned to
HealthGroup's four corners to frame the panel. - What happened: Direct children of
PanelContainerare stretched to fill its rect.ui_set_anchor_preset top_lefthas no visible effect inside a Container. - Workaround: Skipped corner ticks. The v3 pass fixed it by reparenting
the overlay to the grandparent (a
MarginContainerwhose children aren't re-anchored once they have a custom layout, effectively still forcing a single-rect fill, but sitting on the outer edge). - Proposed tool change:
ui_set_anchor_presetcould detect "parent is a Container" and return a helpful error pointing to the workaround, or aui_float_within(panel_path, corner)helper that wraps + places correctly.
- Tool:
animation_preset_pulse - What I tried: Drive a blinking cursor via modulate-alpha ping-pong
(
property=modulate:a,from=0.2,to=1.0). - What happened: The preset hard-codes
scaleas the animated property;propertyisn't a param. Only thefrom_scale/to_scalenaming hints at that. - Workaround:
animation_create_simplewith amodulatetween andloop_mode=pingpong. - Proposed tool change: Either rename to
animation_preset_scale_pulse, or generalise withproperty/from/toparams.
- Tool:
animation_set_autoplay - What I tried: Autoplay
hud_fade_inAND start thecursor_blinkidle loop together on scene load. - What happened: Autoplay takes one clip name, AnimationPlayer plays one clip at a time; the blink couldn't start until the fade finished.
- Workaround:
ap.queue("cursor_blink")in_ready()after the fade play call. Queue mechanism runs cursor_blink once the intro ends. - Proposed tool change: Document the queue pattern in
animation_set_autoplay's docstring, or add aanimation_queuetool that sets up the queue without a script edit.
- Tool: N/A (HTTP transport)
- What I tried: Long sequence of
node_create+node_set_propertycalls (Phase C ability polish, ~40+ calls). - What happened: Mid-stream
Session not found/No active Godot session. Plugin log showedMCP | disconnected (code 1001)followed by reconnect attempts. After reconnect the session was no longer "active" and neededsession_activateagain. - Workaround: Pin every call with an explicit
session_idonce the drop happened, and re-activate on each reconnect. - Proposed tool change: Either preserve active-session identity across reconnects, or surface a clearer "session disconnected" error.
Plugin v1.1.0 shipped control_draw_recipe (the tool proposed in the v2.5
planning prompt) and the _mcp_game_helper autoload. Using them to rebuild
v2's decorations and add radar/gauge/crosshair/waveform/scanlines surfaced:
- Tool:
node_reparent - What I tried:
node_reparent /cyberpunk_hud/ThemeRoot/TopLeft/HealthGroup/Bracketswithnew_parent=/cyberpunk_hud/ThemeRoot/TopLeft(moving Brackets up one level to its grandparent). - What happened:
INVALID_PARAMS: Cannot reparent a node to itself or its descendant. The destination is an ancestor, not a descendant — the guard is rejecting the wrong case. - Workaround:
node_delete+node_createwith attach + property set (loses undo atomicity, doubles the call count). - Proposed tool change: Fix the ancestor/descendant check. A node's grandparent is a legal reparent target (you can do this in the Godot editor via drag-and-drop).
- Tool:
editor_screenshot - What I tried: After pulling a newer plugin and calling
editor_reload_plugin(which rotated the session ID), immediately take a game-view screenshot while the previously-running game was still alive. - What happened: 15s timeout on the IPC. The running game process had
the old plugin's
_mcp_game_helperbound, so the debugger-channel relay the new plugin uses couldn't reach it. - Workaround:
project_stop+project_runto respawn the game under the new plugin, then screenshot works. - Proposed tool change:
editor_reload_plugincould stop any running game automatically (or at least return a warning flagging the stale autoload).
- Tool:
animation_create_simple - What I tried: Two tweens on the same
self_modulateproperty — first a fast fade to red (0→0.05s), then a slow fade back to white (0.05→0.3s). - What happened:
INVALID_PARAMS: Duplicate tween target '...:self_modulate' — merge keyframes into a single track via animation_add_property_track instead of two separate tweens. - Workaround: Did exactly that —
animation_create+ oneanimation_add_property_trackwith 3 keyframes. The error message is accurate and actionable, so really this is a UX suggestion. - Proposed tool change:
animation_create_simplecould auto-merge when two tweens on the same target have non-overlapping time ranges and the second'sfrommatches the first'sto(the common "flash then settle" case). Would save the caller from rewriting.
Built the v3.2 "cyberpunk angular HUD" look on top of v3 — polyline frames
with diagonal corner cuts, chevron header bands, ruler tick marks, inner
double-stroke, corner flags, plus an orbiting data-packet dot and scanline
drift. Two net-new scripts (angular_frame.gd, orbit_dot.gd), two
patched scripts (scanlines.gd, cyberpunk_hud.gd), and three new scene
nodes on hud_v3.tscn.
- Tool: session lifecycle
- What I tried: Activate the
cyberpunk-hud-demosession and take a game-view screenshot. The session listedplugin_version: 0.0.1(wrong — current is 1.1.0). - What happened: Screenshots silently timed out.
addons/godot_aiwas a symlink to/Users/davidsarno/Documents/godot-ai/plugin/addons/godot_aibut that path had been emptied; the real plugin now lives under/Users/davidsarno/godot-ai/plugin/addons/godot_ai. Godot booted an in-memory copy of a stale plugin snapshot, so the session connected but the game_helper relay couldn't service screenshot requests. - Workaround: Fix the symlink (rm + re-ln to the real path), call
editor_reload_plugin, re-session_activateunder the new rotated id, thenproject_stop+project_run. Screenshots then worked. - Proposed tool change:
session_listcould surface broken/empty addon paths as a warning alongsideplugin_version; or the plugin could report its resolved source path so stale in-memory sessions are obvious.
plugin-path one
- Tool:
editor_screenshot source="game" - What I tried: Screenshot the running game right after
project_run. - What happened:
Game screenshot timed out. ... check Project Settings → Autoload. ... for headless ... use source='viewport' instead.In fact the_mcp_game_helperautoload WAS registered (the game log showedregistered mcp capture (debugger active=true, logger=true)), but the addon path was stale (see #21). - Workaround: Cross-check
session_list.plugin_versionagainst the known-good plugin version, and re-symlink the addon if they don't match. - Proposed tool change: Extend the error body to include a third
hypothesis: "plugin source may be stale — check
session_listplugin_versionand the resolved addon path".
inset origin when emulating outer-edge frames
- Tool:
control_draw_recipe-style draw scripts - What I tried: Draw a polyline outline that traces the outer visible
edge of a
PanelContainervia a Control child. - What happened: PanelContainer force-fits its children to the inner
content rect (
outer - content_margin * 2), so the child's(0,0)is offset from the panel's true outer corner. Naively drawing from(0,0)tosizegave an outline that was inset by the content margin. - Workaround: In
_ready(), readparent.get_theme_stylebox("panel")and cacheget_margin(SIDE_*), then draw outline from(-ml, -mt)to(size.x + mr, size.y + mb). Zero the parent's border_width/corner_radius /shadow via a per-instance stylebox override so the bg fill stays but the polyline "owns" the frame. - Proposed tool change: A
panel_frame_recipehelper (or an opt-indraw_outer_rectflag oncontrol_draw_recipe) that auto-accounts for the parent PanelContainer's content margins would remove the margin math boilerplate. Everyone building HUD frames does this.