@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.
Files changed (115) hide show
  1. package/README.md +187 -0
  2. package/addons/godot_mcp/command_handler.gd +161 -0
  3. package/addons/godot_mcp/command_handler.gd.uid +1 -0
  4. package/addons/godot_mcp/commands/base_command_processor.gd +221 -0
  5. package/addons/godot_mcp/commands/base_command_processor.gd.uid +1 -0
  6. package/addons/godot_mcp/commands/debugger_commands.gd +221 -0
  7. package/addons/godot_mcp/commands/debugger_commands.gd.uid +1 -0
  8. package/addons/godot_mcp/commands/editor_commands.gd +237 -0
  9. package/addons/godot_mcp/commands/editor_commands.gd.uid +1 -0
  10. package/addons/godot_mcp/commands/editor_script_commands.gd +365 -0
  11. package/addons/godot_mcp/commands/editor_script_commands.gd.uid +1 -0
  12. package/addons/godot_mcp/commands/input_commands.gd +337 -0
  13. package/addons/godot_mcp/commands/input_commands.gd.uid +1 -0
  14. package/addons/godot_mcp/commands/node_commands.gd +222 -0
  15. package/addons/godot_mcp/commands/node_commands.gd.uid +1 -0
  16. package/addons/godot_mcp/commands/project_commands.gd +298 -0
  17. package/addons/godot_mcp/commands/project_commands.gd.uid +1 -0
  18. package/addons/godot_mcp/commands/scene_commands.gd +337 -0
  19. package/addons/godot_mcp/commands/scene_commands.gd.uid +1 -0
  20. package/addons/godot_mcp/commands/script_commands.gd +349 -0
  21. package/addons/godot_mcp/commands/script_commands.gd.uid +1 -0
  22. package/addons/godot_mcp/mcp_asset_commands.gd +153 -0
  23. package/addons/godot_mcp/mcp_asset_commands.gd.uid +1 -0
  24. package/addons/godot_mcp/mcp_debug_output_publisher.gd +1669 -0
  25. package/addons/godot_mcp/mcp_debug_output_publisher.gd.uid +1 -0
  26. package/addons/godot_mcp/mcp_debugger_bridge.gd +1455 -0
  27. package/addons/godot_mcp/mcp_debugger_bridge.gd.uid +1 -0
  28. package/addons/godot_mcp/mcp_enhanced_commands.gd +1083 -0
  29. package/addons/godot_mcp/mcp_enhanced_commands.gd.uid +1 -0
  30. package/addons/godot_mcp/mcp_input_handler.gd +545 -0
  31. package/addons/godot_mcp/mcp_input_handler.gd.uid +1 -0
  32. package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd +464 -0
  33. package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd.uid +1 -0
  34. package/addons/godot_mcp/mcp_script_resource_commands.gd +165 -0
  35. package/addons/godot_mcp/mcp_script_resource_commands.gd.uid +1 -0
  36. package/addons/godot_mcp/mcp_server.gd +260 -0
  37. package/addons/godot_mcp/mcp_server.gd.uid +1 -0
  38. package/addons/godot_mcp/plugin.cfg +7 -0
  39. package/addons/godot_mcp/runtime_debugger.gd +81 -0
  40. package/addons/godot_mcp/runtime_debugger.gd.uid +1 -0
  41. package/addons/godot_mcp/ui/mcp_panel.gd +94 -0
  42. package/addons/godot_mcp/ui/mcp_panel.gd.uid +1 -0
  43. package/addons/godot_mcp/ui/mcp_panel.tscn +96 -0
  44. package/addons/godot_mcp/utils/node_utils.gd +82 -0
  45. package/addons/godot_mcp/utils/node_utils.gd.uid +1 -0
  46. package/addons/godot_mcp/utils/resource_utils.gd +81 -0
  47. package/addons/godot_mcp/utils/resource_utils.gd.uid +1 -0
  48. package/addons/godot_mcp/utils/script_utils.gd +114 -0
  49. package/addons/godot_mcp/utils/script_utils.gd.uid +1 -0
  50. package/addons/godot_mcp/websocket_server.gd +197 -0
  51. package/addons/godot_mcp/websocket_server.gd.uid +1 -0
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.js +561 -0
  54. package/dist/cli.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +156 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/resources/asset_resources.d.ts +29 -0
  59. package/dist/resources/asset_resources.js +145 -0
  60. package/dist/resources/asset_resources.js.map +1 -0
  61. package/dist/resources/debug_resources.d.ts +11 -0
  62. package/dist/resources/debug_resources.js +106 -0
  63. package/dist/resources/debug_resources.js.map +1 -0
  64. package/dist/resources/debugger_resources.d.ts +62 -0
  65. package/dist/resources/debugger_resources.js +201 -0
  66. package/dist/resources/debugger_resources.js.map +1 -0
  67. package/dist/resources/editor_resources.d.ts +47 -0
  68. package/dist/resources/editor_resources.js +155 -0
  69. package/dist/resources/editor_resources.js.map +1 -0
  70. package/dist/resources/project_resources.d.ts +33 -0
  71. package/dist/resources/project_resources.js +137 -0
  72. package/dist/resources/project_resources.js.map +1 -0
  73. package/dist/resources/scene_resources.d.ts +33 -0
  74. package/dist/resources/scene_resources.js +160 -0
  75. package/dist/resources/scene_resources.js.map +1 -0
  76. package/dist/resources/script_resources.d.ts +51 -0
  77. package/dist/resources/script_resources.js +203 -0
  78. package/dist/resources/script_resources.js.map +1 -0
  79. package/dist/tools/asset_tools.d.ts +5 -0
  80. package/dist/tools/asset_tools.js +125 -0
  81. package/dist/tools/asset_tools.js.map +1 -0
  82. package/dist/tools/debugger_tools.d.ts +2 -0
  83. package/dist/tools/debugger_tools.js +342 -0
  84. package/dist/tools/debugger_tools.js.map +1 -0
  85. package/dist/tools/editor_tools.d.ts +2 -0
  86. package/dist/tools/editor_tools.js +165 -0
  87. package/dist/tools/editor_tools.js.map +1 -0
  88. package/dist/tools/enhanced_tools.d.ts +5 -0
  89. package/dist/tools/enhanced_tools.js +706 -0
  90. package/dist/tools/enhanced_tools.js.map +1 -0
  91. package/dist/tools/input_tools.d.ts +2 -0
  92. package/dist/tools/input_tools.js +408 -0
  93. package/dist/tools/input_tools.js.map +1 -0
  94. package/dist/tools/node_tools.d.ts +5 -0
  95. package/dist/tools/node_tools.js +217 -0
  96. package/dist/tools/node_tools.js.map +1 -0
  97. package/dist/tools/project_tools.d.ts +5 -0
  98. package/dist/tools/project_tools.js +162 -0
  99. package/dist/tools/project_tools.js.map +1 -0
  100. package/dist/tools/scene_tools.d.ts +5 -0
  101. package/dist/tools/scene_tools.js +260 -0
  102. package/dist/tools/scene_tools.js.map +1 -0
  103. package/dist/tools/script_resource_tools.d.ts +5 -0
  104. package/dist/tools/script_resource_tools.js +5 -0
  105. package/dist/tools/script_resource_tools.js.map +1 -0
  106. package/dist/tools/script_tools.d.ts +5 -0
  107. package/dist/tools/script_tools.js +154 -0
  108. package/dist/tools/script_tools.js.map +1 -0
  109. package/dist/utils/godot_connection.d.ts +30 -0
  110. package/dist/utils/godot_connection.js +285 -0
  111. package/dist/utils/godot_connection.js.map +1 -0
  112. package/dist/utils/types.d.ts +16 -0
  113. package/dist/utils/types.js +2 -0
  114. package/dist/utils/types.js.map +1 -0
  115. package/package.json +58 -0
@@ -0,0 +1,1083 @@
1
+ @tool
2
+ class_name MCPEnhancedCommands
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_editor_scene_structure":
8
+ _handle_get_editor_scene_structure(client_id, params, command_id)
9
+ return true
10
+ "get_runtime_scene_structure":
11
+ _handle_get_runtime_scene_structure(client_id, params, command_id)
12
+ return true
13
+ "get_debug_output":
14
+ _handle_get_debug_output(client_id, params, command_id)
15
+ return true
16
+ "get_editor_errors":
17
+ _handle_get_editor_errors(client_id, params, command_id)
18
+ return true
19
+ "update_node_transform":
20
+ _handle_update_node_transform(client_id, params, command_id)
21
+ return true
22
+ "evaluate_runtime":
23
+ _handle_evaluate_runtime(client_id, params, command_id)
24
+ return true
25
+ "subscribe_debug_output":
26
+ _handle_subscribe_debug_output(client_id, command_id)
27
+ return true
28
+ "unsubscribe_debug_output":
29
+ _handle_unsubscribe_debug_output(client_id, command_id)
30
+ return true
31
+ "get_stack_trace_panel":
32
+ _handle_get_stack_trace_panel(client_id, params, command_id)
33
+ return true
34
+ "get_stack_frames_panel":
35
+ _handle_get_stack_frames_panel(client_id, params, command_id)
36
+ return true
37
+ "clear_debug_output":
38
+ _handle_clear_debug_output(client_id, command_id)
39
+ return true
40
+ "clear_editor_errors":
41
+ _handle_clear_editor_errors(client_id, command_id)
42
+ return true
43
+
44
+ # Command not handled by this processor
45
+ return false
46
+
47
+ # Helper function to get EditorInterface
48
+ func _get_editor_interface():
49
+ var plugin_instance = Engine.get_meta("GodotMCPPlugin") as EditorPlugin
50
+ if plugin_instance:
51
+ return plugin_instance.get_editor_interface()
52
+ return null
53
+
54
+ func _get_runtime_bridge() -> MCPRuntimeDebuggerBridge:
55
+ if Engine.has_meta("MCPRuntimeDebuggerBridge"):
56
+ return Engine.get_meta("MCPRuntimeDebuggerBridge") as MCPRuntimeDebuggerBridge
57
+ return null
58
+
59
+ func _get_debugger_bridge() -> MCPDebuggerBridge:
60
+ if Engine.has_meta("MCPDebuggerBridge"):
61
+ return Engine.get_meta("MCPDebuggerBridge") as MCPDebuggerBridge
62
+ if Engine.has_meta("GodotMCPPlugin"):
63
+ var plugin_instance = Engine.get_meta("GodotMCPPlugin")
64
+ if plugin_instance and plugin_instance.has_method("get_debugger_bridge"):
65
+ var bridge = plugin_instance.get_debugger_bridge()
66
+ if bridge and bridge is MCPDebuggerBridge:
67
+ return bridge
68
+ return null
69
+
70
+ # ---- Scene Structure Commands ----
71
+
72
+ func _handle_get_editor_scene_structure(client_id: int, params: Dictionary, command_id: String) -> void:
73
+ var options = _build_scene_options(params, false, false)
74
+ var result = _build_scene_structure_result(options)
75
+ _send_success(client_id, result, command_id)
76
+
77
+ func _handle_get_runtime_scene_structure(client_id: int, params: Dictionary, command_id: String) -> void:
78
+ var runtime_bridge := _get_runtime_bridge()
79
+ if runtime_bridge == null:
80
+ _send_success(client_id, { "error": "Runtime debugger bridge not available. Ensure the project is running." }, command_id)
81
+ return
82
+
83
+ var options = _build_scene_options(params, false, false)
84
+ var timeout_ms = params.get("timeout_ms", MCPRuntimeDebuggerBridge.DEFAULT_TIMEOUT_MS)
85
+ timeout_ms = int(timeout_ms)
86
+ if timeout_ms < 100:
87
+ timeout_ms = 100
88
+ elif timeout_ms > 5000:
89
+ timeout_ms = 5000
90
+
91
+ var request_info = runtime_bridge.request_runtime_scene_snapshot()
92
+ if request_info.has("error"):
93
+ _send_success(client_id, request_info, command_id)
94
+ return
95
+
96
+ var session_id: int = request_info.get("session_id", -1)
97
+ var baseline_version: int = request_info.get("baseline_version", 0)
98
+ var scene_tree := get_tree()
99
+ if scene_tree == null:
100
+ _send_success(client_id, { "error": "Scene tree unavailable for runtime polling." }, command_id)
101
+ return
102
+
103
+ var deadline: int = Time.get_ticks_msec() + timeout_ms
104
+ var snapshot: Dictionary = {}
105
+
106
+ while Time.get_ticks_msec() <= deadline:
107
+ if runtime_bridge.has_new_runtime_snapshot(session_id, baseline_version):
108
+ snapshot = runtime_bridge.build_runtime_snapshot(session_id, options)
109
+ if not snapshot.is_empty():
110
+ break
111
+
112
+ await scene_tree.process_frame
113
+
114
+ if snapshot.is_empty():
115
+ snapshot = {
116
+ "error": "Timed out waiting for runtime scene data.",
117
+ "hint": "Ensure the remote debugger supports scene tree capture; try opening the Remote Scene tab in Godot or enabling EngineDebugger.set_capture('scene', true) inside the running project."
118
+ }
119
+
120
+ _send_success(client_id, snapshot, command_id)
121
+
122
+ func _handle_evaluate_runtime(client_id: int, params: Dictionary, command_id: String) -> void:
123
+ var runtime_bridge := _get_runtime_bridge()
124
+ if runtime_bridge == null:
125
+ _send_success(client_id, {
126
+ "error": "Runtime debugger bridge not available. Ensure the project is running."
127
+ }, command_id)
128
+ return
129
+
130
+ var expression := ""
131
+ if params.has("expression"):
132
+ expression = str(params.get("expression", ""))
133
+ elif params.has("code"):
134
+ expression = str(params.get("code", ""))
135
+
136
+ if expression.strip_edges().is_empty():
137
+ _send_error(client_id, "Expression cannot be empty", command_id)
138
+ return
139
+
140
+ var options: Dictionary = {}
141
+ if params.has("context_path"):
142
+ options["node_path"] = str(params.get("context_path"))
143
+ elif params.has("node_path"):
144
+ options["node_path"] = str(params.get("node_path"))
145
+
146
+ if params.has("capture_prints"):
147
+ options["capture_prints"] = _coerce_bool(params.get("capture_prints"), true)
148
+
149
+ var timeout_ms := int(params.get("timeout_ms", MCPRuntimeDebuggerBridge.DEFAULT_EVAL_TIMEOUT_MS))
150
+ if timeout_ms < 100:
151
+ timeout_ms = 100
152
+ elif timeout_ms > 5000:
153
+ timeout_ms = 5000
154
+
155
+ var request_info := runtime_bridge.evaluate_runtime_expression(expression, options)
156
+ if request_info.has("error"):
157
+ _send_success(client_id, request_info, command_id)
158
+ return
159
+
160
+ var session_id: int = request_info.get("session_id", -1)
161
+ var request_id: int = request_info.get("request_id", -1)
162
+ if session_id < 0 or request_id < 0:
163
+ _send_success(client_id, { "error": "Failed to enqueue runtime evaluation request." }, command_id)
164
+ return
165
+
166
+ var scene_tree := get_tree()
167
+ if scene_tree == null:
168
+ _send_success(client_id, { "error": "Scene tree unavailable while waiting for runtime evaluation." }, command_id)
169
+ return
170
+
171
+ var deadline: int = Time.get_ticks_msec() + timeout_ms
172
+ var response: Dictionary = {}
173
+
174
+ while Time.get_ticks_msec() <= deadline:
175
+ if runtime_bridge.has_eval_result(session_id, request_id):
176
+ response = runtime_bridge.take_eval_result(session_id, request_id)
177
+ break
178
+ await scene_tree.process_frame
179
+
180
+ if response.is_empty():
181
+ response = {
182
+ "error": "Timed out waiting for runtime evaluation result.",
183
+ "hint": "Ensure the running project registers the mcp_eval debugger capture via EngineDebugger.register_message_capture."
184
+ }
185
+ elif not response.get("success", true) and not response.has("error"):
186
+ response["error"] = "Runtime evaluation failed."
187
+
188
+ _send_success(client_id, response, command_id)
189
+
190
+ func _build_scene_structure_result(options: Dictionary) -> Dictionary:
191
+ var editor_interface = _get_editor_interface()
192
+ if not editor_interface:
193
+ return { "error": "Could not access EditorInterface" }
194
+
195
+ var root = editor_interface.get_edited_scene_root()
196
+ if not root:
197
+ return { "error": "No scene is currently being edited" }
198
+
199
+ var scene_path = ""
200
+
201
+ if "scene_file_path" in root:
202
+ scene_path = root.scene_file_path
203
+ if typeof(scene_path) != TYPE_STRING:
204
+ scene_path = str(scene_path)
205
+
206
+ if scene_path.is_empty():
207
+ scene_path = "Unsaved Scene"
208
+
209
+ return {
210
+ "scene_path": scene_path,
211
+ "path": scene_path,
212
+ "root_node_type": root.get_class(),
213
+ "root_node_name": root.name,
214
+ "structure": _build_node_info(root, options, 0)
215
+ }
216
+
217
+ func _build_scene_options(params: Dictionary, include_properties_default: bool, include_scripts_default: bool) -> Dictionary:
218
+ var include_properties = include_properties_default
219
+ if params.has("include_properties"):
220
+ include_properties = _coerce_bool(params.get("include_properties"), include_properties_default)
221
+
222
+ var include_scripts = include_scripts_default
223
+ if params.has("include_scripts"):
224
+ include_scripts = _coerce_bool(params.get("include_scripts"), include_scripts_default)
225
+
226
+ var max_depth = -1
227
+ if params.has("max_depth"):
228
+ max_depth = int(params.get("max_depth"))
229
+
230
+ return {
231
+ "include_properties": include_properties,
232
+ "include_scripts": include_scripts,
233
+ "max_depth": max_depth
234
+ }
235
+
236
+ func _coerce_bool(value, default: bool) -> bool:
237
+ if typeof(value) == TYPE_BOOL:
238
+ return value
239
+ if typeof(value) == TYPE_STRING:
240
+ var lowered = value.to_lower()
241
+ if lowered == "true":
242
+ return true
243
+ if lowered == "false":
244
+ return false
245
+ return bool(value) if value != null else default
246
+
247
+ func _build_node_info(node: Node, options: Dictionary, depth: int) -> Dictionary:
248
+ var info = {
249
+ "name": node.name,
250
+ "type": node.get_class(),
251
+ "path": node.get_path(),
252
+ "children": []
253
+ }
254
+
255
+ if options.get("include_properties", false):
256
+ var properties = {}
257
+ if node.has_method("get_property_list"):
258
+ var props = node.get_property_list()
259
+ for prop in props:
260
+ if prop.usage & PROPERTY_USAGE_EDITOR and not (prop.usage & PROPERTY_USAGE_CATEGORY):
261
+ if prop.name in ["position", "rotation", "scale", "text", "visible"]:
262
+ properties[prop.name] = node.get(prop.name)
263
+ if properties.size() > 0:
264
+ info["properties"] = properties
265
+
266
+ if options.get("include_scripts", false):
267
+ var script = node.get_script()
268
+ if script:
269
+ var script_path = ""
270
+ var class_name_str = ""
271
+
272
+ if typeof(script) == TYPE_OBJECT:
273
+ if script.has_method("get_path") or "resource_path" in script:
274
+ script_path = script.resource_path if "resource_path" in script else ""
275
+
276
+ if script.has_method("get_instance_base_type"):
277
+ class_name_str = script.get_instance_base_type()
278
+
279
+ info["script"] = {
280
+ "path": script_path,
281
+ "class_name": class_name_str
282
+ }
283
+
284
+ var max_depth = options.get("max_depth", -1)
285
+ if max_depth >= 0 and depth >= max_depth:
286
+ return info
287
+
288
+ for child in node.get_children():
289
+ info["children"].append(_build_node_info(child, options, depth + 1))
290
+
291
+ return info
292
+
293
+ # ---- Debug Output Commands ----
294
+
295
+ func _handle_get_debug_output(client_id: int, _params: Dictionary, command_id: String) -> void:
296
+ var result = get_debug_output()
297
+
298
+ _send_success(client_id, result, command_id)
299
+
300
+ func _handle_get_editor_errors(client_id: int, _params: Dictionary, command_id: String) -> void:
301
+ var snapshot := _capture_editor_errors_snapshot()
302
+ var diagnostics := snapshot.get("diagnostics", {})
303
+ if diagnostics.has("error"):
304
+ _send_error(client_id, "Failed to read Errors tab: %s" % diagnostics["error"], command_id)
305
+ return
306
+ _send_success(client_id, snapshot, command_id)
307
+
308
+ func get_debug_output() -> Dictionary:
309
+ var output := ""
310
+ var publisher := _get_debug_output_publisher()
311
+ var diagnostics: Dictionary = {}
312
+ if publisher:
313
+ output = publisher.get_full_log_text()
314
+
315
+ if publisher.has_method("get_capture_diagnostics"):
316
+ diagnostics = publisher.get_capture_diagnostics()
317
+ else:
318
+ # Fallback to remote debugger log if publisher unavailable.
319
+ var source := "none"
320
+ var detail := ""
321
+ if Engine.has_singleton("EditorDebuggerNode"):
322
+ var debugger = Engine.get_singleton("EditorDebuggerNode")
323
+ if debugger and debugger.has_method("get_log"):
324
+ output = debugger.get_log()
325
+ source = "debugger_singleton"
326
+ detail = "len=%d" % output.length()
327
+ elif has_node("/root/EditorNode/DebuggerPanel"):
328
+ var debugger_panel = get_node("/root/EditorNode/DebuggerPanel")
329
+ if debugger_panel and debugger_panel.has_method("get_output"):
330
+ output = debugger_panel.get_output()
331
+ source = "debugger_panel"
332
+ detail = "len=%d" % output.length()
333
+
334
+ diagnostics = {
335
+ "source": source,
336
+ "detail": detail,
337
+ "timestamp": Time.get_ticks_msec()
338
+ }
339
+
340
+ return {
341
+ "output": output,
342
+ "diagnostics": diagnostics
343
+ }
344
+
345
+ func _handle_subscribe_debug_output(client_id: int, command_id: String) -> void:
346
+ var publisher := _get_debug_output_publisher()
347
+ if publisher == null:
348
+ _send_error(client_id, "Debug output publisher unavailable.", command_id)
349
+ return
350
+
351
+ publisher.subscribe(client_id)
352
+ _send_success(client_id, {
353
+ "subscribed": true,
354
+ "message": "Live debug output streaming enabled. Future log frames will be delivered asynchronously."
355
+ }, command_id)
356
+
357
+ func _handle_unsubscribe_debug_output(client_id: int, command_id: String) -> void:
358
+ var publisher := _get_debug_output_publisher()
359
+ if publisher == null:
360
+ _send_error(client_id, "Debug output publisher unavailable.", command_id)
361
+ return
362
+
363
+ publisher.unsubscribe(client_id)
364
+ _send_success(client_id, {
365
+ "subscribed": false,
366
+ "message": "Live debug output streaming disabled for this client."
367
+ }, command_id)
368
+
369
+ func _handle_get_stack_trace_panel(client_id: int, params: Dictionary, command_id: String) -> void:
370
+ var publisher := _get_debug_output_publisher()
371
+ var snapshot: Dictionary = {
372
+ "text": "",
373
+ "lines": [],
374
+ "line_count": 0,
375
+ "frames": [],
376
+ "diagnostics": {
377
+ "error": "debug_output_publisher_unavailable",
378
+ "timestamp": Time.get_ticks_msec()
379
+ }
380
+ }
381
+
382
+ if publisher and publisher.has_method("get_stack_trace_snapshot"):
383
+ var snapshot_session := int(params.get("session_id", -1))
384
+ snapshot = publisher.get_stack_trace_snapshot(snapshot_session)
385
+
386
+ var snapshot_diagnostics := snapshot.get("diagnostics", {})
387
+ if typeof(snapshot_diagnostics) != TYPE_DICTIONARY:
388
+ snapshot_diagnostics = {}
389
+ snapshot["diagnostics"] = snapshot_diagnostics
390
+
391
+ var debugger_state := {}
392
+ var requested_session := int(params.get("session_id", -1))
393
+ var resolved_session_id := requested_session
394
+ var debugger_error := ""
395
+ var debugger_bridge := _get_debugger_bridge()
396
+
397
+ if debugger_bridge:
398
+ debugger_state = debugger_bridge.get_current_state()
399
+ var active_sessions := debugger_state.get("active_sessions", [])
400
+ if resolved_session_id < 0 and active_sessions is Array and not active_sessions.is_empty():
401
+ resolved_session_id = debugger_state.get("current_session_id", -1)
402
+ if resolved_session_id < 0:
403
+ resolved_session_id = active_sessions[0]
404
+ if resolved_session_id >= 0:
405
+ var frames := []
406
+ if snapshot.has("frames") and snapshot["frames"] is Array:
407
+ frames = snapshot["frames"]
408
+ var lines := []
409
+ if snapshot.has("lines") and snapshot["lines"] is Array:
410
+ lines = snapshot["lines"]
411
+ var needs_stack := frames.is_empty()
412
+ var needs_lines := lines.is_empty()
413
+ if needs_stack or needs_lines:
414
+ if debugger_bridge.has_method("get_cached_stack_info"):
415
+ var fallback := debugger_bridge.get_cached_stack_info(resolved_session_id)
416
+ var fallback_frames := fallback.get("frames", [])
417
+ if fallback_frames is Array and not fallback_frames.is_empty():
418
+ snapshot["frames"] = fallback_frames
419
+ snapshot_diagnostics["fallback_source"] = "debugger_bridge"
420
+ snapshot_diagnostics["fallback_frame_count"] = fallback.get("total_frames", fallback_frames.size())
421
+ if needs_lines:
422
+ var formatted_lines := _format_frames_as_lines(fallback_frames)
423
+ snapshot["lines"] = formatted_lines
424
+ if snapshot.get("text", "") == "":
425
+ snapshot["text"] = "\n".join(formatted_lines)
426
+ elif fallback.has("error"):
427
+ snapshot_diagnostics["fallback_error"] = fallback["error"]
428
+ else:
429
+ debugger_error = "Debugger bridge unavailable"
430
+
431
+ var response := {
432
+ "stack_trace_panel": snapshot,
433
+ "session_id": resolved_session_id,
434
+ "debugger_state": debugger_state,
435
+ "timestamp": Time.get_ticks_msec()
436
+ }
437
+
438
+ if not debugger_error.is_empty():
439
+ response["call_stack_error"] = debugger_error
440
+
441
+ _send_success(client_id, response, command_id)
442
+
443
+ func _handle_get_stack_frames_panel(client_id: int, params: Dictionary, command_id: String) -> void:
444
+ var publisher := _get_debug_output_publisher()
445
+ var snapshot: Dictionary = {
446
+ "text": "",
447
+ "lines": [],
448
+ "line_count": 0,
449
+ "frames": [],
450
+ "diagnostics": {
451
+ "error": "debug_output_publisher_unavailable",
452
+ "timestamp": Time.get_ticks_msec()
453
+ }
454
+ }
455
+
456
+ if publisher and publisher.has_method("get_stack_frames_snapshot"):
457
+ var snapshot_session := int(params.get("session_id", -1))
458
+ snapshot = publisher.get_stack_frames_snapshot(snapshot_session)
459
+
460
+ var snapshot_diagnostics := snapshot.get("diagnostics", {})
461
+ if typeof(snapshot_diagnostics) != TYPE_DICTIONARY:
462
+ snapshot_diagnostics = {}
463
+ snapshot["diagnostics"] = snapshot_diagnostics
464
+
465
+ var debugger_state := {}
466
+ var requested_session := int(params.get("session_id", -1))
467
+ var resolved_session_id := requested_session
468
+ var debugger_error := ""
469
+ var debugger_bridge := _get_debugger_bridge()
470
+
471
+ var refresh_requested := bool(params.get("refresh", false))
472
+ if debugger_bridge:
473
+ debugger_state = debugger_bridge.get_current_state()
474
+ var active_sessions := debugger_state.get("active_sessions", [])
475
+ if resolved_session_id < 0 and active_sessions is Array and not active_sessions.is_empty():
476
+ resolved_session_id = debugger_state.get("current_session_id", -1)
477
+ if resolved_session_id < 0:
478
+ resolved_session_id = active_sessions[0]
479
+ if resolved_session_id >= 0:
480
+ var frames := []
481
+ if snapshot.has("frames") and snapshot["frames"] is Array:
482
+ frames = snapshot["frames"]
483
+ var needs_stack := frames.is_empty()
484
+ var needs_enrichment := _frames_need_enrichment(frames)
485
+ var existing_lines := []
486
+ if snapshot.has("lines") and snapshot["lines"] is Array:
487
+ existing_lines = snapshot["lines"]
488
+ var needs_lines := existing_lines.is_empty()
489
+ var require_debugger_frames := refresh_requested or needs_stack or needs_lines or needs_enrichment
490
+ if require_debugger_frames and debugger_bridge.has_method("get_cached_stack_info"):
491
+ var debugger_snapshot := await _fetch_debugger_stack_frames(debugger_bridge, resolved_session_id, refresh_requested)
492
+ var fallback_frames := debugger_snapshot.get("frames", [])
493
+ if fallback_frames is Array and not fallback_frames.is_empty():
494
+ snapshot["frames"] = fallback_frames
495
+ snapshot_diagnostics["fallback_source"] = "debugger_bridge"
496
+ snapshot_diagnostics["fallback_frame_count"] = debugger_snapshot.get("total_frames", fallback_frames.size())
497
+ if needs_lines or snapshot.get("lines", []).is_empty():
498
+ var formatted_lines := _format_stack_frames_panel_lines(fallback_frames)
499
+ if not formatted_lines.is_empty():
500
+ snapshot["lines"] = formatted_lines
501
+ snapshot["line_count"] = formatted_lines.size()
502
+ if snapshot.get("text", "") == "":
503
+ snapshot["text"] = "\n".join(formatted_lines)
504
+ needs_stack = false
505
+ needs_enrichment = false
506
+ needs_lines = false
507
+ elif debugger_snapshot.has("error"):
508
+ snapshot_diagnostics["fallback_error"] = debugger_snapshot["error"]
509
+ if needs_lines and snapshot.get("text", "") != "":
510
+ var fallback_lines := String(snapshot["text"]).split("\n", false)
511
+ if not fallback_lines.is_empty():
512
+ snapshot["lines"] = fallback_lines
513
+ snapshot["line_count"] = fallback_lines.size()
514
+ if needs_stack and not snapshot.has("frames"):
515
+ snapshot["frames"] = []
516
+ else:
517
+ debugger_error = "Debugger bridge unavailable"
518
+
519
+ var snapshot_frames := []
520
+ if snapshot.has("frames") and snapshot["frames"] is Array:
521
+ snapshot_frames = snapshot["frames"]
522
+ if snapshot_frames is Array and not snapshot_frames.is_empty():
523
+ var formatted_frame_lines := _format_stack_frames_panel_lines(snapshot_frames)
524
+ if not formatted_frame_lines.is_empty():
525
+ snapshot["lines"] = formatted_frame_lines
526
+ snapshot["line_count"] = formatted_frame_lines.size()
527
+ snapshot["text"] = "\n".join(formatted_frame_lines)
528
+
529
+ var response := {
530
+ "stack_frames_panel": snapshot,
531
+ "session_id": resolved_session_id,
532
+ "debugger_state": debugger_state,
533
+ "timestamp": Time.get_ticks_msec()
534
+ }
535
+
536
+ if not debugger_error.is_empty():
537
+ response["stack_frames_error"] = debugger_error
538
+
539
+ _send_success(client_id, response, command_id)
540
+
541
+ func _fetch_debugger_stack_frames(debugger_bridge: MCPDebuggerBridge, session_id: int, refresh_requested: bool) -> Dictionary:
542
+ if debugger_bridge == null or session_id < 0:
543
+ return {}
544
+ var result := {}
545
+ if refresh_requested:
546
+ result = await debugger_bridge.get_call_stack(session_id)
547
+ if typeof(result) == TYPE_DICTIONARY and result.get("frames", []).size() > 0:
548
+ return result
549
+
550
+ result = debugger_bridge.get_cached_stack_info(session_id)
551
+ var frames := result.get("frames", [])
552
+ if frames is Array and not frames.is_empty():
553
+ return result
554
+
555
+ var refreshed_result = await debugger_bridge.get_call_stack(session_id)
556
+ if typeof(refreshed_result) == TYPE_DICTIONARY and refreshed_result.get("frames", []).is_empty() == false:
557
+ return refreshed_result
558
+
559
+ var log_frames := _build_frames_from_debug_output()
560
+ if not log_frames.is_empty():
561
+ return log_frames
562
+
563
+ return result
564
+
565
+ func _frames_need_enrichment(frames: Array) -> bool:
566
+ if frames.is_empty():
567
+ return false
568
+ for frame in frames:
569
+ if typeof(frame) != TYPE_DICTIONARY:
570
+ continue
571
+ var script := ""
572
+ if frame.has("script") and frame["script"] is String:
573
+ script = frame["script"]
574
+ elif frame.has("file") and frame["file"] is String:
575
+ script = frame["file"]
576
+ var line := -1
577
+ if frame.has("line") and frame["line"] is int:
578
+ line = frame["line"]
579
+ if script == "" or line < 0:
580
+ return true
581
+ return false
582
+
583
+ func _handle_clear_debug_output(client_id: int, command_id: String) -> void:
584
+ var publisher := _get_debug_output_publisher()
585
+ if publisher == null or not publisher.has_method("clear_log_output"):
586
+ _send_error(client_id, "Cannot clear Output panel because the debug output publisher is unavailable.", command_id)
587
+ return
588
+
589
+ var result := publisher.clear_log_output()
590
+ if not result.has("message"):
591
+ if result.get("cleared", false):
592
+ result["message"] = "Debug Output panel cleared."
593
+ else:
594
+ result["message"] = "Debug Output panel could not be cleared automatically."
595
+
596
+ _send_success(client_id, result, command_id)
597
+
598
+ func _handle_clear_editor_errors(client_id: int, command_id: String) -> void:
599
+ var publisher := _get_debug_output_publisher()
600
+ if publisher == null or not publisher.has_method("clear_errors_panel"):
601
+ _send_error(client_id, "Cannot clear Errors tab because the debug output publisher is unavailable.", command_id)
602
+ return
603
+
604
+ var result := publisher.clear_errors_panel()
605
+ if not result.has("message"):
606
+ if result.get("cleared", false):
607
+ result["message"] = "Errors tab cleared successfully."
608
+ else:
609
+ result["message"] = "Errors tab could not be cleared automatically."
610
+
611
+ _send_success(client_id, result, command_id)
612
+
613
+ func _get_debug_output_publisher() -> MCPDebugOutputPublisher:
614
+ if Engine.has_meta("MCPDebugOutputPublisher"):
615
+ var publisher = Engine.get_meta("MCPDebugOutputPublisher")
616
+ if publisher and publisher is MCPDebugOutputPublisher:
617
+ return publisher
618
+ return null
619
+
620
+ func _capture_editor_errors_snapshot() -> Dictionary:
621
+ var publisher := _get_debug_output_publisher()
622
+ if publisher and publisher.has_method("get_errors_panel_snapshot"):
623
+ return publisher.get_errors_panel_snapshot()
624
+
625
+ var diagnostics := {
626
+ "timestamp": Time.get_ticks_msec()
627
+ }
628
+
629
+ if not Engine.is_editor_hint():
630
+ diagnostics["error"] = "editor_only"
631
+ return {
632
+ "text": "",
633
+ "lines": [],
634
+ "line_count": 0,
635
+ "diagnostics": diagnostics
636
+ }
637
+
638
+ var search_roots: Array = []
639
+ var editor_node = Engine.get_singleton("EditorNode") if Engine.has_singleton("EditorNode") else null
640
+ if editor_node:
641
+ if editor_node.has_method("get_log"):
642
+ var editor_log = editor_node.call("get_log")
643
+ if is_instance_valid(editor_log):
644
+ search_roots.append(editor_log)
645
+ search_roots.append(editor_node)
646
+
647
+ var plugin = Engine.get_meta("GodotMCPPlugin") if Engine.has_meta("GodotMCPPlugin") else null
648
+ if plugin and plugin is EditorPlugin:
649
+ var editor_interface = plugin.get_editor_interface()
650
+ if editor_interface and editor_interface.has_method("get_base_control"):
651
+ var base_control = editor_interface.call("get_base_control")
652
+ if is_instance_valid(base_control):
653
+ search_roots.append(base_control)
654
+
655
+ var scene_tree := get_tree()
656
+ if scene_tree:
657
+ var tree_root := scene_tree.get_root()
658
+ if is_instance_valid(tree_root):
659
+ search_roots.append(tree_root)
660
+
661
+ var aggregated_summary: Array = []
662
+ var tab_info := {}
663
+ for root in search_roots:
664
+ tab_info = _find_errors_tab_in_editor_log(root)
665
+ var summary := String(tab_info.get("summary", ""))
666
+ if not summary.is_empty():
667
+ aggregated_summary.append(summary)
668
+ if tab_info.has("control"):
669
+ break
670
+ if aggregated_summary.size() > 0:
671
+ diagnostics["search_summary"] = " | ".join(aggregated_summary)
672
+ else:
673
+ diagnostics["search_summary"] = ""
674
+
675
+ if tab_info.is_empty() or not tab_info.has("control"):
676
+ diagnostics["error"] = "errors_tab_not_found"
677
+ return {
678
+ "text": "",
679
+ "lines": [],
680
+ "line_count": 0,
681
+ "diagnostics": diagnostics
682
+ }
683
+
684
+ var tab_control: Control = tab_info.get("control")
685
+ if not is_instance_valid(tab_control):
686
+ diagnostics["error"] = "tab_control_invalid"
687
+ return {
688
+ "text": "",
689
+ "lines": [],
690
+ "line_count": 0,
691
+ "diagnostics": diagnostics
692
+ }
693
+
694
+ diagnostics["tab_title"] = tab_info.get("tab_title", "")
695
+ if tab_control.is_inside_tree():
696
+ diagnostics["control_path"] = String(tab_control.get_path())
697
+ else:
698
+ diagnostics["control_path"] = ""
699
+
700
+ var lines: Array = []
701
+ var text := ""
702
+ var tree := _locate_descendant_tree(tab_control)
703
+ if tree:
704
+ if tree.is_inside_tree():
705
+ diagnostics["tree_path"] = String(tree.get_path())
706
+ lines = _collect_tree_lines(tree)
707
+ text = "\n".join(lines)
708
+ else:
709
+ text = _extract_text_from_control_local(tab_control)
710
+ if not text.is_empty():
711
+ lines = text.split("\n", false)
712
+
713
+ return {
714
+ "text": text,
715
+ "lines": lines,
716
+ "line_count": lines.size(),
717
+ "diagnostics": diagnostics
718
+ }
719
+
720
+ func _find_errors_tab_in_editor_log(root: Node) -> Dictionary:
721
+ var queue: Array = []
722
+ var summary: Array = []
723
+ if is_instance_valid(root):
724
+ queue.append(root)
725
+ else:
726
+ return {}
727
+
728
+ var visited := 0
729
+ while queue.size() > 0:
730
+ var candidate = queue.pop_front()
731
+ if not is_instance_valid(candidate):
732
+ continue
733
+ visited += 1
734
+
735
+ if candidate is TabContainer:
736
+ var tab_container: TabContainer = candidate
737
+ var tab_count: int = tab_container.get_tab_count() if tab_container.has_method("get_tab_count") else tab_container.get_child_count()
738
+ for i in range(tab_count):
739
+ var title := ""
740
+ if tab_container.has_method("get_tab_title"):
741
+ title = String(tab_container.get_tab_title(i))
742
+ var title_lower := title.to_lower()
743
+ if title_lower.find("error") != -1:
744
+ var tab_control: Control = null
745
+ if tab_container.has_method("get_tab_control"):
746
+ tab_control = tab_container.get_tab_control(i)
747
+ if not is_instance_valid(tab_control) and i < tab_container.get_child_count():
748
+ var child = tab_container.get_child(i)
749
+ if child is Control:
750
+ tab_control = child
751
+
752
+ if is_instance_valid(tab_control):
753
+ tab_control = _unwrap_single_child_control(tab_control)
754
+ summary.append("tab_found=%s" % title)
755
+ summary.append("visited=%d" % visited)
756
+ return {
757
+ "control": tab_control,
758
+ "tab_title": title,
759
+ "summary": "; ".join(summary)
760
+ }
761
+ for child in candidate.get_children():
762
+ if child is Node:
763
+ queue.append(child)
764
+
765
+ return {"summary": "; ".join(summary)}
766
+
767
+ func _unwrap_single_child_control(control: Control) -> Control:
768
+ if not is_instance_valid(control):
769
+ return control
770
+
771
+ var current := control
772
+ var safety := 0
773
+ while safety < 5 and current.get_child_count() == 1:
774
+ var child = current.get_child(0)
775
+ if child is Control:
776
+ current = child
777
+ safety += 1
778
+ else:
779
+ break
780
+
781
+ if current.get_child_count() > 1:
782
+ for child in current.get_children():
783
+ if child is Control and (_is_text_display_control_local(child)):
784
+ return child
785
+
786
+ return current
787
+
788
+ func _locate_descendant_tree(root: Node, max_nodes: int = 8192) -> Tree:
789
+ if not is_instance_valid(root):
790
+ return null
791
+ var queue: Array = [root]
792
+ var visited := 0
793
+ while queue.size() > 0 and visited < max_nodes:
794
+ var candidate = queue.pop_front()
795
+ visited += 1
796
+ if not is_instance_valid(candidate):
797
+ continue
798
+ if candidate is Tree:
799
+ return candidate
800
+ for child in candidate.get_children():
801
+ if child is Node:
802
+ queue.append(child)
803
+ return null
804
+
805
+ func _collect_tree_lines(tree: Tree) -> Array:
806
+ var lines: Array = []
807
+ if not is_instance_valid(tree):
808
+ return lines
809
+ var root := tree.get_root()
810
+ if not root:
811
+ return lines
812
+ var item := root.get_first_child()
813
+ var column_count: int = tree.columns
814
+ while item:
815
+ _collect_tree_item_lines(item, lines, 0, column_count)
816
+ item = item.get_next()
817
+ return lines
818
+
819
+ func _collect_tree_item_lines(item: TreeItem, lines: Array, depth: int, column_count: int) -> void:
820
+ if not is_instance_valid(item):
821
+ return
822
+
823
+ var parts: Array = []
824
+ if item.has_meta("_is_warning"):
825
+ parts.append("[warning]")
826
+ elif item.has_meta("_is_error"):
827
+ parts.append("[error]")
828
+
829
+ var primary := item.get_text(0)
830
+ if not primary.is_empty():
831
+ parts.append(primary)
832
+
833
+ for col in range(1, column_count):
834
+ var extra := item.get_text(col)
835
+ if not extra.is_empty():
836
+ parts.append(extra)
837
+
838
+ if parts.size() > 0:
839
+ var prefix := _make_indent_local(depth)
840
+ lines.append(prefix + " ".join(parts))
841
+
842
+ var child := item.get_first_child()
843
+ while child:
844
+ _collect_tree_item_lines(child, lines, depth + 1, column_count)
845
+ child = child.get_next()
846
+
847
+ func _is_text_display_control_local(control: Control) -> bool:
848
+ if not is_instance_valid(control):
849
+ return false
850
+ return control.is_class("TextEdit") or control.is_class("CodeEdit") or control.is_class("RichTextLabel")
851
+
852
+ func _extract_text_from_control_local(control: Object) -> String:
853
+ if not is_instance_valid(control):
854
+ return ""
855
+
856
+ if control.has_method("get_parsed_text"):
857
+ return String(control.call("get_parsed_text"))
858
+ if control.has_method("get_text"):
859
+ return String(control.call("get_text"))
860
+ if control.has_method("get_full_text"):
861
+ return String(control.call("get_full_text"))
862
+ if control.has_method("get_line_count") and control.has_method("get_line"):
863
+ var lines: Array = []
864
+ var count := int(control.call("get_line_count"))
865
+ for i in range(count):
866
+ lines.append(String(control.call("get_line", i)))
867
+ return "\n".join(lines)
868
+ return ""
869
+
870
+ func _make_indent_local(depth: int) -> String:
871
+ if depth <= 0:
872
+ return ""
873
+ var spaces := depth * 2
874
+ var builder := ""
875
+ for i in range(spaces):
876
+ builder += " "
877
+ return builder
878
+
879
+ # ---- Node Transform Commands ----
880
+
881
+ func _handle_update_node_transform(client_id: int, params: Dictionary, command_id: String) -> void:
882
+ var node_path = params.get("node_path", "")
883
+ var position = params.get("position", null)
884
+ var rotation = params.get("rotation", null)
885
+ var scale = params.get("scale", null)
886
+
887
+ var result = update_node_transform(node_path, position, rotation, scale)
888
+
889
+ _send_success(client_id, result, command_id)
890
+
891
+ func update_node_transform(node_path: String, position, rotation, scale) -> Dictionary:
892
+ var editor_interface = _get_editor_interface()
893
+
894
+ if not editor_interface:
895
+ return { "error": "Could not access EditorInterface" }
896
+
897
+ var scene_root = editor_interface.get_edited_scene_root()
898
+
899
+ if not scene_root:
900
+ return { "error": "No scene open" }
901
+
902
+ var node = scene_root.get_node_or_null(node_path)
903
+ if not node:
904
+ return { "error": "Node not found" }
905
+
906
+ # Update all specified properties
907
+ if position != null and node.has_method("set_position"):
908
+ if position is Array and position.size() >= 2:
909
+ node.set_position(Vector2(position[0], position[1]))
910
+ elif typeof(position) == TYPE_DICTIONARY and "x" in position and "y" in position:
911
+ node.set_position(Vector2(position.x, position.y))
912
+
913
+ if rotation != null and node.has_method("set_rotation"):
914
+ node.set_rotation(rotation)
915
+
916
+ if scale != null and node.has_method("set_scale"):
917
+ if scale is Array and scale.size() >= 2:
918
+ node.set_scale(Vector2(scale[0], scale[1]))
919
+ elif typeof(scale) == TYPE_DICTIONARY and "x" in scale and "y" in scale:
920
+ node.set_scale(Vector2(scale.x, scale.y))
921
+
922
+ # Mark the scene as modified
923
+ editor_interface.mark_scene_as_unsaved()
924
+
925
+ return {
926
+ "success": true,
927
+ "node_path": node_path,
928
+ "updated": {
929
+ "position": position != null,
930
+ "rotation": rotation != null,
931
+ "scale": scale != null
932
+ }
933
+ }
934
+
935
+ func _format_frames_as_lines(frames: Array) -> Array:
936
+ var lines: Array = []
937
+ for i in range(frames.size()):
938
+ var frame = frames[i]
939
+ if typeof(frame) != TYPE_DICTIONARY:
940
+ continue
941
+ var index: int = i
942
+ if frame.has("index") and frame["index"] is int:
943
+ index = frame["index"]
944
+ var script: String = ""
945
+ if frame.has("script") and frame["script"] is String:
946
+ script = frame["script"]
947
+ elif frame.has("file") and frame["file"] is String:
948
+ script = frame["file"]
949
+ var line_num: int = -1
950
+ if frame.has("line") and frame["line"] is int:
951
+ line_num = frame["line"]
952
+ var function_name: String = ""
953
+ if frame.has("function") and frame["function"] is String:
954
+ function_name = frame["function"]
955
+ var location := ""
956
+ if script is String and not script.is_empty():
957
+ location = script
958
+ if line_num is int and line_num >= 0:
959
+ if location.is_empty():
960
+ location = ":%d" % line_num
961
+ else:
962
+ location += ":%d" % line_num
963
+ if location.is_empty():
964
+ location = String(frame.get("location", "unknown location"))
965
+ var fn_display := function_name if function_name is String and not function_name.is_empty() else "(anonymous)"
966
+ lines.append("#%d %s — %s" % [index, fn_display, location])
967
+ return lines
968
+
969
+ func _build_frames_from_debug_output() -> Dictionary:
970
+ var frames := _parse_frames_from_debug_output()
971
+ if frames.is_empty():
972
+ return {}
973
+ return {
974
+ "frames": frames,
975
+ "total_frames": frames.size(),
976
+ "current_frame": 0,
977
+ "source": "debug_output"
978
+ }
979
+
980
+ func _parse_frames_from_debug_output() -> Array:
981
+ var publisher := _get_debug_output_publisher()
982
+ if publisher == null or not publisher.has_method("get_full_log_text"):
983
+ return []
984
+ var text := publisher.get_full_log_text()
985
+ if text.is_empty():
986
+ return []
987
+ var lines := text.split("\n", false)
988
+ var frame_lines: Array = []
989
+ var collecting := false
990
+ for i in range(lines.size() - 1, -1, -1):
991
+ var line := String(lines[i]).strip_edges()
992
+ if line.begins_with("Frame "):
993
+ collecting = true
994
+ frame_lines.push_front(line)
995
+ elif collecting:
996
+ break
997
+ if frame_lines.is_empty():
998
+ return []
999
+ var frames := []
1000
+ for line in frame_lines:
1001
+ var frame_dict := _parse_print_stack_line(line)
1002
+ if not frame_dict.is_empty():
1003
+ frames.append(frame_dict)
1004
+ return frames
1005
+
1006
+ func _parse_print_stack_line(line: String) -> Dictionary:
1007
+ var trimmed := line.strip_edges()
1008
+ if not trimmed.begins_with("Frame "):
1009
+ return {}
1010
+ var dash_index := trimmed.find(" - ")
1011
+ if dash_index == -1:
1012
+ return {}
1013
+ var index_text := trimmed.substr(6, dash_index - 6).strip_edges()
1014
+ var index_value := 0
1015
+ if index_text.is_valid_int():
1016
+ index_value = int(index_text)
1017
+ var rest := trimmed.substr(dash_index + 3).strip_edges()
1018
+ var func_name := ""
1019
+ var location := rest
1020
+ var at_index := rest.find("@")
1021
+ if at_index != -1:
1022
+ location = rest.substr(0, at_index).strip_edges()
1023
+ func_name = rest.substr(at_index + 1).strip_edges()
1024
+ if func_name.ends_with("()"):
1025
+ func_name = func_name.substr(0, func_name.length() - 2)
1026
+ var script_path := ""
1027
+ var line_number := -1
1028
+ var colon_index := location.rfind(":")
1029
+ if colon_index != -1:
1030
+ var line_text := location.substr(colon_index + 1).strip_edges()
1031
+ if line_text.is_valid_int():
1032
+ line_number = int(line_text)
1033
+ script_path = location.substr(0, colon_index).strip_edges()
1034
+ else:
1035
+ script_path = location
1036
+ return {
1037
+ "index": index_value,
1038
+ "function": func_name,
1039
+ "script": script_path,
1040
+ "file": script_path,
1041
+ "line": line_number,
1042
+ "location": location
1043
+ }
1044
+
1045
+ func _format_stack_frames_panel_lines(frames: Array) -> Array:
1046
+ var lines: Array = []
1047
+ for i in range(frames.size()):
1048
+ var frame = frames[i]
1049
+ if typeof(frame) != TYPE_DICTIONARY:
1050
+ continue
1051
+ var index_value := i
1052
+ if frame.has("index"):
1053
+ var raw_index = frame["index"]
1054
+ if typeof(raw_index) == TYPE_INT:
1055
+ index_value = raw_index
1056
+ elif typeof(raw_index) == TYPE_STRING:
1057
+ var index_text := String(raw_index).strip_edges()
1058
+ if index_text.is_valid_int():
1059
+ index_value = int(index_text)
1060
+ var script_path := ""
1061
+ if frame.has("script") and typeof(frame["script"]) == TYPE_STRING:
1062
+ script_path = frame["script"]
1063
+ elif frame.has("file") and typeof(frame["file"]) == TYPE_STRING:
1064
+ script_path = frame["file"]
1065
+ var line_number := -1
1066
+ if frame.has("line") and typeof(frame["line"]) == TYPE_INT:
1067
+ line_number = frame["line"]
1068
+ var location := ""
1069
+ if not script_path.is_empty():
1070
+ location = script_path
1071
+ if line_number >= 0:
1072
+ location += ":%d" % line_number
1073
+ else:
1074
+ location = String(frame.get("location", ""))
1075
+ if location.is_empty():
1076
+ location = "<unknown>"
1077
+ var function_name := ""
1078
+ if frame.has("function") and typeof(frame["function"]) == TYPE_STRING:
1079
+ function_name = frame["function"]
1080
+ if function_name.is_empty():
1081
+ function_name = "(anonymous)"
1082
+ lines.append("%d - %s - at function: %s" % [index_value, location, function_name])
1083
+ return lines