@baixfeng/godot-mcp-cli 1.0.11
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/README.md +187 -0
- package/addons/godot_mcp/command_handler.gd +161 -0
- package/addons/godot_mcp/command_handler.gd.uid +1 -0
- package/addons/godot_mcp/commands/base_command_processor.gd +221 -0
- package/addons/godot_mcp/commands/base_command_processor.gd.uid +1 -0
- package/addons/godot_mcp/commands/debugger_commands.gd +221 -0
- package/addons/godot_mcp/commands/debugger_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/editor_commands.gd +237 -0
- package/addons/godot_mcp/commands/editor_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/editor_script_commands.gd +365 -0
- package/addons/godot_mcp/commands/editor_script_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/input_commands.gd +337 -0
- package/addons/godot_mcp/commands/input_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/node_commands.gd +222 -0
- package/addons/godot_mcp/commands/node_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/project_commands.gd +298 -0
- package/addons/godot_mcp/commands/project_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/scene_commands.gd +337 -0
- package/addons/godot_mcp/commands/scene_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/script_commands.gd +349 -0
- package/addons/godot_mcp/commands/script_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_asset_commands.gd +153 -0
- package/addons/godot_mcp/mcp_asset_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_debug_output_publisher.gd +1669 -0
- package/addons/godot_mcp/mcp_debug_output_publisher.gd.uid +1 -0
- package/addons/godot_mcp/mcp_debugger_bridge.gd +1455 -0
- package/addons/godot_mcp/mcp_debugger_bridge.gd.uid +1 -0
- package/addons/godot_mcp/mcp_enhanced_commands.gd +1083 -0
- package/addons/godot_mcp/mcp_enhanced_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_input_handler.gd +545 -0
- package/addons/godot_mcp/mcp_input_handler.gd.uid +1 -0
- package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd +464 -0
- package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd.uid +1 -0
- package/addons/godot_mcp/mcp_script_resource_commands.gd +165 -0
- package/addons/godot_mcp/mcp_script_resource_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_server.gd +260 -0
- package/addons/godot_mcp/mcp_server.gd.uid +1 -0
- package/addons/godot_mcp/plugin.cfg +7 -0
- package/addons/godot_mcp/runtime_debugger.gd +81 -0
- package/addons/godot_mcp/runtime_debugger.gd.uid +1 -0
- package/addons/godot_mcp/ui/mcp_panel.gd +94 -0
- package/addons/godot_mcp/ui/mcp_panel.gd.uid +1 -0
- package/addons/godot_mcp/ui/mcp_panel.tscn +96 -0
- package/addons/godot_mcp/utils/node_utils.gd +82 -0
- package/addons/godot_mcp/utils/node_utils.gd.uid +1 -0
- package/addons/godot_mcp/utils/resource_utils.gd +81 -0
- package/addons/godot_mcp/utils/resource_utils.gd.uid +1 -0
- package/addons/godot_mcp/utils/script_utils.gd +114 -0
- package/addons/godot_mcp/utils/script_utils.gd.uid +1 -0
- package/addons/godot_mcp/websocket_server.gd +197 -0
- package/addons/godot_mcp/websocket_server.gd.uid +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +561 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/asset_resources.d.ts +29 -0
- package/dist/resources/asset_resources.js +145 -0
- package/dist/resources/asset_resources.js.map +1 -0
- package/dist/resources/debug_resources.d.ts +11 -0
- package/dist/resources/debug_resources.js +106 -0
- package/dist/resources/debug_resources.js.map +1 -0
- package/dist/resources/debugger_resources.d.ts +62 -0
- package/dist/resources/debugger_resources.js +201 -0
- package/dist/resources/debugger_resources.js.map +1 -0
- package/dist/resources/editor_resources.d.ts +47 -0
- package/dist/resources/editor_resources.js +155 -0
- package/dist/resources/editor_resources.js.map +1 -0
- package/dist/resources/project_resources.d.ts +33 -0
- package/dist/resources/project_resources.js +137 -0
- package/dist/resources/project_resources.js.map +1 -0
- package/dist/resources/scene_resources.d.ts +33 -0
- package/dist/resources/scene_resources.js +160 -0
- package/dist/resources/scene_resources.js.map +1 -0
- package/dist/resources/script_resources.d.ts +51 -0
- package/dist/resources/script_resources.js +203 -0
- package/dist/resources/script_resources.js.map +1 -0
- package/dist/tools/asset_tools.d.ts +5 -0
- package/dist/tools/asset_tools.js +125 -0
- package/dist/tools/asset_tools.js.map +1 -0
- package/dist/tools/debugger_tools.d.ts +2 -0
- package/dist/tools/debugger_tools.js +342 -0
- package/dist/tools/debugger_tools.js.map +1 -0
- package/dist/tools/editor_tools.d.ts +2 -0
- package/dist/tools/editor_tools.js +165 -0
- package/dist/tools/editor_tools.js.map +1 -0
- package/dist/tools/enhanced_tools.d.ts +5 -0
- package/dist/tools/enhanced_tools.js +706 -0
- package/dist/tools/enhanced_tools.js.map +1 -0
- package/dist/tools/input_tools.d.ts +2 -0
- package/dist/tools/input_tools.js +408 -0
- package/dist/tools/input_tools.js.map +1 -0
- package/dist/tools/node_tools.d.ts +5 -0
- package/dist/tools/node_tools.js +217 -0
- package/dist/tools/node_tools.js.map +1 -0
- package/dist/tools/project_tools.d.ts +5 -0
- package/dist/tools/project_tools.js +162 -0
- package/dist/tools/project_tools.js.map +1 -0
- package/dist/tools/scene_tools.d.ts +5 -0
- package/dist/tools/scene_tools.js +260 -0
- package/dist/tools/scene_tools.js.map +1 -0
- package/dist/tools/script_resource_tools.d.ts +5 -0
- package/dist/tools/script_resource_tools.js +5 -0
- package/dist/tools/script_resource_tools.js.map +1 -0
- package/dist/tools/script_tools.d.ts +5 -0
- package/dist/tools/script_tools.js +154 -0
- package/dist/tools/script_tools.js.map +1 -0
- package/dist/utils/godot_connection.d.ts +30 -0
- package/dist/utils/godot_connection.js +285 -0
- package/dist/utils/godot_connection.js.map +1 -0
- package/dist/utils/types.d.ts +16 -0
- package/dist/utils/types.js +2 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPRuntimeDebuggerBridge
|
|
3
|
+
extends EditorDebuggerPlugin
|
|
4
|
+
|
|
5
|
+
signal scene_tree_updated(session_id: int)
|
|
6
|
+
signal runtime_eval_completed(session_id: int, request_id: int)
|
|
7
|
+
signal input_result_received(session_id: int, request_id: int)
|
|
8
|
+
|
|
9
|
+
const CAPTURE_SCENE := "scene"
|
|
10
|
+
const VIEW_HAS_VISIBLE_METHOD := 1 << 1
|
|
11
|
+
const VIEW_VISIBLE := 1 << 2
|
|
12
|
+
const VIEW_VISIBLE_IN_TREE := 1 << 3
|
|
13
|
+
const DEFAULT_TIMEOUT_MS := 800
|
|
14
|
+
const DEFAULT_EVAL_TIMEOUT_MS := 800
|
|
15
|
+
const SCENE_CAPTURE_NAMES := ["scene", "limboai"]
|
|
16
|
+
const EVAL_CAPTURE_NAME := "mcp_eval"
|
|
17
|
+
const INPUT_CAPTURE_NAME := "mcp_input"
|
|
18
|
+
|
|
19
|
+
var _sessions: Dictionary = {}
|
|
20
|
+
var _next_eval_request_id: int = 1
|
|
21
|
+
|
|
22
|
+
func _init() -> void:
|
|
23
|
+
_sessions.clear()
|
|
24
|
+
_next_eval_request_id = 1
|
|
25
|
+
|
|
26
|
+
func _setup_session(session_id: int) -> void:
|
|
27
|
+
_trace("setup_session %s" % session_id)
|
|
28
|
+
_ensure_session(session_id)
|
|
29
|
+
var session := get_session(session_id)
|
|
30
|
+
if session:
|
|
31
|
+
session.started.connect(_on_session_started.bind(session_id), CONNECT_DEFERRED)
|
|
32
|
+
session.stopped.connect(_on_session_stopped.bind(session_id), CONNECT_DEFERRED)
|
|
33
|
+
session.breaked.connect(_on_session_breaked.bind(session_id), CONNECT_DEFERRED)
|
|
34
|
+
if session.is_active():
|
|
35
|
+
var state: Dictionary = _sessions[session_id]
|
|
36
|
+
state["active"] = true
|
|
37
|
+
_sessions[session_id] = state
|
|
38
|
+
_trace("session %s already active" % session_id)
|
|
39
|
+
|
|
40
|
+
func _has_capture(capture: String) -> bool:
|
|
41
|
+
for prefix in SCENE_CAPTURE_NAMES:
|
|
42
|
+
if capture == prefix or capture.begins_with(prefix + ":"):
|
|
43
|
+
return true
|
|
44
|
+
if capture == INPUT_CAPTURE_NAME or capture.begins_with(INPUT_CAPTURE_NAME + ":"):
|
|
45
|
+
return true
|
|
46
|
+
return false
|
|
47
|
+
|
|
48
|
+
func _capture(message: String, data: Array, session_id: int) -> bool:
|
|
49
|
+
_trace("capture %s session=%s payload_len=%s" % [message, session_id, data.size()])
|
|
50
|
+
var normalized := _normalize_capture_name(message)
|
|
51
|
+
if normalized == "scene:scene_tree":
|
|
52
|
+
if data.size() == 0 or data.size() % 6 != 0:
|
|
53
|
+
_trace("discarding malformed scene_tree payload (size=%s)" % data.size())
|
|
54
|
+
return false
|
|
55
|
+
_trace("storing scene tree for session %s (size=%s)" % [session_id, data.size()])
|
|
56
|
+
_store_scene_tree(session_id, data)
|
|
57
|
+
return true
|
|
58
|
+
elif normalized == "%s:result" % EVAL_CAPTURE_NAME:
|
|
59
|
+
_trace("received runtime eval result for session %s" % session_id)
|
|
60
|
+
_store_eval_result(session_id, data)
|
|
61
|
+
return true
|
|
62
|
+
elif normalized == "%s:result" % INPUT_CAPTURE_NAME:
|
|
63
|
+
_trace("received input result for session %s" % session_id)
|
|
64
|
+
_store_input_result(session_id, data)
|
|
65
|
+
return true
|
|
66
|
+
return false
|
|
67
|
+
|
|
68
|
+
func request_runtime_scene_snapshot() -> Dictionary:
|
|
69
|
+
var active_sessions := _get_active_session_ids()
|
|
70
|
+
_trace("active sessions: %s" % active_sessions)
|
|
71
|
+
if active_sessions.is_empty():
|
|
72
|
+
return { "error": "No active runtime session. Start the project or attach the debugger first." }
|
|
73
|
+
var session_id: int = active_sessions[0]
|
|
74
|
+
_ensure_session(session_id)
|
|
75
|
+
|
|
76
|
+
var state: Dictionary = _sessions[session_id]
|
|
77
|
+
var baseline_version: int = state.get("tree_version", 0)
|
|
78
|
+
_request_scene_tree(session_id)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"session_id": session_id,
|
|
82
|
+
"baseline_version": baseline_version
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func has_new_runtime_snapshot(session_id: int, baseline_version: int) -> bool:
|
|
86
|
+
if not _sessions.has(session_id):
|
|
87
|
+
return false
|
|
88
|
+
var state: Dictionary = _sessions[session_id]
|
|
89
|
+
return state.get("tree_version", 0) > baseline_version and state.get("tree")
|
|
90
|
+
|
|
91
|
+
func build_runtime_snapshot(session_id: int, options: Dictionary = {}) -> Dictionary:
|
|
92
|
+
if not _sessions.has(session_id):
|
|
93
|
+
return {}
|
|
94
|
+
var state: Dictionary = _sessions[session_id]
|
|
95
|
+
if not state.get("tree"):
|
|
96
|
+
return {}
|
|
97
|
+
return _build_response(state["tree"], options)
|
|
98
|
+
|
|
99
|
+
func evaluate_runtime_expression(expression: String, options: Dictionary = {}) -> Dictionary:
|
|
100
|
+
var trimmed := expression.strip_edges()
|
|
101
|
+
if trimmed.is_empty():
|
|
102
|
+
return { "error": "Expression cannot be empty." }
|
|
103
|
+
|
|
104
|
+
var active_sessions := _get_active_session_ids()
|
|
105
|
+
if active_sessions.is_empty():
|
|
106
|
+
return { "error": "No active runtime session. Start the project or attach the debugger first." }
|
|
107
|
+
|
|
108
|
+
var session_id: int = active_sessions[0]
|
|
109
|
+
_ensure_session(session_id)
|
|
110
|
+
var session := get_session(session_id)
|
|
111
|
+
if session == null or not session.is_active():
|
|
112
|
+
return { "error": "Runtime debugger session is not active." }
|
|
113
|
+
|
|
114
|
+
var request_id: int = _next_eval_request_id
|
|
115
|
+
_next_eval_request_id += 1
|
|
116
|
+
|
|
117
|
+
var payload := Array()
|
|
118
|
+
payload.append(request_id)
|
|
119
|
+
payload.append(trimmed)
|
|
120
|
+
if typeof(options) == TYPE_DICTIONARY:
|
|
121
|
+
payload.append(options.duplicate(true))
|
|
122
|
+
else:
|
|
123
|
+
payload.append({})
|
|
124
|
+
|
|
125
|
+
_trace("sending runtime eval request %s to session %s" % [request_id, session_id])
|
|
126
|
+
session.send_message("%s:evaluate" % EVAL_CAPTURE_NAME, payload)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"session_id": session_id,
|
|
130
|
+
"request_id": request_id
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func has_eval_result(session_id: int, request_id: int) -> bool:
|
|
134
|
+
if not _sessions.has(session_id):
|
|
135
|
+
return false
|
|
136
|
+
var state: Dictionary = _sessions[session_id]
|
|
137
|
+
var results: Dictionary = state.get("eval_results", {})
|
|
138
|
+
return results.has(request_id)
|
|
139
|
+
|
|
140
|
+
func take_eval_result(session_id: int, request_id: int) -> Dictionary:
|
|
141
|
+
if not _sessions.has(session_id):
|
|
142
|
+
return {}
|
|
143
|
+
var state: Dictionary = _sessions[session_id]
|
|
144
|
+
var results: Dictionary = state.get("eval_results", {})
|
|
145
|
+
if not results.has(request_id):
|
|
146
|
+
return {}
|
|
147
|
+
var payload: Variant = results[request_id]
|
|
148
|
+
results.erase(request_id)
|
|
149
|
+
state["eval_results"] = results
|
|
150
|
+
_sessions[session_id] = state
|
|
151
|
+
|
|
152
|
+
var response := {}
|
|
153
|
+
if typeof(payload) == TYPE_DICTIONARY:
|
|
154
|
+
response = payload.duplicate(true)
|
|
155
|
+
if response.has("_received_at"):
|
|
156
|
+
response.erase("_received_at")
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
func _get_active_session_ids() -> Array:
|
|
160
|
+
var result: Array = []
|
|
161
|
+
var sessions := get_sessions()
|
|
162
|
+
_trace("get_sessions count=%s" % sessions.size())
|
|
163
|
+
for i in range(sessions.size()):
|
|
164
|
+
var session = sessions[i]
|
|
165
|
+
if session and session.has_method("is_active") and session.is_active():
|
|
166
|
+
_ensure_session(i)
|
|
167
|
+
result.append(i)
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
func _request_scene_tree(session_id: int) -> void:
|
|
171
|
+
var session := get_session(session_id)
|
|
172
|
+
_trace("request_scene_tree session=%s session=%s" % [session_id, session])
|
|
173
|
+
if session and session.is_active():
|
|
174
|
+
var payload := Array()
|
|
175
|
+
payload.push_back("")
|
|
176
|
+
payload.push_back(Array())
|
|
177
|
+
for prefix in SCENE_CAPTURE_NAMES:
|
|
178
|
+
payload[0] = "%s:scene_tree" % prefix
|
|
179
|
+
session.send_message("request_message", payload)
|
|
180
|
+
var state: Dictionary = _sessions.get(session_id, {})
|
|
181
|
+
state["last_request_time"] = Time.get_ticks_msec()
|
|
182
|
+
_sessions[session_id] = state
|
|
183
|
+
else:
|
|
184
|
+
_trace("session inactive, cannot request scene tree")
|
|
185
|
+
|
|
186
|
+
func _store_scene_tree(session_id: int, payload: Array) -> void:
|
|
187
|
+
if payload.is_empty():
|
|
188
|
+
return
|
|
189
|
+
var parsed := _parse_remote_tree(payload)
|
|
190
|
+
if parsed.is_empty():
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
var state: Dictionary = _sessions.get(session_id, {})
|
|
194
|
+
state["tree"] = parsed
|
|
195
|
+
state["tree_version"] = state.get("tree_version", 0) + 1
|
|
196
|
+
state["last_update"] = Time.get_ticks_msec()
|
|
197
|
+
_sessions[session_id] = state
|
|
198
|
+
|
|
199
|
+
scene_tree_updated.emit(session_id)
|
|
200
|
+
|
|
201
|
+
func _store_eval_result(session_id: int, payload: Array) -> void:
|
|
202
|
+
_ensure_session(session_id)
|
|
203
|
+
if payload.is_empty():
|
|
204
|
+
_trace("runtime eval payload empty")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
var entry: Variant = payload[0]
|
|
208
|
+
if typeof(entry) != TYPE_DICTIONARY:
|
|
209
|
+
_trace("runtime eval payload not dictionary")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
var result_dict: Dictionary = entry.duplicate(true)
|
|
213
|
+
var request_id := int(result_dict.get("request_id", -1))
|
|
214
|
+
if request_id < 0:
|
|
215
|
+
_trace("runtime eval payload missing request_id")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if result_dict.has("output"):
|
|
219
|
+
var raw_output = result_dict["output"]
|
|
220
|
+
var normalized_output: Array = []
|
|
221
|
+
if typeof(raw_output) == TYPE_ARRAY:
|
|
222
|
+
for item in raw_output:
|
|
223
|
+
normalized_output.append(str(item))
|
|
224
|
+
elif typeof(raw_output) == TYPE_PACKED_STRING_ARRAY:
|
|
225
|
+
for item in raw_output:
|
|
226
|
+
normalized_output.append(str(item))
|
|
227
|
+
elif raw_output == null:
|
|
228
|
+
pass
|
|
229
|
+
else:
|
|
230
|
+
normalized_output.append(str(raw_output))
|
|
231
|
+
result_dict["output"] = normalized_output
|
|
232
|
+
else:
|
|
233
|
+
result_dict["output"] = []
|
|
234
|
+
|
|
235
|
+
result_dict["_received_at"] = Time.get_ticks_msec()
|
|
236
|
+
|
|
237
|
+
var state: Dictionary = _sessions.get(session_id, {})
|
|
238
|
+
var results: Dictionary = state.get("eval_results", {})
|
|
239
|
+
results[request_id] = result_dict
|
|
240
|
+
state["eval_results"] = results
|
|
241
|
+
_sessions[session_id] = state
|
|
242
|
+
|
|
243
|
+
runtime_eval_completed.emit(session_id, request_id)
|
|
244
|
+
|
|
245
|
+
func _parse_remote_tree(flat_data: Array) -> Dictionary:
|
|
246
|
+
if flat_data.size() % 6 != 0:
|
|
247
|
+
return {}
|
|
248
|
+
|
|
249
|
+
var nodes: Array = []
|
|
250
|
+
var index := 0
|
|
251
|
+
while index < flat_data.size():
|
|
252
|
+
var node := {
|
|
253
|
+
"child_count": int(flat_data[index]),
|
|
254
|
+
"name": str(flat_data[index + 1]),
|
|
255
|
+
"type": str(flat_data[index + 2]),
|
|
256
|
+
"object_id": int(flat_data[index + 3]),
|
|
257
|
+
"scene_file_path": str(flat_data[index + 4]),
|
|
258
|
+
"view_flags": int(flat_data[index + 5]),
|
|
259
|
+
"children": []
|
|
260
|
+
}
|
|
261
|
+
nodes.append(node)
|
|
262
|
+
index += 6
|
|
263
|
+
if nodes.is_empty():
|
|
264
|
+
return {}
|
|
265
|
+
|
|
266
|
+
var stack: Array = []
|
|
267
|
+
var root: Dictionary = nodes[0]
|
|
268
|
+
root["_remaining"] = root["child_count"]
|
|
269
|
+
stack.append(root)
|
|
270
|
+
|
|
271
|
+
for i in range(1, nodes.size()):
|
|
272
|
+
var current: Dictionary = nodes[i]
|
|
273
|
+
current["_remaining"] = current["child_count"]
|
|
274
|
+
while not stack.is_empty() and stack.back()["_remaining"] <= 0:
|
|
275
|
+
stack.pop_back()
|
|
276
|
+
if stack.is_empty():
|
|
277
|
+
# Malformed stream; abort to avoid inconsistent data.
|
|
278
|
+
return {}
|
|
279
|
+
var parent: Dictionary = stack.back()
|
|
280
|
+
parent["children"].append(current)
|
|
281
|
+
parent["_remaining"] -= 1
|
|
282
|
+
if current["_remaining"] > 0:
|
|
283
|
+
stack.append(current)
|
|
284
|
+
|
|
285
|
+
while not stack.is_empty():
|
|
286
|
+
stack.pop_back()
|
|
287
|
+
|
|
288
|
+
_cleanup_internal_keys(root)
|
|
289
|
+
_assign_paths(root, "")
|
|
290
|
+
return root
|
|
291
|
+
|
|
292
|
+
func _cleanup_internal_keys(node: Dictionary) -> void:
|
|
293
|
+
node.erase("_remaining")
|
|
294
|
+
for child in node["children"]:
|
|
295
|
+
_cleanup_internal_keys(child)
|
|
296
|
+
|
|
297
|
+
func _assign_paths(node: Dictionary, parent_path: String) -> void:
|
|
298
|
+
var name: String = node.get("name", "")
|
|
299
|
+
var current_path := ""
|
|
300
|
+
if parent_path.is_empty():
|
|
301
|
+
current_path = "/root/%s" % name
|
|
302
|
+
else:
|
|
303
|
+
current_path = "%s/%s" % [parent_path, name]
|
|
304
|
+
node["path"] = current_path
|
|
305
|
+
|
|
306
|
+
var view_flags: int = node.get("view_flags", 0)
|
|
307
|
+
node["visibility"] = {
|
|
308
|
+
"has_visible_method": bool(view_flags & VIEW_HAS_VISIBLE_METHOD),
|
|
309
|
+
"visible": bool(view_flags & VIEW_VISIBLE),
|
|
310
|
+
"visible_in_tree": bool(view_flags & VIEW_VISIBLE_IN_TREE)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for child in node["children"]:
|
|
314
|
+
_assign_paths(child, current_path)
|
|
315
|
+
|
|
316
|
+
func _build_response(root: Dictionary, options: Dictionary) -> Dictionary:
|
|
317
|
+
var max_depth: int = options.get("max_depth", -1)
|
|
318
|
+
var include_props := options.get("include_properties", false)
|
|
319
|
+
var include_scripts := options.get("include_scripts", false)
|
|
320
|
+
|
|
321
|
+
var response := {
|
|
322
|
+
"scene_path": root.get("scene_file_path", ""),
|
|
323
|
+
"root_node_name": root.get("name", ""),
|
|
324
|
+
"root_node_type": root.get("type", ""),
|
|
325
|
+
"runtime": true,
|
|
326
|
+
"structure": _project_node(root, 0, max_depth)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if include_props:
|
|
330
|
+
response["warning_properties"] = "Runtime inspection does not expose live properties yet."
|
|
331
|
+
if include_scripts:
|
|
332
|
+
response["warning_scripts"] = "Runtime inspection does not expose script metadata yet."
|
|
333
|
+
|
|
334
|
+
return response
|
|
335
|
+
|
|
336
|
+
func _project_node(node: Dictionary, depth: int, max_depth: int) -> Dictionary:
|
|
337
|
+
var projected := {
|
|
338
|
+
"name": node.get("name", ""),
|
|
339
|
+
"type": node.get("type", ""),
|
|
340
|
+
"path": node.get("path", ""),
|
|
341
|
+
"object_id": node.get("object_id", 0),
|
|
342
|
+
"scene_file_path": node.get("scene_file_path", ""),
|
|
343
|
+
"visibility": node.get("visibility", {}),
|
|
344
|
+
"children": []
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if max_depth >= 0 and depth >= max_depth:
|
|
348
|
+
return projected
|
|
349
|
+
|
|
350
|
+
for child in node["children"]:
|
|
351
|
+
projected["children"].append(_project_node(child, depth + 1, max_depth))
|
|
352
|
+
|
|
353
|
+
return projected
|
|
354
|
+
|
|
355
|
+
func _ensure_session(session_id: int) -> void:
|
|
356
|
+
if not _sessions.has(session_id):
|
|
357
|
+
_sessions[session_id] = {
|
|
358
|
+
"tree": null,
|
|
359
|
+
"tree_version": 0,
|
|
360
|
+
"last_update": 0,
|
|
361
|
+
"active": false,
|
|
362
|
+
"eval_results": {},
|
|
363
|
+
"input_results": {}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func _on_session_started(session_id: int) -> void:
|
|
367
|
+
_ensure_session(session_id)
|
|
368
|
+
var state: Dictionary = _sessions[session_id]
|
|
369
|
+
state["active"] = true
|
|
370
|
+
_sessions[session_id] = state
|
|
371
|
+
_trace("session %s started" % session_id)
|
|
372
|
+
|
|
373
|
+
func _on_session_stopped(session_id: int) -> void:
|
|
374
|
+
_ensure_session(session_id)
|
|
375
|
+
var state: Dictionary = _sessions[session_id]
|
|
376
|
+
state["active"] = false
|
|
377
|
+
_sessions[session_id] = state
|
|
378
|
+
_trace("session %s stopped" % session_id)
|
|
379
|
+
|
|
380
|
+
func _on_session_breaked(can_debug: bool, session_id: int) -> void:
|
|
381
|
+
_ensure_session(session_id)
|
|
382
|
+
var state: Dictionary = _sessions[session_id]
|
|
383
|
+
state["can_debug"] = can_debug
|
|
384
|
+
_sessions[session_id] = state
|
|
385
|
+
_trace("session %s breaked can_debug=%s" % [session_id, can_debug])
|
|
386
|
+
|
|
387
|
+
func _normalize_capture_name(message: String) -> String:
|
|
388
|
+
for prefix in SCENE_CAPTURE_NAMES:
|
|
389
|
+
var needle := "%s:" % prefix
|
|
390
|
+
if message.begins_with(needle):
|
|
391
|
+
var suffix := message.substr(needle.length())
|
|
392
|
+
if suffix == "scene_tree":
|
|
393
|
+
return "scene:scene_tree"
|
|
394
|
+
return "%s:%s" % [prefix, suffix]
|
|
395
|
+
if message.begins_with("%s:" % EVAL_CAPTURE_NAME):
|
|
396
|
+
var eval_suffix := message.substr(EVAL_CAPTURE_NAME.length() + 1)
|
|
397
|
+
return "%s:%s" % [EVAL_CAPTURE_NAME, eval_suffix]
|
|
398
|
+
if message.begins_with("%s:" % INPUT_CAPTURE_NAME):
|
|
399
|
+
var input_suffix := message.substr(INPUT_CAPTURE_NAME.length() + 1)
|
|
400
|
+
return "%s:%s" % [INPUT_CAPTURE_NAME, input_suffix]
|
|
401
|
+
if message.ends_with(":scene_tree"):
|
|
402
|
+
return "scene:scene_tree"
|
|
403
|
+
return message
|
|
404
|
+
|
|
405
|
+
func _trace(text: String) -> void:
|
|
406
|
+
if OS.is_stdout_verbose():
|
|
407
|
+
print("[RuntimeBridge] %s" % text)
|
|
408
|
+
|
|
409
|
+
# Input simulation result handling
|
|
410
|
+
|
|
411
|
+
func _store_input_result(session_id: int, payload: Array) -> void:
|
|
412
|
+
_ensure_session(session_id)
|
|
413
|
+
if payload.is_empty():
|
|
414
|
+
_trace("input result payload empty")
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
var entry: Variant = payload[0]
|
|
418
|
+
if typeof(entry) != TYPE_DICTIONARY:
|
|
419
|
+
_trace("input result payload not dictionary")
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
var result_dict: Dictionary = entry.duplicate(true)
|
|
423
|
+
var request_id := int(result_dict.get("request_id", -1))
|
|
424
|
+
if request_id < 0:
|
|
425
|
+
_trace("input result payload missing request_id")
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
result_dict["_received_at"] = Time.get_ticks_msec()
|
|
429
|
+
|
|
430
|
+
var state: Dictionary = _sessions.get(session_id, {})
|
|
431
|
+
var results: Dictionary = state.get("input_results", {})
|
|
432
|
+
results[request_id] = result_dict
|
|
433
|
+
state["input_results"] = results
|
|
434
|
+
_sessions[session_id] = state
|
|
435
|
+
|
|
436
|
+
input_result_received.emit(session_id, request_id)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
func has_input_result(session_id: int, request_id: int) -> bool:
|
|
440
|
+
if not _sessions.has(session_id):
|
|
441
|
+
return false
|
|
442
|
+
var state: Dictionary = _sessions[session_id]
|
|
443
|
+
var results: Dictionary = state.get("input_results", {})
|
|
444
|
+
return results.has(request_id)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
func take_input_result(session_id: int, request_id: int) -> Dictionary:
|
|
448
|
+
if not _sessions.has(session_id):
|
|
449
|
+
return {}
|
|
450
|
+
var state: Dictionary = _sessions[session_id]
|
|
451
|
+
var results: Dictionary = state.get("input_results", {})
|
|
452
|
+
if not results.has(request_id):
|
|
453
|
+
return {}
|
|
454
|
+
var payload: Variant = results[request_id]
|
|
455
|
+
results.erase(request_id)
|
|
456
|
+
state["input_results"] = results
|
|
457
|
+
_sessions[session_id] = state
|
|
458
|
+
|
|
459
|
+
var response := {}
|
|
460
|
+
if typeof(payload) == TYPE_DICTIONARY:
|
|
461
|
+
response = payload.duplicate(true)
|
|
462
|
+
if response.has("_received_at"):
|
|
463
|
+
response.erase("_received_at")
|
|
464
|
+
return response
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c4vuvdpmhh01l
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPScriptResourceCommands
|
|
3
|
+
extends MCPBaseCommandProcessor
|
|
4
|
+
|
|
5
|
+
func process_command(client_id: int, command_type: String, params: Dictionary, command_id: String) -> bool:
|
|
6
|
+
match command_type:
|
|
7
|
+
"get_script":
|
|
8
|
+
_handle_get_script(client_id, params, command_id)
|
|
9
|
+
return true
|
|
10
|
+
"edit_script":
|
|
11
|
+
_handle_edit_script(client_id, params, command_id)
|
|
12
|
+
return true
|
|
13
|
+
return false # Command not handled
|
|
14
|
+
|
|
15
|
+
func _handle_get_script(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
16
|
+
var path = params.get("path", "")
|
|
17
|
+
var node_path = params.get("node_path", "")
|
|
18
|
+
|
|
19
|
+
# Handle based on which parameter is provided
|
|
20
|
+
var script_path = ""
|
|
21
|
+
var result = {}
|
|
22
|
+
|
|
23
|
+
if not path.is_empty():
|
|
24
|
+
# Direct script path provided
|
|
25
|
+
result = _get_script_by_path(path)
|
|
26
|
+
elif not node_path.is_empty():
|
|
27
|
+
# Node path provided, get attached script
|
|
28
|
+
result = _get_script_by_node(node_path)
|
|
29
|
+
else:
|
|
30
|
+
result = {
|
|
31
|
+
"error": "Either script_path or node_path must be provided",
|
|
32
|
+
"script_found": false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_send_success(client_id, result, command_id)
|
|
36
|
+
|
|
37
|
+
func _get_script_by_path(script_path: String) -> Dictionary:
|
|
38
|
+
var normalized_path = _normalize_script_path(script_path)
|
|
39
|
+
|
|
40
|
+
if normalized_path.is_empty():
|
|
41
|
+
return {
|
|
42
|
+
"error": "Script path is required",
|
|
43
|
+
"script_found": false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if not FileAccess.file_exists(normalized_path):
|
|
47
|
+
return {
|
|
48
|
+
"error": "Script file not found",
|
|
49
|
+
"script_found": false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var file = FileAccess.open(normalized_path, FileAccess.READ)
|
|
53
|
+
if not file:
|
|
54
|
+
return {
|
|
55
|
+
"error": "Failed to open script file",
|
|
56
|
+
"script_found": false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var content = file.get_as_text()
|
|
60
|
+
return {
|
|
61
|
+
"script_found": true,
|
|
62
|
+
"script_path": normalized_path,
|
|
63
|
+
"content": content
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func _get_script_by_node(node_path: String) -> Dictionary:
|
|
67
|
+
# Get editor plugin and interfaces
|
|
68
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
69
|
+
if not plugin:
|
|
70
|
+
return {
|
|
71
|
+
"error": "GodotMCPPlugin not found in Engine metadata",
|
|
72
|
+
"script_found": false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var editor_interface = plugin.get_editor_interface()
|
|
76
|
+
var edited_scene_root = editor_interface.get_edited_scene_root()
|
|
77
|
+
|
|
78
|
+
if not edited_scene_root:
|
|
79
|
+
return {
|
|
80
|
+
"error": "No scene is currently being edited",
|
|
81
|
+
"script_found": false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var node = edited_scene_root.get_node_or_null(node_path)
|
|
85
|
+
if not node:
|
|
86
|
+
return {
|
|
87
|
+
"error": "Node not found",
|
|
88
|
+
"script_found": false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var script = node.get_script()
|
|
92
|
+
if not script:
|
|
93
|
+
return {
|
|
94
|
+
"error": "Node has no script attached",
|
|
95
|
+
"script_found": false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var script_path = script.resource_path
|
|
99
|
+
return _get_script_by_path(script_path)
|
|
100
|
+
|
|
101
|
+
func _handle_edit_script(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
102
|
+
var script_path = params.get("script_path", "")
|
|
103
|
+
|
|
104
|
+
var result = {}
|
|
105
|
+
|
|
106
|
+
if script_path.is_empty():
|
|
107
|
+
result = {
|
|
108
|
+
"error": "Script path is required",
|
|
109
|
+
"success": false
|
|
110
|
+
}
|
|
111
|
+
elif not params.has("content"):
|
|
112
|
+
result = {
|
|
113
|
+
"error": "Content is required",
|
|
114
|
+
"success": false
|
|
115
|
+
}
|
|
116
|
+
else:
|
|
117
|
+
var content = params.get("content")
|
|
118
|
+
if content == null:
|
|
119
|
+
result = {
|
|
120
|
+
"error": "Content is required",
|
|
121
|
+
"success": false
|
|
122
|
+
}
|
|
123
|
+
else:
|
|
124
|
+
result = _edit_script_content(script_path, str(content))
|
|
125
|
+
|
|
126
|
+
_send_success(client_id, result, command_id)
|
|
127
|
+
|
|
128
|
+
func _edit_script_content(script_path: String, content: String) -> Dictionary:
|
|
129
|
+
var normalized_path = _normalize_script_path(script_path)
|
|
130
|
+
|
|
131
|
+
var file = FileAccess.open(normalized_path, FileAccess.WRITE)
|
|
132
|
+
if not file:
|
|
133
|
+
return {
|
|
134
|
+
"error": "Failed to open script file for writing",
|
|
135
|
+
"success": false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
file.store_string(content)
|
|
139
|
+
|
|
140
|
+
# Open the script in the editor if possible
|
|
141
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
142
|
+
if plugin:
|
|
143
|
+
var editor_interface = plugin.get_editor_interface()
|
|
144
|
+
var script = load(normalized_path)
|
|
145
|
+
if script:
|
|
146
|
+
editor_interface.edit_resource(script)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"success": true,
|
|
150
|
+
"script_path": normalized_path
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func _normalize_script_path(script_path: String) -> String:
|
|
154
|
+
var normalized_path = script_path
|
|
155
|
+
|
|
156
|
+
if normalized_path.is_empty():
|
|
157
|
+
return normalized_path
|
|
158
|
+
|
|
159
|
+
if not normalized_path.begins_with("res://"):
|
|
160
|
+
normalized_path = "res://" + normalized_path
|
|
161
|
+
|
|
162
|
+
if normalized_path.get_extension().is_empty():
|
|
163
|
+
normalized_path += ".gd"
|
|
164
|
+
|
|
165
|
+
return normalized_path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cpsu8nnmrh2oc
|