@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.
- package/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- 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,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,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
|
+
}
|