Skip to content

Commit 2579952

Browse files
dsarnoclaude
andcommitted
Add Phase 3 Batch 1: readiness gating, signal/autoload/input_map tools, project_settings_set
Closes Phase 2 exit criteria (readiness gating) and begins Phase 3 with 12 new MCP tools for signals, autoloads, input actions, and project settings writes. Test output compacted to summary-only by default (verbose opt-in). Readiness gating: - GDScript Connection computes readiness (ready/importing/playing/no_scene) and sends readiness_changed events + includes readiness in handshake - Python Session tracks readiness; require_writable() gates all 20 write handlers - editor_state response includes readiness field New tools (Phase 3 Batch 1): - project_settings_set — write settings with old_value tracking + save rollback - signal_list, signal_connect, signal_disconnect — undoable signal wiring - autoload_list, autoload_add, autoload_remove — persistent autoload management - input_map_list (filters builtins by default), input_map_add_action, input_map_remove_action, input_map_bind_event (key/mouse/joy_button) Quality improvements: - All ProjectSettings mutators roll back in-memory state on save failure - Test runner returns compact summary + failures only (verbose=true for full) - Deduplicated readiness logic (Connection.get_readiness() is static, shared) - Extracted _resolve_signal_params() to eliminate copy-paste in signal handler 228 Python + 153 GDScript = 381 total tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c07c995 commit 2579952

43 files changed

Lines changed: 2185 additions & 25 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,15 @@ The Godot dock also has a **Start/Stop Dev Server** button for convenience.
6060

6161
### Python tests
6262
```bash
63-
pytest -v # 192 unit + integration tests
63+
pytest -v # 227 unit + integration tests
6464
```
6565

6666
### Godot-side tests
6767
GDScript test suites in `test_project/tests/` exercise handlers inside the running editor. Run via MCP:
6868
```
69-
run_tests # run all suites
69+
run_tests # compact: summary + failures only
7070
run_tests suite=scene # run one suite
71+
run_tests verbose=true # include every individual test result
7172
get_test_results # review last results
7273
```
7374

@@ -80,6 +81,22 @@ Test suites extend `McpTestSuite` (assertion methods: `assert_true`, `assert_eq`
8081
3. Plugin starts the server automatically; logs should show `Session connected`
8182
4. Use `/mcp` in Claude Code to connect
8283

84+
## Pre-commit smoke test
85+
86+
**Always do this before every commit.** Python mocks don't catch GDScript bugs, editor API regressions, or undo/redo issues.
87+
88+
1. `ruff check src/ tests/` — lint passes
89+
2. `pytest -v` — all Python tests pass
90+
3. Open `test_project/` in Godot (or launch: `/Applications/Godot_mono.app/Contents/MacOS/Godot --editor --path test_project/`)
91+
4. `session_activate` the test_project session if multiple editors are connected
92+
5. `run_tests` via MCP — all GDScript tests pass (0 failures)
93+
6. **Live smoke test** new/changed features against the real editor:
94+
- Call each new tool and verify the response makes sense
95+
- For write tools: verify the change is visible in the editor, and verify undo works (Ctrl+Z in Godot)
96+
- For read tools: compare response against what you see in the editor
97+
- Check `editor_state` to confirm readiness field is present
98+
7. Only commit when all of the above are green
99+
83100
## Client configuration
84101

85102
The plugin can configure MCP clients via `client_configurator.gd`:

docs/implementation-plan.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ All write tools check readiness before executing:
592592
### Phase 2 exit criteria
593593
- [x] All Batch 5-7 tools implemented and tested
594594
- [x] Undo works for node operations (create, delete, reparent, set_property, duplicate, move, group ops)
595-
- [ ] Write operations are gated on readiness
595+
- [x] Write operations are gated on readiness (session tracks readiness state via events, Python handlers call require_writable() before writes)
596596
- [x] Manual test: ask Claude to create a scene with 5 nodes and a script — it can (smoke tested: 7 nodes, player script with 3 signals/4 exports/3 functions, attached, properties set)
597597
- [x] 60+ passing tests (192 Python + 124 GDScript = 316 total)
598598

@@ -634,8 +634,13 @@ All write tools check readiness before executing:
634634
- Session selection UI or tool parameter
635635
- Session metadata includes enough info to distinguish (project path, window title)
636636

637+
### Phase 3 progress
638+
- [x] Batch 1: Signal, autoload, input_map, project_settings_set — 11 new tools, 14 new GDScript handlers
639+
- [x] Readiness gating — GDScript computes readiness (ready/importing/playing/no_scene), sends events, Python gates all write operations
640+
- [x] 227 Python tests + 152 Godot-side tests = 379 total
641+
637642
### Phase 3 exit criteria
638-
- [ ] Signal, autoload, input_map tools work
643+
- [x] Signal, autoload, input_map tools work (signal_list/connect/disconnect, autoload_list/add/remove, input_map_list/add_action/remove_action/bind_event, project_settings_set)
639644
- [ ] Project run/stop cycle works reliably
640645
- [ ] Batch execution handles partial failures
641646
- [ ] Multi-instance: two Godot editors connected, commands route correctly

plugin/addons/godot_ai/connection.gd

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,15 @@ func _attempt_reconnect() -> void:
9494

9595

9696
func _send_handshake() -> void:
97+
_last_readiness = get_readiness()
9798
_send_json({
9899
"type": "handshake",
99100
"session_id": _session_id,
100101
"godot_version": Engine.get_version_info().get("string", "unknown"),
101102
"project_path": ProjectSettings.globalize_path("res://"),
102103
"plugin_version": "0.0.1",
103104
"protocol_version": 1,
105+
"readiness": _last_readiness,
104106
})
105107

106108

@@ -129,6 +131,18 @@ func _hook_editor_signals() -> void:
129131

130132
var _last_scene_path := ""
131133
var _last_play_state := false
134+
var _last_readiness := ""
135+
136+
137+
## Compute current editor readiness from live Godot state.
138+
static func get_readiness() -> String:
139+
if EditorInterface.get_resource_filesystem().is_scanning():
140+
return "importing"
141+
if EditorInterface.is_playing_scene():
142+
return "playing"
143+
if EditorInterface.get_edited_scene_root() == null:
144+
return "no_scene"
145+
return "ready"
132146

133147

134148
## Check for scene/play state changes each frame (lightweight polling).
@@ -148,6 +162,13 @@ func _check_state_changes() -> void:
148162
if log_buffer:
149163
log_buffer.log("[event] play_state_changed -> %s" % state)
150164

165+
var readiness := get_readiness()
166+
if readiness != _last_readiness:
167+
_last_readiness = readiness
168+
send_event("readiness_changed", {"readiness": readiness})
169+
if log_buffer:
170+
log_buffer.log("[event] readiness -> %s" % readiness)
171+
151172

152173
func _get_current_scene_path() -> String:
153174
var scene_root := EditorInterface.get_edited_scene_root()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
@tool
2+
class_name AutoloadHandler
3+
extends RefCounted
4+
5+
## Handles autoload listing, adding, and removing via ProjectSettings.
6+
7+
8+
func list_autoloads(_params: Dictionary) -> Dictionary:
9+
var autoloads: Array[Dictionary] = []
10+
for prop in ProjectSettings.get_property_list():
11+
var key: String = prop.get("name", "")
12+
if not key.begins_with("autoload/"):
13+
continue
14+
var name := key.substr("autoload/".length())
15+
var raw_value: String = ProjectSettings.get_setting(key, "")
16+
var is_singleton := raw_value.begins_with("*")
17+
var path := raw_value.substr(1) if is_singleton else raw_value
18+
autoloads.append({
19+
"name": name,
20+
"path": path,
21+
"singleton": is_singleton,
22+
})
23+
return {"data": {"autoloads": autoloads, "count": autoloads.size()}}
24+
25+
26+
func add_autoload(params: Dictionary) -> Dictionary:
27+
var name: String = params.get("name", "")
28+
var path: String = params.get("path", "")
29+
var singleton: bool = params.get("singleton", true)
30+
31+
if name.is_empty():
32+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Missing required param: name")
33+
if path.is_empty():
34+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Missing required param: path")
35+
if not path.begins_with("res://"):
36+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Path must start with res:// (got: %s)" % path)
37+
if not FileAccess.file_exists(path):
38+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "File not found: %s" % path)
39+
40+
var key := "autoload/%s" % name
41+
if ProjectSettings.has_setting(key):
42+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Autoload '%s' already exists" % name)
43+
44+
var value := ("*" if singleton else "") + path
45+
ProjectSettings.set_setting(key, value)
46+
ProjectSettings.set_initial_value(key, "")
47+
ProjectSettings.set_as_basic(key, true)
48+
var err := ProjectSettings.save()
49+
if err != OK:
50+
ProjectSettings.clear(key)
51+
return McpErrorCodes.make(McpErrorCodes.INTERNAL_ERROR, "Failed to save project settings (error %d)" % err)
52+
53+
return {
54+
"data": {
55+
"name": name,
56+
"path": path,
57+
"singleton": singleton,
58+
"undoable": false,
59+
"reason": "Autoload changes are saved to project.godot",
60+
}
61+
}
62+
63+
64+
func remove_autoload(params: Dictionary) -> Dictionary:
65+
var name: String = params.get("name", "")
66+
if name.is_empty():
67+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Missing required param: name")
68+
69+
var key := "autoload/%s" % name
70+
if not ProjectSettings.has_setting(key):
71+
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Autoload '%s' not found" % name)
72+
73+
var old_value: String = ProjectSettings.get_setting(key, "")
74+
ProjectSettings.clear(key)
75+
var err := ProjectSettings.save()
76+
if err != OK:
77+
ProjectSettings.set_setting(key, old_value)
78+
return McpErrorCodes.make(McpErrorCodes.INTERNAL_ERROR, "Failed to save project settings (error %d)" % err)
79+
80+
return {
81+
"data": {
82+
"name": name,
83+
"removed": true,
84+
"undoable": false,
85+
"reason": "Autoload changes are saved to project.godot",
86+
}
87+
}

plugin/addons/godot_ai/handlers/editor_handler.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func get_editor_state(_params: Dictionary) -> Dictionary:
1919
"project_name": ProjectSettings.get_setting("application/config/name", ""),
2020
"current_scene": scene_root.scene_file_path if scene_root else "",
2121
"is_playing": EditorInterface.is_playing_scene(),
22+
"readiness": Connection.get_readiness(),
2223
}
2324
}
2425

0 commit comments

Comments
 (0)