@beremaran/godot-agent-loop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tugcan Topaloglu
4
+ Copyright (c) 2026 Berke Arslan
5
+ Copyright (c) 2025 Solomon Elias (Original godot-mcp: https://github.com/Coding-Solo/godot-mcp)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ # Godot Agent Loop Bridge
2
+
3
+ An optional Godot 4 editor addon for
4
+ [Godot Agent Loop](https://github.com/beremaran/godot-agent-loop). It adds a
5
+ dock with authenticated connection status, live Agent Activity, human
6
+ Pause/Resume Agent control, protocol and Godot compatibility diagnostics, and
7
+ setup help for Claude Code, Codex, OpenCode, and Pi.
8
+
9
+ ## Install
10
+
11
+ Copy `addons/godot_agent_loop/` into a Godot project, then enable **Godot Agent
12
+ Loop Bridge** under **Project > Project Settings > Plugins**. Installing the
13
+ addon is optional: the external MCP package creates and removes a transient
14
+ bridge when this persistent addon is absent.
15
+
16
+ The addon listens only on loopback. It accepts commands only after a secret
17
+ authenticated protocol handshake from an editor launched through Godot Agent
18
+ Loop. A normally launched editor shows setup diagnostics but does not accept
19
+ agent commands.
20
+
21
+ ## Human control
22
+
23
+ Use **Pause Agent** before editing shared project state. Inspection remains
24
+ available, while subsequent agent mutations are refused before dispatch. Use
25
+ **Resume Agent** to return control.
26
+
27
+ This addon does not install Node.js, an MCP client, or an AI agent. Follow the
28
+ client setup in the main project documentation, then use `launch_editor` so the
29
+ bridge receives its per-session authentication secret.
30
+
31
+ Licensed under the MIT License; see [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ [plugin]
2
+
3
+ name="Godot Agent Loop Bridge"
4
+ description="Authenticated agent activity, pause control, compatibility diagnostics, and setup help for Godot Agent Loop."
5
+ author="Berke Arslan"
6
+ version="1.0.0"
7
+ script="plugin.gd"
8
+ protocol_version="1"
@@ -0,0 +1,417 @@
1
+ @tool
2
+ extends EditorPlugin
3
+
4
+ ## Small, authenticated editor bridge used by editor-capable integrations.
5
+ ## Mutations are recorded through EditorUndoRedoManager; the bridge never edits
6
+ ## scene text directly and never evaluates arbitrary code in the editor.
7
+
8
+ var _server: TCPServer
9
+ var _peer: StreamPeerTCP
10
+ var _buffer: PackedByteArray = PackedByteArray()
11
+ var _port: int = 9091
12
+ var _secret: String = ""
13
+ var _activity_dock: VBoxContainer
14
+ var _activity_list: ItemList
15
+ var _connection_status: Label
16
+ var _compatibility_status: Label
17
+ var _driver_status: Label
18
+ var _driver_pause_button: Button
19
+ var _driver_paused: bool = false
20
+ var _session_authenticated: bool = false
21
+ var _server_version: String = ""
22
+ var _last_history_id: int = EditorUndoRedoManager.GLOBAL_HISTORY
23
+ var _activity_entries: Array[Dictionary] = []
24
+ var _last_filesystem_sync: Dictionary = {}
25
+ const MAX_ACTIVITY_ENTRIES: int = 200
26
+ const PROTOCOL_VERSION: String = "1"
27
+ const ADDON_VERSION: String = "1.0.0"
28
+
29
+ func _enter_tree() -> void:
30
+ set_process(true)
31
+ _port = _read_port()
32
+ _secret = OS.get_environment("GODOT_MCP_EDITOR_SECRET")
33
+ _driver_paused = OS.get_environment("GODOT_MCP_EDITOR_START_PAUSED").strip_edges().to_lower() in ["1", "true", "yes"]
34
+ _server = TCPServer.new()
35
+ _create_activity_dock()
36
+ var error: int = _server.listen(_port, "127.0.0.1")
37
+ if error != OK:
38
+ push_error("Godot Agent Loop editor bridge could not listen on %d: %s" % [_port, error_string(error)])
39
+ _set_connection_status("Unavailable — port %d: %s" % [_port, error_string(error)])
40
+ elif _secret.is_empty():
41
+ _set_connection_status("Waiting — relaunch the editor through Godot Agent Loop")
42
+ else:
43
+ _set_connection_status("Waiting for an authenticated agent")
44
+
45
+ func _exit_tree() -> void:
46
+ set_process(false)
47
+ if _peer != null:
48
+ _peer.disconnect_from_host()
49
+ _peer = null
50
+ if _server != null:
51
+ _server.stop()
52
+ _server = null
53
+ if _activity_dock != null:
54
+ remove_control_from_docks(_activity_dock)
55
+ _activity_dock.queue_free()
56
+ _activity_dock = null
57
+ _activity_list = null
58
+ _connection_status = null
59
+ _compatibility_status = null
60
+ _driver_status = null
61
+ _driver_pause_button = null
62
+
63
+ func _process(_delta: float) -> void:
64
+ if _server != null and _server.is_connection_available():
65
+ if _peer != null:
66
+ _server.take_connection().disconnect_from_host()
67
+ else:
68
+ _peer = _server.take_connection()
69
+ _session_authenticated = false
70
+ _server_version = ""
71
+ _set_connection_status("Connected — authenticating")
72
+ if _peer == null:
73
+ return
74
+ var poll_error: Error = _peer.poll()
75
+ if poll_error != OK:
76
+ return
77
+ if _peer.get_status() != StreamPeerTCP.STATUS_CONNECTED:
78
+ _peer = null
79
+ _buffer = PackedByteArray()
80
+ _session_authenticated = false
81
+ _server_version = ""
82
+ _set_connection_status("Waiting for an authenticated agent")
83
+ return
84
+ var available: int = _peer.get_available_bytes()
85
+ if available <= 0:
86
+ return
87
+ var incoming: Array = _peer.get_data(mini(available, 64 * 1024))
88
+ if incoming[0] != OK:
89
+ return
90
+ var incoming_variant: Variant = incoming[1]
91
+ if not incoming_variant is PackedByteArray:
92
+ return
93
+ var incoming_bytes: PackedByteArray = incoming_variant
94
+ _buffer.append_array(incoming_bytes)
95
+ while true:
96
+ var newline: int = _buffer.find(10)
97
+ if newline < 0:
98
+ break
99
+ var line: String = _buffer.slice(0, newline).get_string_from_utf8().strip_edges()
100
+ _buffer = _buffer.slice(newline + 1)
101
+ if line.is_empty():
102
+ continue
103
+ _handle_request(line)
104
+
105
+ func _handle_request(line: String) -> void:
106
+ var parsed: Variant = JSON.parse_string(line)
107
+ if not parsed is Dictionary:
108
+ _send({"id": null, "error": "invalid_request"})
109
+ return
110
+ var request: Dictionary = parsed
111
+ if _secret.is_empty() or str(request.get("secret", "")) != _secret:
112
+ _send({"id": request.get("id", null), "error": "authentication_required"})
113
+ return
114
+ var command: String = str(request.get("command", ""))
115
+ if command == "handshake":
116
+ var handshake: Dictionary = _handshake(request.get("params", {}))
117
+ handshake["id"] = request.get("id", null)
118
+ _send(handshake)
119
+ return
120
+ if not _session_authenticated:
121
+ _send({"id": request.get("id", null), "error": "handshake_required", "protocol_version": PROTOCOL_VERSION})
122
+ return
123
+ var result: Dictionary = _dispatch(command, request.get("params", {}))
124
+ result["id"] = request.get("id", null)
125
+ _send(result)
126
+
127
+ func _dispatch(command: String, raw_params: Variant) -> Dictionary:
128
+ var params: Dictionary = raw_params if raw_params is Dictionary else {}
129
+ match command:
130
+ "inspect":
131
+ return _inspect()
132
+ "activity":
133
+ return _record_activity(params)
134
+ "driver_state":
135
+ return _driver_state()
136
+ "filesystem_changed":
137
+ return _sync_filesystem(params)
138
+ "select":
139
+ return _select(params)
140
+ "save":
141
+ var save_error: Error = EditorInterface.save_scene()
142
+ return {"success": save_error == OK, "saved": save_error == OK, "error_code": save_error}
143
+ "reload":
144
+ var scene_path: String = str(params.get("scene_path", ""))
145
+ EditorInterface.reload_scene_from_path(scene_path)
146
+ return {"success": true, "scene_path": scene_path}
147
+ "open_scene":
148
+ var open_path: String = str(params.get("scene_path", ""))
149
+ if open_path.is_empty():
150
+ return {"error": "scene_path is required"}
151
+ EditorInterface.open_scene_from_path(open_path)
152
+ return {"success": true, "scene_path": open_path}
153
+ "set_property":
154
+ return _set_property(params)
155
+ "rename_node":
156
+ return _rename_node(params)
157
+ "undo":
158
+ var undo_redo: EditorUndoRedoManager = EditorInterface.get_editor_undo_redo()
159
+ if undo_redo == null:
160
+ return {"success": false, "action": "undo"}
161
+ var undo_history: UndoRedo = undo_redo.get_history_undo_redo(_last_history_id)
162
+ return {"success": undo_history.undo(), "action": "undo"}
163
+ "redo":
164
+ var redo_manager: EditorUndoRedoManager = EditorInterface.get_editor_undo_redo()
165
+ if redo_manager == null:
166
+ return {"success": false, "action": "redo"}
167
+ var redo_history: UndoRedo = redo_manager.get_history_undo_redo(_last_history_id)
168
+ return {"success": redo_history.redo(), "action": "redo"}
169
+ _:
170
+ return {"error": "unknown_command", "allowed": ["inspect", "activity", "driver_state", "filesystem_changed", "select", "save", "reload", "open_scene", "set_property", "rename_node", "undo", "redo"]}
171
+
172
+ func _handshake(raw_params: Variant) -> Dictionary:
173
+ var params: Dictionary = raw_params if raw_params is Dictionary else {}
174
+ var requested_protocol: String = str(params.get("protocol_version", ""))
175
+ _server_version = str(params.get("server_version", ""))
176
+ if requested_protocol != PROTOCOL_VERSION:
177
+ _session_authenticated = false
178
+ _set_connection_status("Incompatible protocol — server %s, addon %s" % [requested_protocol, PROTOCOL_VERSION])
179
+ return {
180
+ "error": "incompatible_protocol",
181
+ "requested_protocol": requested_protocol,
182
+ "protocol_version": PROTOCOL_VERSION,
183
+ "addon_version": ADDON_VERSION,
184
+ }
185
+ _session_authenticated = true
186
+ _set_connection_status("Authenticated — server %s" % (_server_version if not _server_version.is_empty() else "unknown"))
187
+ return {
188
+ "success": true,
189
+ "product": "Godot Agent Loop",
190
+ "protocol_version": PROTOCOL_VERSION,
191
+ "addon_version": ADDON_VERSION,
192
+ "server_version": _server_version,
193
+ "godot_version": Engine.get_version_info().get("string", "unknown"),
194
+ }
195
+
196
+ func _create_activity_dock() -> void:
197
+ _activity_dock = VBoxContainer.new()
198
+ _activity_dock.name = "Godot Agent Loop"
199
+ var title := Label.new()
200
+ title.text = "Agent Activity"
201
+ title.tooltip_text = "Live authenticated MCP command lifecycle"
202
+ _activity_dock.add_child(title)
203
+ _connection_status = Label.new()
204
+ _connection_status.name = "ConnectionStatus"
205
+ _connection_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
206
+ _activity_dock.add_child(_connection_status)
207
+ _compatibility_status = Label.new()
208
+ _compatibility_status.name = "CompatibilityStatus"
209
+ _compatibility_status.text = "Addon %s · protocol %s · Godot %s" % [ADDON_VERSION, PROTOCOL_VERSION, Engine.get_version_info().get("string", "unknown")]
210
+ _compatibility_status.tooltip_text = "The MCP server and addon must use the same editor protocol version."
211
+ _activity_dock.add_child(_compatibility_status)
212
+ var driver_row := HBoxContainer.new()
213
+ driver_row.name = "DriverControls"
214
+ _driver_status = Label.new()
215
+ _driver_status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
216
+ driver_row.add_child(_driver_status)
217
+ _driver_pause_button = Button.new()
218
+ var connect_error: Error = _driver_pause_button.pressed.connect(_toggle_driver_pause) as Error
219
+ if connect_error != OK:
220
+ push_error("Godot Agent Loop could not connect its pause control: %s" % error_string(connect_error))
221
+ driver_row.add_child(_driver_pause_button)
222
+ _activity_dock.add_child(driver_row)
223
+ _update_driver_controls()
224
+ _activity_list = ItemList.new()
225
+ _activity_list.name = "Activity"
226
+ _activity_list.custom_minimum_size = Vector2(320, 180)
227
+ _activity_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
228
+ _activity_dock.add_child(_activity_list)
229
+ var setup_title := Label.new()
230
+ setup_title.text = "Setup help"
231
+ _activity_dock.add_child(setup_title)
232
+ var setup_help := RichTextLabel.new()
233
+ setup_help.name = "SetupHelp"
234
+ setup_help.fit_content = true
235
+ setup_help.custom_minimum_size = Vector2(320, 96)
236
+ setup_help.text = "Claude Code / Codex: install the Godot Agent Loop plugin\nOpenCode: godot-agent-loop setup opencode --write\nPi: pi install npm:@beremaran/godot-agent-loop\nRelaunch the editor through the MCP launch_editor tool to authenticate."
237
+ _activity_dock.add_child(setup_help)
238
+ add_control_to_dock(DOCK_SLOT_RIGHT_BL, _activity_dock)
239
+
240
+ func _set_connection_status(message: String) -> void:
241
+ if _connection_status != null:
242
+ _connection_status.text = "Connection: %s" % message
243
+
244
+ func _toggle_driver_pause() -> void:
245
+ _driver_paused = not _driver_paused
246
+ _update_driver_controls()
247
+
248
+ func _update_driver_controls() -> void:
249
+ if _driver_status != null:
250
+ _driver_status.text = "Agent paused — human editing" if _driver_paused else "Agent is driving"
251
+ if _driver_pause_button != null:
252
+ _driver_pause_button.text = "Resume Agent" if _driver_paused else "Pause Agent"
253
+ _driver_pause_button.tooltip_text = "Allow subsequent MCP mutations" if _driver_paused else "Refuse subsequent MCP mutations while you edit"
254
+
255
+ func _driver_state() -> Dictionary:
256
+ return {"success": true, "paused": _driver_paused, "agent_driving": not _driver_paused}
257
+
258
+ func _record_activity(params: Dictionary) -> Dictionary:
259
+ var event: String = str(params.get("event", ""))
260
+ if not event in ["request_started", "request_finished", "request_timed_out"]:
261
+ return {"error": "invalid_activity_event"}
262
+ var command: String = str(params.get("command", "unknown"))
263
+ var target: String = str(params.get("target", "game"))
264
+ var outcome: String = str(params.get("outcome", "running"))
265
+ var raw_duration_ms: Variant = params.get("duration_ms", 0)
266
+ var duration_ms: int = 0
267
+ if raw_duration_ms is int:
268
+ duration_ms = raw_duration_ms
269
+ elif raw_duration_ms is float:
270
+ var float_duration_ms: float = raw_duration_ms
271
+ duration_ms = roundi(float_duration_ms)
272
+ var correlation_id: String = str(params.get("correlation_id", ""))
273
+ var marker: String = "…" if event == "request_started" else "✓" if outcome == "success" else "✗"
274
+ var suffix: String = "" if event == "request_started" else " · %d ms" % duration_ms
275
+ var text: String = "%s %s → %s%s" % [marker, command, target, suffix]
276
+ var entry: Dictionary = {
277
+ "event": event, "correlation_id": correlation_id, "command": command,
278
+ "target": target, "outcome": outcome, "duration_ms": duration_ms, "text": text,
279
+ }
280
+ _activity_entries.append(entry)
281
+ var item_index: int = _activity_list.add_item(text)
282
+ _activity_list.set_item_tooltip(item_index, correlation_id)
283
+ if event != "request_started":
284
+ var color := Color(0.35, 0.85, 0.45) if outcome == "success" else Color(1.0, 0.4, 0.35)
285
+ _activity_list.set_item_custom_fg_color(item_index, color)
286
+ while _activity_entries.size() > MAX_ACTIVITY_ENTRIES:
287
+ _activity_entries.pop_front()
288
+ _activity_list.remove_item(0)
289
+ return {"success": true, "activity_count": _activity_entries.size()}
290
+
291
+ func _sync_filesystem(params: Dictionary) -> Dictionary:
292
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
293
+ filesystem.scan()
294
+ var scene_path: String = str(params.get("scene_path", ""))
295
+ var root: Node = EditorInterface.get_edited_scene_root()
296
+ var reloaded: bool = false
297
+ if root != null and not scene_path.is_empty() and root.scene_file_path == scene_path:
298
+ EditorInterface.reload_scene_from_path(scene_path)
299
+ reloaded = true
300
+ var focus_path: String = str(params.get("focus_path", ""))
301
+ var focused: bool = false
302
+ if reloaded and not focus_path.is_empty():
303
+ focused = _focus_editor_node(focus_path)
304
+ _last_filesystem_sync = {
305
+ "success": true, "rescanned": true, "scene_path": scene_path,
306
+ "resource_path": str(params.get("resource_path", "")), "reloaded": reloaded,
307
+ "command": str(params.get("command", "")), "focus_path": focus_path,
308
+ "focused": focused,
309
+ }
310
+ return _last_filesystem_sync.duplicate(true)
311
+
312
+ func _inspect() -> Dictionary:
313
+ var root: Node = EditorInterface.get_edited_scene_root()
314
+ var selected: Array[String] = []
315
+ var selection: EditorSelection = EditorInterface.get_selection()
316
+ for node: Node in selection.get_selected_nodes():
317
+ selected.append(str(node.get_path()))
318
+ var open_scenes: Array = []
319
+ var open_result: Variant = EditorInterface.get_open_scenes()
320
+ if open_result is PackedStringArray:
321
+ var packed_open_scenes: PackedStringArray = open_result
322
+ open_scenes.assign(packed_open_scenes)
323
+ var edited_root: Variant = null
324
+ if root != null:
325
+ edited_root = {"name": root.name, "type": root.get_class(), "path": str(root.get_path())}
326
+ return {"success": true, "edited_scene": "" if root == null else str(root.scene_file_path),
327
+ "edited_root": edited_root,
328
+ "selection": selected, "open_scenes": open_scenes,
329
+ "has_undo_redo": EditorInterface.get_editor_undo_redo() != null,
330
+ "activity_dock": _activity_dock != null, "activity": _activity_entries.duplicate(true),
331
+ "driver_paused": _driver_paused, "agent_driving": not _driver_paused,
332
+ "authenticated": _session_authenticated, "server_version": _server_version,
333
+ "addon_version": ADDON_VERSION, "protocol_version": PROTOCOL_VERSION,
334
+ "godot_version": Engine.get_version_info().get("string", "unknown"),
335
+ "last_filesystem_sync": _last_filesystem_sync.duplicate(true)}
336
+
337
+ func _select(params: Dictionary) -> Dictionary:
338
+ var selection: EditorSelection = EditorInterface.get_selection()
339
+ selection.clear()
340
+ var root: Node = EditorInterface.get_edited_scene_root()
341
+ if root == null:
342
+ return {"error": "no_edited_scene"}
343
+ var selected: Array[String] = []
344
+ var paths: Variant = params.get("node_paths", [])
345
+ if not paths is Array:
346
+ return {"error": "node_paths must be an array"}
347
+ for raw_path: Variant in paths:
348
+ var node: Node = root.get_node_or_null(NodePath(str(raw_path)))
349
+ if node == null:
350
+ return {"error": "node_not_found", "node_path": str(raw_path)}
351
+ selection.add_node(node)
352
+ selected.append(str(node.get_path()))
353
+ if selected.size() == 1:
354
+ EditorInterface.edit_node(node)
355
+ return {"success": true, "selection": selected}
356
+
357
+ func _focus_editor_node(path: String) -> bool:
358
+ var root: Node = EditorInterface.get_edited_scene_root()
359
+ if root == null:
360
+ return false
361
+ var normalized: String = path.trim_prefix("root/")
362
+ var node: Node = root if normalized == "." or normalized == "root" or normalized.is_empty() else root.get_node_or_null(NodePath(normalized))
363
+ if node == null:
364
+ return false
365
+ var selection: EditorSelection = EditorInterface.get_selection()
366
+ selection.clear()
367
+ selection.add_node(node)
368
+ EditorInterface.edit_node(node)
369
+ return true
370
+
371
+ func _set_property(params: Dictionary) -> Dictionary:
372
+ var target: Node = _edited_node(params)
373
+ var property: String = str(params.get("property", ""))
374
+ if target == null: return {"error": "node_not_found"}
375
+ if property.is_empty() or not property in target: return {"error": "property_not_found", "property": property}
376
+ var before: Variant = target.get(property)
377
+ var after: Variant = params.get("value")
378
+ return _commit_property_action(target, property, before, after)
379
+
380
+ func _rename_node(params: Dictionary) -> Dictionary:
381
+ var target: Node = _edited_node(params)
382
+ var new_name: String = str(params.get("name", ""))
383
+ if target == null: return {"error": "node_not_found"}
384
+ if new_name.is_empty() or not NodePath(new_name).is_absolute() and new_name.contains("/"):
385
+ return {"error": "invalid_name"}
386
+ var before: String = target.name
387
+ return _commit_property_action(target, "name", before, new_name)
388
+
389
+ func _edited_node(params: Dictionary) -> Node:
390
+ var root: Node = EditorInterface.get_edited_scene_root()
391
+ if root == null: return null
392
+ var path: String = str(params.get("node_path", "."))
393
+ return root if path == "." or path.is_empty() else root.get_node_or_null(NodePath(path))
394
+
395
+ func _commit_property_action(target: Node, property: String, before: Variant, after: Variant) -> Dictionary:
396
+ var manager: EditorUndoRedoManager = EditorInterface.get_editor_undo_redo()
397
+ if manager == null: return {"error": "undo_redo_unavailable"}
398
+ manager.create_action("MCP %s" % property)
399
+ manager.add_do_property(target, property, after)
400
+ manager.add_undo_property(target, property, before)
401
+ manager.commit_action()
402
+ _last_history_id = manager.get_object_history_id(target)
403
+ return {"success": true, "property": property, "before": before, "after": target.get(property), "undo_recorded": true}
404
+
405
+ func _send(response: Dictionary) -> void:
406
+ if _peer == null:
407
+ return
408
+ var send_error: Error = _peer.put_data((JSON.stringify(response) + "\n").to_utf8_buffer())
409
+ if send_error != OK:
410
+ push_warning("Godot Agent Loop editor bridge send failed: %s" % error_string(send_error))
411
+
412
+ func _read_port() -> int:
413
+ var configured: String = OS.get_environment("GODOT_MCP_EDITOR_PORT")
414
+ if configured.is_valid_int():
415
+ var value: int = int(configured)
416
+ if value > 0 and value < 65536: return value
417
+ return _port
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
+ "name": "godot-agent-loop",
4
+ "displayName": "Godot Agent Loop",
5
+ "version": "1.0.0",
6
+ "description": "Build, playtest, and prove Godot games with an evidence-first MCP automation loop.",
7
+ "author": {
8
+ "name": "Berke Arslan"
9
+ },
10
+ "homepage": "https://github.com/beremaran/godot-agent-loop",
11
+ "repository": "https://github.com/beremaran/godot-agent-loop",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "godot",
15
+ "mcp",
16
+ "game-development"
17
+ ],
18
+ "mcpServers": "./.mcp.json"
19
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "godot-agent-loop",
3
+ "version": "1.0.0",
4
+ "description": "Build, playtest, and prove Godot games with an evidence-first MCP automation loop.",
5
+ "author": {
6
+ "name": "Berke Arslan",
7
+ "url": "https://github.com/beremaran"
8
+ },
9
+ "homepage": "https://github.com/beremaran/godot-agent-loop",
10
+ "repository": "https://github.com/beremaran/godot-agent-loop",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "godot",
14
+ "mcp",
15
+ "game-development"
16
+ ],
17
+ "skills": "./skills/",
18
+ "mcpServers": "./.mcp.json",
19
+ "interface": {
20
+ "displayName": "Godot Agent Loop",
21
+ "shortDescription": "Build, playtest, and prove Godot games",
22
+ "longDescription": "Build, playtest, and prove Godot games with an evidence-first MCP automation loop.",
23
+ "developerName": "Berke Arslan",
24
+ "category": "Developer Tools",
25
+ "capabilities": [
26
+ "Read",
27
+ "Write"
28
+ ],
29
+ "websiteURL": "https://github.com/beremaran/godot-agent-loop",
30
+ "defaultPrompt": [
31
+ "Build a small playable Godot game and prove both its win and lose states.",
32
+ "Debug this Godot project from a reproducible failure and verify the fix independently.",
33
+ "Prepare this Godot game for release and report export evidence and remaining gates."
34
+ ],
35
+ "brandColor": "#478CBF"
36
+ }
37
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "godot-agent-loop": {
4
+ "command": "npx",
5
+ "args": [
6
+ "-y",
7
+ "@beremaran/godot-agent-loop@1.0.0"
8
+ ],
9
+ "env": {
10
+ "GODOT_MCP_TOOL_SURFACE": "compact"
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "../docs/agent-adapter.schema.json",
3
+ "name": "godot-agent-loop",
4
+ "displayName": "Godot Agent Loop",
5
+ "version": "1.0.0",
6
+ "description": "Build, playtest, and prove Godot games with an evidence-first MCP automation loop.",
7
+ "package": "@beremaran/godot-agent-loop",
8
+ "mcp": {
9
+ "command": ["npx", "-y", "@beremaran/godot-agent-loop@1.0.0"],
10
+ "environment": {
11
+ "GODOT_MCP_TOOL_SURFACE": "compact"
12
+ },
13
+ "defaultToolCount": 39,
14
+ "discoveryTool": "godot_tools"
15
+ },
16
+ "skills": [
17
+ {
18
+ "name": "build-godot-game",
19
+ "description": "Build a new playable Godot 4 game or substantial gameplay slice through Godot Agent Loop. Use for requests to create a game from an empty project or project path, add a complete mechanic, or deliver a playable scene with input, visible state, and win/lose behavior."
20
+ },
21
+ {
22
+ "name": "debug-godot-game",
23
+ "description": "Diagnose and fix a Godot 4 game that crashes, logs errors, renders incorrectly, ignores input, has wrong scene state, or fails a gameplay expectation. Use for runtime bugs, script failures, broken scenes/resources, flaky behavior, and regressions that need reproduction before repair."
24
+ },
25
+ {
26
+ "name": "ship-godot-game",
27
+ "description": "Prepare and verify a Godot 4 game for a reproducible release. Use for export readiness, project test gates, .NET/C# verification, addon and import integrity, export artifact inspection, smoke runs, release evidence, or deterministic teardown before shipping."
28
+ },
29
+ {
30
+ "name": "verify-godot-change",
31
+ "description": "Verify a Godot 4 project change with static checks and independent evidence from the running game. Use after editing scenes, scripts, resources, input, UI, rendering, or gameplay, and when deciding whether a Godot task is genuinely complete."
32
+ }
33
+ ],
34
+ "defaultPrompts": [
35
+ "Build a small playable Godot game and prove both its win and lose states.",
36
+ "Debug this Godot project from a reproducible failure and verify the fix independently.",
37
+ "Prepare this Godot game for release and report export evidence and remaining gates."
38
+ ],
39
+ "clients": {
40
+ "claudeCode": { "manifest": ".claude-plugin/plugin.json" },
41
+ "codex": { "manifest": ".codex-plugin/plugin.json" },
42
+ "openCode": { "setupCommand": "godot-agent-loop setup opencode" },
43
+ "pi": { "extension": "pi/extension.ts" }
44
+ }
45
+ }