@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,1455 @@
1
+ @tool
2
+ class_name MCPDebuggerBridge
3
+ extends EditorDebuggerPlugin
4
+
5
+ # Signals for debugger events
6
+ signal breakpoint_hit(session_id: int, script_path: String, line: int, stack_info: Dictionary)
7
+ signal execution_paused(session_id: int, reason: String)
8
+ signal execution_resumed(session_id: int)
9
+ signal stack_frame_changed(session_id: int, frame_info: Dictionary)
10
+ signal breakpoint_set(session_id: int, script_path: String, line: int, success: bool)
11
+ signal breakpoint_removed(session_id: int, script_path: String, line: int, success: bool)
12
+
13
+ # Debugger state tracking
14
+ var _active_sessions: Dictionary = {}
15
+ var _breakpoints: Dictionary = {}
16
+ var _current_client_id: int = -1
17
+ var _websocket_server = null
18
+ var _session_breakpoints: Dictionary = {} # Track breakpoints per session
19
+ var _session_stack_cache: Dictionary = {}
20
+
21
+ # Constants for message throttling
22
+ const MAX_STACK_FRAMES: int = 50
23
+ const EVENT_THROTTLE_MS: int = 100
24
+ var _last_event_time: int = 0
25
+ const STACK_CAPTURE_NAMES := ["stack", "call_stack", "callstack", "stack_dump"]
26
+ const DEBUGGER_CAPTURE_NAMES := ["mcp_debugger", "breakpoint", "debugger"]
27
+
28
+ func _init():
29
+ _active_sessions.clear()
30
+ _breakpoints.clear()
31
+ _session_breakpoints.clear()
32
+ _session_stack_cache.clear()
33
+
34
+ func set_websocket_server(server):
35
+ _websocket_server = server
36
+
37
+ func set_client_id(client_id: int):
38
+ _current_client_id = client_id
39
+
40
+ func _normalize_capture_name(value: String) -> String:
41
+ return value.to_lower()
42
+
43
+ func _is_stack_capture_name(name: String) -> bool:
44
+ return name in STACK_CAPTURE_NAMES
45
+
46
+ func _is_debugger_capture_name(name: String) -> bool:
47
+ return name in DEBUGGER_CAPTURE_NAMES
48
+
49
+ func _has_capture(capture: String) -> bool:
50
+ # We handle multiple captures to integrate with Godot's debugger system
51
+ var capture_lc = _normalize_capture_name(capture)
52
+ return _is_debugger_capture_name(capture_lc) or _is_stack_capture_name(capture_lc)
53
+
54
+ func _build_stack_capture_payload(data: Array) -> Dictionary:
55
+ if not data.is_empty() and typeof(data[0]) == TYPE_DICTIONARY:
56
+ return data[0]
57
+ return {"stack": data}
58
+
59
+ func _capture(message: String, data: Array, session_id: int) -> bool:
60
+ var normalized_session_id = _normalize_session_id(session_id)
61
+ _trace("Debugger capture received: %s for session %s" % [message, normalized_session_id])
62
+
63
+ var message_lc = _normalize_capture_name(message)
64
+ if _is_stack_capture_name(message_lc):
65
+ var payload := _build_stack_capture_payload(data)
66
+ _handle_stack_dump(normalized_session_id, payload)
67
+ return true
68
+
69
+ # Handle different Godot debugger messages
70
+ match message_lc:
71
+ "breakpoint":
72
+ _handle_native_breakpoint(normalized_session_id, data)
73
+ "debugger":
74
+ _handle_debugger_message(normalized_session_id, data)
75
+ "mcp_debugger":
76
+ _handle_mcp_debugger_message(normalized_session_id, data)
77
+ _:
78
+ return false
79
+
80
+ return true
81
+
82
+ func _handle_native_breakpoint(session_id: int, data: Array) -> void:
83
+ # Handle native Godot breakpoint events
84
+ if data.size() > 0:
85
+ var breakpoint_data = data[0]
86
+ if typeof(breakpoint_data) == TYPE_DICTIONARY:
87
+ var script_path = breakpoint_data.get("script_path", "")
88
+ var line = breakpoint_data.get("line", -1)
89
+ var reason = breakpoint_data.get("reason", "breakpoint_hit")
90
+
91
+ _trace("Native breakpoint hit: %s:%d" % [script_path, line])
92
+
93
+ # Update session state
94
+ _ensure_session_state(session_id, true)
95
+ _active_sessions[session_id]["paused"] = true
96
+ _active_sessions[session_id]["current_script"] = script_path
97
+ _active_sessions[session_id]["current_line"] = line
98
+
99
+ # Get stack info
100
+ var stack_info = _get_session_stack_info(session_id)
101
+
102
+ # Send event to MCP client
103
+ _handle_breakpoint_hit(session_id, {
104
+ "script_path": script_path,
105
+ "line": line,
106
+ "stack_info": stack_info
107
+ })
108
+
109
+ func _handle_debugger_message(session_id: int, data: Array) -> void:
110
+ # Handle general debugger messages (pause, resume, etc.)
111
+ if data.size() > 0:
112
+ var message_data = data[0]
113
+ if typeof(message_data) == TYPE_DICTIONARY:
114
+ var event_type = message_data.get("type", "")
115
+ match event_type:
116
+ "paused":
117
+ _handle_execution_paused(session_id, message_data)
118
+ "resumed":
119
+ _handle_execution_resumed(session_id)
120
+ "stack_frame":
121
+ _handle_stack_frame_changed(session_id, message_data)
122
+ "stack_dump":
123
+ _handle_stack_dump(session_id, message_data)
124
+
125
+ func _handle_mcp_debugger_message(session_id: int, data: Array) -> void:
126
+ # Handle our custom MCP debugger messages
127
+ if data.is_empty():
128
+ return
129
+
130
+ var event_data = data[0]
131
+ if typeof(event_data) != TYPE_DICTIONARY:
132
+ return
133
+
134
+ var event_type = event_data.get("type", "")
135
+ match event_type:
136
+ "breakpoint_hit":
137
+ _handle_breakpoint_hit(session_id, event_data)
138
+ "execution_paused":
139
+ _handle_execution_paused(session_id, event_data)
140
+ "execution_resumed":
141
+ _handle_execution_resumed(session_id)
142
+ "stack_frame_changed":
143
+ _handle_stack_frame_changed(session_id, event_data)
144
+ _:
145
+ _trace("Unknown MCP debugger event type: %s" % event_type)
146
+
147
+ func _setup_session(session_id: int) -> void:
148
+ session_id = _normalize_session_id(session_id)
149
+ _trace("Setting up debugger session %s" % session_id)
150
+
151
+ var session = _get_session_instance(session_id)
152
+ if session:
153
+ # Diagnostic: Log available methods and properties
154
+ _diagnose_session_capabilities(session)
155
+
156
+ session.started.connect(_on_session_started.bind(session_id), CONNECT_DEFERRED)
157
+ session.stopped.connect(_on_session_stopped.bind(session_id), CONNECT_DEFERRED)
158
+ session.breaked.connect(_on_session_breaked.bind(session_id), CONNECT_DEFERRED)
159
+
160
+ if session.is_active():
161
+ _ensure_session_state(session_id, true)
162
+ # Restore any existing breakpoints for this session
163
+ _setup_session_breakpoints(session_id)
164
+
165
+ func _normalize_session_id(value) -> Variant:
166
+ var value_type := typeof(value)
167
+ if value_type == TYPE_INT:
168
+ return value
169
+ if value_type == TYPE_FLOAT:
170
+ return int(value)
171
+ if value_type == TYPE_STRING:
172
+ var text: String = value
173
+ if text.is_valid_int():
174
+ return int(text)
175
+ return value
176
+
177
+ # Session tracking helpers
178
+ func _is_empty_session_request(value) -> bool:
179
+ if value == null:
180
+ return true
181
+ var value_type := typeof(value)
182
+ match value_type:
183
+ TYPE_INT:
184
+ return value < 0
185
+ TYPE_FLOAT:
186
+ return int(value) < 0
187
+ TYPE_STRING:
188
+ return String(value).strip_edges().is_empty()
189
+ _:
190
+ return false
191
+
192
+ func _collect_session_candidate_ids(requested_session_id) -> Array:
193
+ var candidates: Array = []
194
+ var use_active_sessions := _is_empty_session_request(requested_session_id)
195
+
196
+ if not use_active_sessions:
197
+ candidates.append(requested_session_id)
198
+
199
+ if use_active_sessions:
200
+ var active_ids = _get_active_session_ids()
201
+ for session_id in active_ids:
202
+ if session_id not in candidates:
203
+ candidates.append(session_id)
204
+
205
+ return candidates
206
+
207
+ func _find_tracked_session_key(session_id) -> Variant:
208
+ if _active_sessions.has(session_id):
209
+ return session_id
210
+ var normalized_id = _normalize_session_id(session_id)
211
+ if _active_sessions.has(normalized_id):
212
+ return normalized_id
213
+ for key in _active_sessions.keys():
214
+ if _normalize_session_id(key) == normalized_id:
215
+ return key
216
+ return null
217
+
218
+ func _resolve_session_index(session_identifier) -> int:
219
+ var normalized = _normalize_session_id(session_identifier)
220
+
221
+ var sessions := get_sessions()
222
+ for i in range(sessions.size()):
223
+ var session = sessions[i]
224
+ if session == null:
225
+ continue
226
+
227
+ var reported_id = session.get_session_id() if session.has_method("get_session_id") else i
228
+ var reported_normalized = _normalize_session_id(reported_id)
229
+ if reported_normalized == normalized:
230
+ return i
231
+ if typeof(normalized) == TYPE_STRING:
232
+ var reported_text := str(reported_normalized)
233
+ if reported_text == normalized:
234
+ return i
235
+
236
+ if typeof(normalized) == TYPE_INT and normalized >= 0 and normalized < sessions.size():
237
+ return normalized
238
+
239
+ return -1
240
+
241
+ func _get_session_instance(session_identifier):
242
+ var session_index := _resolve_session_index(session_identifier)
243
+ if session_index < 0:
244
+ return null
245
+ return get_session(session_index)
246
+
247
+ func _move_dict_key(target: Dictionary, old_key, new_key) -> void:
248
+ if old_key == null or new_key == null or old_key == new_key:
249
+ return
250
+ if not target.has(old_key):
251
+ return
252
+ if target.has(new_key):
253
+ target.erase(old_key)
254
+ return
255
+ target[new_key] = target[old_key]
256
+ target.erase(old_key)
257
+
258
+ func _rekey_session_data(old_key, new_key) -> void:
259
+ if old_key == null or new_key == null or old_key == new_key:
260
+ return
261
+ _move_dict_key(_active_sessions, old_key, new_key)
262
+ _move_dict_key(_session_breakpoints, old_key, new_key)
263
+ _move_dict_key(_session_stack_cache, old_key, new_key)
264
+
265
+ func _sync_tracked_session(session_id) -> bool:
266
+ var normalized_id = _normalize_session_id(session_id)
267
+ if _active_sessions.has(normalized_id):
268
+ return true
269
+
270
+ var existing_key = _find_tracked_session_key(normalized_id)
271
+ if existing_key != null:
272
+ _rekey_session_data(existing_key, normalized_id)
273
+ return true
274
+
275
+ # Refresh session tracking in case the debugger state changed
276
+ _get_active_session_ids()
277
+ existing_key = _find_tracked_session_key(normalized_id)
278
+ if existing_key != null:
279
+ _rekey_session_data(existing_key, normalized_id)
280
+ return true
281
+
282
+ return false
283
+
284
+ func _ensure_session_state(session_id, mark_active: bool = false) -> void:
285
+ var normalized_id = _normalize_session_id(session_id)
286
+ var tracked_key = _find_tracked_session_key(normalized_id)
287
+
288
+ if tracked_key == null:
289
+ _active_sessions[normalized_id] = {
290
+ "active": mark_active,
291
+ "paused": false,
292
+ "current_script": "",
293
+ "current_line": -1,
294
+ "breakpoints": []
295
+ }
296
+ elif tracked_key != normalized_id:
297
+ _rekey_session_data(tracked_key, normalized_id)
298
+
299
+ if mark_active and _active_sessions.has(normalized_id):
300
+ _active_sessions[normalized_id]["active"] = true
301
+
302
+ func _track_local_breakpoint(script_path: String, line: int) -> void:
303
+ if not _breakpoints.has(script_path):
304
+ _breakpoints[script_path] = []
305
+
306
+ var script_breakpoints: Array = _breakpoints[script_path]
307
+ if line not in script_breakpoints:
308
+ script_breakpoints.append(line)
309
+
310
+ func _untrack_local_breakpoint(script_path: String, line: int) -> void:
311
+ if not _breakpoints.has(script_path):
312
+ return
313
+
314
+ var script_breakpoints: Array = _breakpoints[script_path]
315
+ script_breakpoints.erase(line)
316
+ if script_breakpoints.is_empty():
317
+ _breakpoints.erase(script_path)
318
+
319
+ func _ensure_session_breakpoint_storage(session_id) -> Dictionary:
320
+ var normalized_id = _normalize_session_id(session_id)
321
+ if not _session_breakpoints.has(normalized_id):
322
+ _session_breakpoints[normalized_id] = {}
323
+
324
+ return _session_breakpoints[normalized_id]
325
+
326
+ func _session_breakpoint_lines(session_id, script_path: String) -> Array:
327
+ var storage = _ensure_session_breakpoint_storage(session_id)
328
+ if not storage.has(script_path):
329
+ storage[script_path] = []
330
+
331
+ return storage[script_path]
332
+
333
+ func _get_primary_session_info() -> Dictionary:
334
+ var active_sessions = _get_active_session_ids()
335
+ if active_sessions.is_empty():
336
+ return {}
337
+
338
+ var session_id = active_sessions[0]
339
+ var session = _get_session_instance(session_id)
340
+ if not session or not session.is_active():
341
+ return {"error": "Debugger session not active"}
342
+
343
+ return {
344
+ "id": session_id,
345
+ "session": session
346
+ }
347
+
348
+ func _with_primary_session(error_message: String, action: Callable) -> Dictionary:
349
+ var info = _get_primary_session_info()
350
+ if info.is_empty():
351
+ return {"success": false, "message": error_message}
352
+
353
+ if info.has("error"):
354
+ return {"success": false, "message": info["error"]}
355
+
356
+ return action.call(info["id"], info["session"])
357
+
358
+ func _active_session_objects() -> Array:
359
+ var sessions: Array = []
360
+ for session_id in _get_active_session_ids():
361
+ var session = _get_session_instance(session_id)
362
+ if session and session.is_active():
363
+ sessions.append({
364
+ "id": session_id,
365
+ "session": session
366
+ })
367
+
368
+ return sessions
369
+
370
+ func _add_breakpoint_source(target: Dictionary, sources: Dictionary, source_name: String, breakpoint_map: Dictionary) -> void:
371
+ if breakpoint_map.is_empty():
372
+ return
373
+
374
+ _merge_breakpoint_map(target, breakpoint_map)
375
+ sources[source_name] = breakpoint_map
376
+
377
+ func _send_session_command(command: String, trace_label: String) -> Dictionary:
378
+ return _with_primary_session("No active debugger session", func(session_id: int, session):
379
+ if not session or not session.has_method("send_message"):
380
+ return {"success": false, "message": "Debugger session not active"}
381
+
382
+ session.send_message(command, [])
383
+ _trace(trace_label)
384
+ return {"success": true, "session_id": session_id}
385
+ )
386
+
387
+ # Breakpoint management
388
+ func set_breakpoint(script_path: String, line: int) -> Dictionary:
389
+ _track_local_breakpoint(script_path, line)
390
+
391
+ return _with_primary_session(
392
+ "No active debugger session. Start the project with debugging first.",
393
+ func(session_id: int, session):
394
+ var session_lines: Array = _session_breakpoint_lines(session_id, script_path)
395
+ if line not in session_lines:
396
+ session_lines.append(line)
397
+
398
+ var success = _set_native_breakpoint(session, script_path, line)
399
+ if success:
400
+ breakpoint_set.emit(session_id, script_path, line, true)
401
+ _trace("Breakpoint set successfully: %s:%d" % [script_path, line])
402
+
403
+ return {
404
+ "success": true,
405
+ "session_id": session_id,
406
+ "script_path": script_path,
407
+ "line": line
408
+ }
409
+
410
+ return {
411
+ "success": false,
412
+ "message": "Failed to set breakpoint in Godot's debugger"
413
+ }
414
+ )
415
+
416
+
417
+ func remove_breakpoint(script_path: String, line: int) -> Dictionary:
418
+ _untrack_local_breakpoint(script_path, line)
419
+
420
+ return _with_primary_session("No active debugger session", func(session_id: int, session):
421
+ if _session_breakpoints.has(session_id) and _session_breakpoints[session_id].has(script_path):
422
+ _session_breakpoints[session_id][script_path].erase(line)
423
+ if _session_breakpoints[session_id][script_path].is_empty():
424
+ _session_breakpoints[session_id].erase(script_path)
425
+
426
+ var success = _remove_native_breakpoint(session, script_path, line)
427
+ if success:
428
+ breakpoint_removed.emit(session_id, script_path, line, true)
429
+ _trace("Breakpoint removed successfully: %s:%d" % [script_path, line])
430
+
431
+ return {"success": true, "session_id": session_id}
432
+
433
+ return {
434
+ "success": false,
435
+ "message": "Failed to remove breakpoint in Godot's debugger"
436
+ }
437
+ )
438
+
439
+
440
+ func get_breakpoints() -> Dictionary:
441
+ var aggregated: Dictionary = {}
442
+ var sources: Dictionary = {}
443
+
444
+ _add_breakpoint_source(aggregated, sources, "mcp_tracked", _breakpoints.duplicate(true))
445
+ _add_breakpoint_source(aggregated, sources, "session_tracked", _collect_tracked_session_breakpoints())
446
+ _add_breakpoint_source(aggregated, sources, "session_reported", _collect_active_session_breakpoints())
447
+ _add_breakpoint_source(aggregated, sources, "editor", _collect_editor_breakpoints())
448
+
449
+ return {
450
+ "breakpoints": aggregated,
451
+ "sources": sources
452
+ }
453
+
454
+ func clear_all_breakpoints() -> Dictionary:
455
+ var old_breakpoints = _breakpoints.duplicate(true)
456
+ var old_session_breakpoints = _session_breakpoints.duplicate(true)
457
+
458
+ # Clear local tracking dictionaries
459
+ _breakpoints.clear()
460
+ _session_breakpoints.clear()
461
+
462
+ # Actually remove breakpoints from Godot's debugger system
463
+ var cleared_count = 0
464
+
465
+ for session_info in _active_session_objects():
466
+ var session_id: int = session_info["id"]
467
+ var session = session_info["session"]
468
+
469
+ # Clear breakpoints from this session using native methods
470
+ if old_session_breakpoints.has(session_id):
471
+ var session_bps = old_session_breakpoints[session_id]
472
+ for script_path in session_bps:
473
+ for line in session_bps[script_path]:
474
+ var success = _remove_native_breakpoint(session, script_path, line)
475
+ if success:
476
+ cleared_count += 1
477
+ _trace("Cleared breakpoint %s:%d from session %s" % [script_path, line, session_id])
478
+
479
+ # Also try to clear any remaining breakpoints using direct methods
480
+ _clear_all_native_breakpoints(session)
481
+
482
+ return {
483
+ "success": true,
484
+ "cleared_breakpoints": old_breakpoints,
485
+ "cleared_count": cleared_count
486
+ }
487
+
488
+ # Execution control
489
+ func pause_execution() -> Dictionary:
490
+ return _send_session_command("break", "Sent break command to debugger")
491
+
492
+ func resume_execution() -> Dictionary:
493
+ return _send_session_command("continue", "Sent continue command to debugger")
494
+
495
+ func step_over() -> Dictionary:
496
+ return _send_session_command("next", "Sent next (step over) command to debugger")
497
+
498
+ func step_into() -> Dictionary:
499
+ return _send_session_command("step", "Sent step (step into) command to debugger")
500
+
501
+
502
+ # Call stack and inspection
503
+ func get_call_stack(session_id = null, timeout_ms: int = 750) -> Dictionary:
504
+ var candidate_ids := _collect_session_candidate_ids(session_id)
505
+ if candidate_ids.is_empty():
506
+ return {
507
+ "error": "session_not_found",
508
+ "message": "No active debugger session available"
509
+ }
510
+
511
+ var last_error: Dictionary = {}
512
+ for candidate in candidate_ids:
513
+ var normalized_id = _normalize_session_id(candidate)
514
+ if not _sync_tracked_session(normalized_id):
515
+ if _resolve_session_index(normalized_id) < 0:
516
+ last_error = {
517
+ "error": "session_not_found",
518
+ "session_id": normalized_id
519
+ }
520
+ continue
521
+ _ensure_session_state(normalized_id, true)
522
+
523
+ var session = _get_session_instance(normalized_id)
524
+ if session == null or not session.is_active():
525
+ last_error = {
526
+ "error": "session_not_active",
527
+ "session_id": normalized_id
528
+ }
529
+ continue
530
+
531
+ var request_timestamp := Time.get_ticks_msec()
532
+ session.send_message("get_stack_dump", [])
533
+
534
+ var stack_info := await _wait_for_stack_dump(normalized_id, request_timestamp, timeout_ms)
535
+ if stack_info.has("error"):
536
+ last_error = stack_info
537
+ continue
538
+
539
+ return stack_info
540
+
541
+ if last_error.is_empty():
542
+ return {
543
+ "error": "session_not_found",
544
+ "message": "No debugger session responded"
545
+ }
546
+ return last_error
547
+
548
+ func get_cached_stack_info(session_id) -> Dictionary:
549
+ var normalized_id = _normalize_session_id(session_id)
550
+ if typeof(normalized_id) == TYPE_INT and normalized_id < 0:
551
+ return {"error": "invalid_session"}
552
+ if _session_stack_cache.has(normalized_id):
553
+ return _session_stack_cache[normalized_id].duplicate(true)
554
+ var stack_info := _update_session_stack_cache(normalized_id)
555
+ if stack_info.has("error"):
556
+ return stack_info
557
+ return stack_info.duplicate(true)
558
+
559
+ func get_current_state() -> Dictionary:
560
+ var active_sessions = _get_active_session_ids()
561
+ var diagnostics := _collect_session_diagnostics(active_sessions)
562
+ if active_sessions.is_empty():
563
+ return {
564
+ "active_sessions": [],
565
+ "total_breakpoints": 0,
566
+ "debugger_active": false,
567
+ "diagnostics": diagnostics
568
+ }
569
+
570
+ var session_id = active_sessions[0]
571
+ var state = _active_sessions.get(session_id, {})
572
+
573
+ return {
574
+ "active_sessions": active_sessions,
575
+ "current_session_id": session_id,
576
+ "total_breakpoints": _count_total_breakpoints(),
577
+ "debugger_active": true,
578
+ "paused": state.get("paused", false),
579
+ "current_script": state.get("current_script", ""),
580
+ "current_line": state.get("current_line", -1),
581
+ "breakpoints": _breakpoints.duplicate(true),
582
+ "diagnostics": diagnostics
583
+ }
584
+
585
+ # Private methods
586
+ # All handlers below assume session_id has already been normalized.
587
+ func _handle_breakpoint_hit(session_id: int, event_data: Dictionary) -> void:
588
+ var script_path = event_data.get("script_path", "")
589
+ var line = event_data.get("line", -1)
590
+ var stack_info = event_data.get("stack_info", {})
591
+ var sanitized_stack := _update_session_stack_cache(session_id, stack_info)
592
+
593
+ # Update session state
594
+ _ensure_session_state(session_id, true)
595
+ _active_sessions[session_id]["paused"] = true
596
+ _active_sessions[session_id]["current_script"] = script_path
597
+ _active_sessions[session_id]["current_line"] = line
598
+ _session_stack_cache[session_id] = sanitized_stack.duplicate(true)
599
+
600
+ # Throttle events if needed
601
+ if not _should_send_event():
602
+ return
603
+
604
+ # Send to MCP client
605
+ _send_debugger_event("breakpoint_hit", {
606
+ "session_id": session_id,
607
+ "script_path": script_path,
608
+ "line": line,
609
+ "stack_info": sanitized_stack
610
+ })
611
+
612
+ breakpoint_hit.emit(session_id, script_path, line, stack_info)
613
+
614
+ func _handle_execution_paused(session_id: int, event_data: Dictionary) -> void:
615
+ var reason = event_data.get("reason", "unknown")
616
+
617
+ _ensure_session_state(session_id, true)
618
+ _active_sessions[session_id]["paused"] = true
619
+ _update_session_stack_cache(session_id)
620
+
621
+ if not _should_send_event():
622
+ return
623
+
624
+ _send_debugger_event("execution_paused", {
625
+ "session_id": session_id,
626
+ "reason": reason
627
+ })
628
+
629
+ execution_paused.emit(session_id, reason)
630
+
631
+ func _handle_execution_resumed(session_id: int) -> void:
632
+ _ensure_session_state(session_id, true)
633
+ _active_sessions[session_id]["paused"] = false
634
+ _active_sessions[session_id]["current_script"] = ""
635
+ _active_sessions[session_id]["current_line"] = -1
636
+ _session_stack_cache.erase(session_id)
637
+
638
+ if not _should_send_event():
639
+ return
640
+
641
+ _send_debugger_event("execution_resumed", {
642
+ "session_id": session_id
643
+ })
644
+
645
+ execution_resumed.emit(session_id)
646
+
647
+ func _handle_stack_frame_changed(session_id: int, event_data: Dictionary) -> void:
648
+ var frame_info = event_data.get("frame_info", {})
649
+
650
+ if not _should_send_event():
651
+ return
652
+
653
+ _send_debugger_event("stack_frame_changed", {
654
+ "session_id": session_id,
655
+ "frame_info": frame_info
656
+ })
657
+
658
+ stack_frame_changed.emit(session_id, frame_info)
659
+
660
+ func _handle_stack_dump(session_id: int, event_data: Dictionary) -> void:
661
+ var raw_frames = event_data.get("stack", event_data.get("frames", event_data.get("dump", [])))
662
+ var structured_frames: Array = []
663
+
664
+ if raw_frames is Array:
665
+ for frame in raw_frames:
666
+ if typeof(frame) == TYPE_DICTIONARY:
667
+ structured_frames.append(frame.duplicate(true))
668
+ elif frame is Array and frame.size() >= 3:
669
+ var script_path = String(frame[0])
670
+ var line_number = int(frame[1])
671
+ var function_name = String(frame[2])
672
+ structured_frames.append({
673
+ "script": script_path,
674
+ "file": script_path,
675
+ "line": line_number,
676
+ "function": function_name
677
+ })
678
+ else:
679
+ structured_frames.append({"raw": frame})
680
+
681
+ var stack_info := {
682
+ "frames": structured_frames,
683
+ "total_frames": structured_frames.size(),
684
+ "current_frame": event_data.get("current_frame", 0)
685
+ }
686
+
687
+ if event_data.has("current_script"):
688
+ stack_info["current_script"] = event_data["current_script"]
689
+ if event_data.has("current_line"):
690
+ stack_info["current_line"] = event_data["current_line"]
691
+
692
+ _update_session_stack_cache(session_id, stack_info)
693
+
694
+ func _send_debugger_event(event_type: String, data: Dictionary) -> void:
695
+ if _websocket_server and _current_client_id >= 0:
696
+ var response = {
697
+ "status": "event",
698
+ "type": "debugger_event",
699
+ "event": event_type,
700
+ "data": data,
701
+ "timestamp": Time.get_ticks_msec()
702
+ }
703
+
704
+ _websocket_server.send_response(_current_client_id, response)
705
+ _trace("Sent debugger event: %s" % event_type)
706
+
707
+ func _should_send_event() -> bool:
708
+ var current_time = Time.get_ticks_msec()
709
+ if current_time - _last_event_time < EVENT_THROTTLE_MS:
710
+ return false
711
+
712
+ _last_event_time = current_time
713
+ return true
714
+
715
+ func _sanitize_stack_info(stack_info: Dictionary) -> Dictionary:
716
+ # Limit stack frames to prevent overwhelming the client
717
+ var frames = stack_info.get("frames", [])
718
+ if frames.size() > MAX_STACK_FRAMES:
719
+ stack_info["frames"] = frames.slice(0, MAX_STACK_FRAMES)
720
+ stack_info["truncated"] = true
721
+ stack_info["total_frames"] = frames.size()
722
+
723
+ return stack_info
724
+
725
+ func _update_session_stack_cache(session_id, stack_info = null) -> Dictionary:
726
+ session_id = _normalize_session_id(session_id)
727
+ var info: Dictionary
728
+ if typeof(stack_info) == TYPE_DICTIONARY:
729
+ info = stack_info.duplicate(true)
730
+ else:
731
+ info = _get_session_stack_info(session_id)
732
+ if typeof(info) != TYPE_DICTIONARY:
733
+ info = {}
734
+ if info.has("error"):
735
+ return info
736
+ var sanitized := _sanitize_stack_info(info)
737
+ sanitized["timestamp"] = Time.get_ticks_msec()
738
+ sanitized["session_id"] = session_id
739
+ _session_stack_cache[session_id] = sanitized.duplicate(true)
740
+ return sanitized
741
+
742
+ func _wait_for_stack_dump(session_id, request_timestamp: int, timeout_ms: int) -> Dictionary:
743
+ session_id = _normalize_session_id(session_id)
744
+ var cached := _duplicate_stack_cache(session_id)
745
+ if _stack_info_ready(cached, request_timestamp):
746
+ return cached
747
+
748
+ var loop := Engine.get_main_loop()
749
+ if loop == null or not (loop is SceneTree):
750
+ return {
751
+ "error": "stack_dump_unavailable",
752
+ "session_id": session_id,
753
+ "message": "Editor tree unavailable while waiting for stack dump."
754
+ }
755
+ var tree: SceneTree = loop
756
+
757
+ var deadline := Time.get_ticks_msec() + timeout_ms
758
+ while Time.get_ticks_msec() <= deadline:
759
+ await tree.process_frame
760
+ cached = _duplicate_stack_cache(session_id)
761
+ if _stack_info_ready(cached, request_timestamp):
762
+ return cached
763
+
764
+ if not cached.is_empty():
765
+ if cached.get("frames", []).is_empty():
766
+ var fallback := _build_frames_from_debug_output()
767
+ if not fallback.is_empty():
768
+ fallback["timestamp"] = Time.get_ticks_msec()
769
+ fallback["session_id"] = session_id
770
+ fallback["warning"] = "stack_dump_timeout"
771
+ _session_stack_cache[session_id] = fallback.duplicate(true)
772
+ return fallback
773
+ return cached
774
+
775
+ var fallback_result := _build_frames_from_debug_output()
776
+ if not fallback_result.is_empty():
777
+ fallback_result["timestamp"] = Time.get_ticks_msec()
778
+ fallback_result["session_id"] = session_id
779
+ fallback_result["warning"] = "stack_dump_timeout"
780
+ _session_stack_cache[session_id] = fallback_result.duplicate(true)
781
+ return fallback_result
782
+
783
+ return {
784
+ "error": "stack_dump_timeout",
785
+ "session_id": session_id,
786
+ "message": "Timed out waiting for debugger stack dump."
787
+ }
788
+
789
+ func _duplicate_stack_cache(session_id) -> Dictionary:
790
+ session_id = _normalize_session_id(session_id)
791
+ if not _session_stack_cache.has(session_id):
792
+ return {}
793
+ var cached = _session_stack_cache[session_id]
794
+ if typeof(cached) != TYPE_DICTIONARY:
795
+ return {}
796
+ return cached.duplicate(true)
797
+
798
+ func _stack_info_ready(info: Dictionary, request_timestamp: int) -> bool:
799
+ if info.is_empty():
800
+ return false
801
+ if info.has("error"):
802
+ return true
803
+ if not info.has("frames"):
804
+ return false
805
+ var frames = info.get("frames", [])
806
+ if typeof(frames) != TYPE_ARRAY:
807
+ return false
808
+ var info_timestamp := int(info.get("timestamp", 0))
809
+ if info_timestamp < request_timestamp:
810
+ return false
811
+ return true
812
+
813
+ func _count_total_breakpoints() -> int:
814
+ var count = 0
815
+ for script_breakpoints in _breakpoints.values():
816
+ count += script_breakpoints.size()
817
+ return count
818
+
819
+ func _collect_session_diagnostics(active_sessions: Array) -> Dictionary:
820
+ var diagnostics := {
821
+ "active_session_ids": active_sessions.duplicate(),
822
+ "tracked_sessions": _active_sessions.keys(),
823
+ "godot_session_objects": [],
824
+ "godot_session_count": 0
825
+ }
826
+
827
+ var sessions := get_sessions()
828
+ diagnostics["godot_session_count"] = sessions.size()
829
+
830
+ for i in range(sessions.size()):
831
+ var session = sessions[i]
832
+ if not session:
833
+ continue
834
+
835
+ var session_id := i
836
+ if session.has_method("get_session_id"):
837
+ session_id = session.get_session_id()
838
+
839
+ var session_info := {
840
+ "id": session_id,
841
+ "has_session": true,
842
+ "active": session.has_method("is_active") and session.is_active(),
843
+ "breaked": session.has_method("is_breaked") and session.is_breaked()
844
+ }
845
+ diagnostics["godot_session_objects"].append(session_info)
846
+ return diagnostics
847
+
848
+ func _collect_tracked_session_breakpoints() -> Dictionary:
849
+ var collected: Dictionary = {}
850
+
851
+ for session_id in _session_breakpoints.keys():
852
+ var session_breakpoints = _session_breakpoints[session_id]
853
+ if typeof(session_breakpoints) != TYPE_DICTIONARY:
854
+ continue
855
+ _merge_breakpoint_map(collected, session_breakpoints)
856
+
857
+ return collected
858
+
859
+ func _collect_active_session_breakpoints() -> Dictionary:
860
+ var collected: Dictionary = {}
861
+ var sessions := get_sessions()
862
+
863
+ for i in range(sessions.size()):
864
+ var session = sessions[i]
865
+ if not session or not session.has_method("is_active") or not session.is_active():
866
+ continue
867
+
868
+ var session_breakpoints = null
869
+
870
+ if session.has_method("get_breakpoints"):
871
+ session_breakpoints = session.get_breakpoints()
872
+ elif "breakpoints" in session and session.breakpoints and session.breakpoints.has_method("get_breakpoints"):
873
+ session_breakpoints = session.breakpoints.get_breakpoints()
874
+
875
+ if session_breakpoints != null:
876
+ var normalized := _convert_breakpoint_payload(session_breakpoints)
877
+ if not normalized.is_empty():
878
+ _merge_breakpoint_map(collected, normalized)
879
+
880
+ return collected
881
+
882
+ func _collect_editor_breakpoints() -> Dictionary:
883
+ if not Engine.has_singleton("EditorInterface"):
884
+ return {}
885
+
886
+ var editor_interface = Engine.get_singleton("EditorInterface")
887
+ if not editor_interface or not editor_interface.has_method("get_script_editor"):
888
+ return {}
889
+
890
+ var script_editor = editor_interface.get_script_editor()
891
+ if not script_editor or not script_editor.has_method("get_breakpoints"):
892
+ return {}
893
+
894
+ var editor_breakpoints = script_editor.get_breakpoints()
895
+ var normalized := _convert_breakpoint_payload(editor_breakpoints)
896
+
897
+ return normalized
898
+
899
+ func _convert_breakpoint_payload(payload) -> Dictionary:
900
+ var result: Dictionary = {}
901
+ if payload == null:
902
+ return result
903
+
904
+ match typeof(payload):
905
+ TYPE_DICTIONARY:
906
+ for script_path in payload.keys():
907
+ var line_values = payload[script_path]
908
+ var normalized_lines := _normalize_breakpoint_lines(line_values)
909
+ if normalized_lines.is_empty():
910
+ continue
911
+ result[script_path] = normalized_lines
912
+ TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY:
913
+ for entry in payload:
914
+ var parsed := _parse_breakpoint_entry(entry)
915
+ if parsed.is_empty():
916
+ continue
917
+ var script_path: String = parsed.get("script_path", "")
918
+ var line: int = parsed.get("line", -1)
919
+ if script_path.is_empty() or line < 0:
920
+ continue
921
+ if not result.has(script_path):
922
+ result[script_path] = []
923
+ var lines: Array = result[script_path]
924
+ if line not in lines:
925
+ lines.append(line)
926
+ lines.sort()
927
+ _:
928
+ if typeof(payload) == TYPE_STRING:
929
+ var parsed := _parse_breakpoint_entry(payload)
930
+ if parsed.is_empty():
931
+ return result
932
+ result[parsed["script_path"]] = [parsed["line"]]
933
+
934
+ return result
935
+
936
+ func _parse_breakpoint_entry(entry) -> Dictionary:
937
+ if typeof(entry) == TYPE_DICTIONARY:
938
+ var script_path := ""
939
+ if entry.has("script_path"):
940
+ script_path = str(entry["script_path"])
941
+ elif entry.has("source"):
942
+ script_path = str(entry["source"])
943
+ elif entry.has("script"):
944
+ script_path = str(entry["script"])
945
+
946
+ var line := -1
947
+ if entry.has("line"):
948
+ line = _parse_line_value(entry["line"])
949
+ elif entry.has("line_number"):
950
+ line = _parse_line_value(entry["line_number"])
951
+
952
+ if script_path.is_empty() or line < 0:
953
+ return {}
954
+
955
+ return {
956
+ "script_path": script_path,
957
+ "line": line
958
+ }
959
+
960
+ if typeof(entry) == TYPE_ARRAY:
961
+ if entry.size() >= 2:
962
+ var script_path_entry = entry[0]
963
+ var line_entry = entry[1]
964
+ var script_path_value := str(script_path_entry)
965
+ var line_value := _parse_line_value(line_entry)
966
+ if not script_path_value.is_empty() and line_value >= 0:
967
+ return {
968
+ "script_path": script_path_value,
969
+ "line": line_value
970
+ }
971
+ return {}
972
+
973
+ if typeof(entry) == TYPE_STRING:
974
+ return _parse_breakpoint_string(entry)
975
+
976
+ if typeof(entry) == TYPE_INT:
977
+ return {}
978
+
979
+ return {}
980
+
981
+ func _parse_breakpoint_string(entry: String) -> Dictionary:
982
+ var separator_index := entry.rfind(":")
983
+ if separator_index == -1:
984
+ return {}
985
+
986
+ var script_path := entry.substr(0, separator_index).strip_edges()
987
+ var line_str := entry.substr(separator_index + 1, entry.length()).strip_edges()
988
+
989
+ if script_path.is_empty() or not line_str.is_valid_int():
990
+ return {}
991
+
992
+ return {
993
+ "script_path": script_path,
994
+ "line": int(line_str)
995
+ }
996
+
997
+ func _parse_line_value(value) -> int:
998
+ if value == null:
999
+ return -1
1000
+
1001
+ match typeof(value):
1002
+ TYPE_INT:
1003
+ return value
1004
+ TYPE_FLOAT:
1005
+ return int(value)
1006
+ TYPE_STRING, TYPE_STRING_NAME:
1007
+ var text_value := str(value)
1008
+ if text_value.is_valid_int():
1009
+ return int(text_value)
1010
+ return -1
1011
+ _:
1012
+ var text_value := str(value)
1013
+ if text_value.is_valid_int():
1014
+ return int(text_value)
1015
+ return -1
1016
+
1017
+ func _normalize_breakpoint_lines(line_values) -> Array:
1018
+ var result: Array = []
1019
+
1020
+ if line_values == null:
1021
+ return result
1022
+
1023
+ match typeof(line_values):
1024
+ TYPE_ARRAY:
1025
+ for value in line_values:
1026
+ var parsed_line := _parse_line_value(value)
1027
+ if parsed_line >= 0 and parsed_line not in result:
1028
+ result.append(parsed_line)
1029
+ TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY, TYPE_PACKED_FLOAT32_ARRAY:
1030
+ for value in line_values:
1031
+ var parsed_line := _parse_line_value(value)
1032
+ if parsed_line >= 0 and parsed_line not in result:
1033
+ result.append(parsed_line)
1034
+ TYPE_INT:
1035
+ if line_values >= 0:
1036
+ result.append(line_values)
1037
+ TYPE_STRING, TYPE_STRING_NAME:
1038
+ var parsed_line := _parse_line_value(line_values)
1039
+ if parsed_line >= 0 and parsed_line not in result:
1040
+ result.append(parsed_line)
1041
+ _:
1042
+ if typeof(line_values) == TYPE_DICTIONARY:
1043
+ for key in line_values.keys():
1044
+ var parsed_line := _parse_line_value(line_values[key])
1045
+ if parsed_line >= 0 and parsed_line not in result:
1046
+ result.append(parsed_line)
1047
+
1048
+ result.sort()
1049
+ return result
1050
+
1051
+ func _merge_breakpoint_map(target: Dictionary, source: Dictionary) -> void:
1052
+ for script_path in source.keys():
1053
+ var source_lines = source[script_path]
1054
+ var normalized_lines := _normalize_breakpoint_lines(source_lines)
1055
+ if normalized_lines.is_empty():
1056
+ continue
1057
+
1058
+ if not target.has(script_path):
1059
+ target[script_path] = []
1060
+
1061
+ var target_lines: Array = target[script_path]
1062
+ for line in normalized_lines:
1063
+ if line not in target_lines:
1064
+ target_lines.append(line)
1065
+
1066
+ target_lines.sort()
1067
+ target[script_path] = target_lines
1068
+
1069
+ func _get_active_session_ids() -> Array:
1070
+ var result: Array = []
1071
+ var sessions := get_sessions()
1072
+
1073
+ for i in range(sessions.size()):
1074
+ var session = sessions[i]
1075
+ if not session:
1076
+ continue
1077
+
1078
+ var session_id_value := i
1079
+ if session.has_method("get_session_id"):
1080
+ session_id_value = session.get_session_id()
1081
+
1082
+ var session_id = _normalize_session_id(session_id_value)
1083
+
1084
+ var session_active: bool = session.has_method("is_active") and session.is_active()
1085
+ var session_breaked: bool = session.has_method("is_breaked") and session.is_breaked()
1086
+
1087
+ if session_active or session_breaked:
1088
+ _ensure_session_state(session_id, true)
1089
+ if session_breaked:
1090
+ _active_sessions[session_id]["paused"] = true
1091
+ if session_id not in result:
1092
+ result.append(session_id)
1093
+
1094
+ # Fallback to tracked sessions when Godot doesn't report them via get_sessions().
1095
+ for session_key in _active_sessions.keys():
1096
+ var session_id = _normalize_session_id(session_key)
1097
+ var state: Dictionary = _active_sessions[session_key]
1098
+ if state.get("active", false) or state.get("paused", false):
1099
+ if session_id not in result:
1100
+ result.append(session_id)
1101
+
1102
+ return result
1103
+
1104
+ func _on_session_started(session_id: int) -> void:
1105
+ session_id = _normalize_session_id(session_id)
1106
+ _ensure_session_state(session_id, true)
1107
+ _trace("Debugger session %s started" % session_id)
1108
+ # Restore any existing breakpoints for this session
1109
+ _setup_session_breakpoints(session_id)
1110
+
1111
+ func _on_session_stopped(session_id: int) -> void:
1112
+ session_id = _normalize_session_id(session_id)
1113
+ _ensure_session_state(session_id)
1114
+ _active_sessions[session_id]["active"] = false
1115
+ _active_sessions[session_id]["paused"] = false
1116
+ _session_stack_cache.erase(session_id)
1117
+ _trace("Debugger session %s stopped" % session_id)
1118
+
1119
+ func _on_session_breaked(can_debug: bool, session_id: int) -> void:
1120
+ session_id = _normalize_session_id(session_id)
1121
+ _ensure_session_state(session_id)
1122
+ _active_sessions[session_id]["paused"] = can_debug
1123
+ _trace("Debugger session %s breaked, can_debug=%s" % [session_id, can_debug])
1124
+
1125
+ # Native breakpoint integration
1126
+ func _set_native_breakpoint(session: EditorDebuggerSession, script_path: String, line: int) -> bool:
1127
+ # Try different approaches to set breakpoints in Godot's debugger system
1128
+
1129
+ # Method 1: Try to use the session's breakpoint methods directly
1130
+ if session and session.has_method("breakpoints"):
1131
+ var breakpoints = session.breakpoints
1132
+ if breakpoints.has_method("set_line_breakpoint"):
1133
+ # This might be the correct method for setting breakpoints
1134
+ breakpoints.set_line_breakpoint(script_path, line)
1135
+ _trace("Set breakpoint using session.breakpoints.set_line_breakpoint: %s:%d" % [script_path, line])
1136
+ return true
1137
+ elif breakpoints.has_method("add_breakpoint"):
1138
+ breakpoints.add_breakpoint(script_path, line)
1139
+ _trace("Set breakpoint using session.breakpoints.add_breakpoint: %s:%d" % [script_path, line])
1140
+ return true
1141
+
1142
+ # Method 2: Try direct session methods
1143
+ if session and session.has_method("set_breakpoint"):
1144
+ # Try different parameter formats for set_breakpoint
1145
+ # Based on error, Godot expects exactly 3 arguments with String first
1146
+ var attempts = [
1147
+ [script_path, line, true], # script_path, line, enabled (most likely correct)
1148
+ [script_path, line, 1], # script_path, line, enabled (as int)
1149
+ ]
1150
+
1151
+ for attempt in attempts:
1152
+ # Use callv to safely call with parameter array
1153
+ var result = session.callv("set_breakpoint", attempt)
1154
+ _trace("Set breakpoint using session.set_breakpoint with params %s: %s:%d" % [attempt, script_path, line])
1155
+ return true
1156
+
1157
+ # Method 3: Try standard Godot debugger protocol
1158
+ if session and session.has_method("send_message"):
1159
+ # Try different message formats that Godot might understand
1160
+ # Using the format from DEBUGGER_FIX_GUIDE.md
1161
+ var attempts = [
1162
+ ["breakpoint:insert", [{"script_path": script_path, "line": line, "enabled": true}]],
1163
+ ["breakpoint:insert", [{"source": script_path, "line": line, "enabled": true}]],
1164
+ ["breakpoint:set", [{"script": script_path, "line": line, "enabled": true}]],
1165
+ ["debugger:breakpoint", [{"script_path": script_path, "line": line, "enabled": true}]]
1166
+ ]
1167
+
1168
+ for attempt in attempts:
1169
+ session.send_message(attempt[0], attempt[1])
1170
+ _trace("Attempted breakpoint set with format %s: %s:%d" % [attempt[0], script_path, line])
1171
+
1172
+ return true
1173
+
1174
+ return false
1175
+
1176
+ func _clear_all_native_breakpoints(session: EditorDebuggerSession) -> void:
1177
+ _trace("Attempting to clear all native breakpoints from session")
1178
+
1179
+ # Method 1: Try to access and clear breakpoints through the session object
1180
+ if session and session.has_method("get_breakpoints"):
1181
+ var current_breakpoints = session.get_breakpoints()
1182
+ _trace("Current breakpoints from session before clear: %s" % current_breakpoints)
1183
+
1184
+ if current_breakpoints and current_breakpoints is Dictionary:
1185
+ # Try to clear using session methods
1186
+ if session.has_method("clear_breakpoints"):
1187
+ session.clear_breakpoints()
1188
+ _trace("Called session.clear_breakpoints()")
1189
+ elif session.get("breakpoints") and session.breakpoints.has_method("clear"):
1190
+ session.breakpoints.clear()
1191
+ _trace("Called session.breakpoints.clear()")
1192
+
1193
+ # Method 2: Try different clear message formats
1194
+ if session and session.has_method("send_message"):
1195
+ var clear_attempts = [
1196
+ ["breakpoint:clear_all", []],
1197
+ ["breakpoint:clear", []],
1198
+ ["debugger:clear_breakpoints", []],
1199
+ ["debugger:breakpoints_clear", []]
1200
+ ]
1201
+
1202
+ for attempt in clear_attempts:
1203
+ session.send_message(attempt[0], attempt[1])
1204
+ _trace("Sent clear breakpoints message: %s" % attempt[0])
1205
+
1206
+ func _remove_native_breakpoint(session: EditorDebuggerSession, script_path: String, line: int) -> bool:
1207
+ # Try different approaches to remove breakpoints in Godot's debugger system
1208
+ _trace("Attempting to remove breakpoint at %s:%d" % [script_path, line])
1209
+
1210
+ # Method 1: Try the direct message approach with "breakpoint:remove" first (most reliable)
1211
+ if session and session.has_method("send_message"):
1212
+ # Try different message formats that Godot might understand for removal
1213
+ var attempts = [
1214
+ ["breakpoint:remove", [{"source": script_path, "line": line}]],
1215
+ ["breakpoint:remove", [{"script_path": script_path, "line": line}]],
1216
+ ["breakpoint:remove", [{"script": script_path, "line": line}]],
1217
+ ["debugger:breakpoint_remove", [{"script_path": script_path, "line": line}]]
1218
+ ]
1219
+
1220
+ for attempt in attempts:
1221
+ session.send_message(attempt[0], attempt[1])
1222
+ _trace("Sent breakpoint removal message %s for %s:%d" % [attempt[0], script_path, line])
1223
+
1224
+ # Method 2: Try to use the session's breakpoint methods directly
1225
+ if session and session.has_method("set_breakpoint"):
1226
+ # Some versions of Godot might support setting breakpoints with enabled=false to remove them
1227
+ var attempts = [
1228
+ [script_path, line, false], # script_path, line, enabled=false
1229
+ [script_path, line, 0], # script_path, line, enabled=0
1230
+ ]
1231
+
1232
+ for attempt in attempts:
1233
+ var result = session.callv("set_breakpoint", attempt)
1234
+ _trace("Attempted removal using set_breakpoint with params %s: %s:%d" % [attempt, script_path, line])
1235
+
1236
+ # Method 3: Try using the breakpoints object if it exists
1237
+ if session and session.get("breakpoints"):
1238
+ var breakpoints = session.breakpoints
1239
+ _trace("Accessing session.breakpoints property: %s" % breakpoints)
1240
+
1241
+ if breakpoints:
1242
+ # Try to get current breakpoints and rebuild without the target line
1243
+ var current_breakpoints = session.get_breakpoints()
1244
+ _trace("Current breakpoints from session: %s" % current_breakpoints)
1245
+
1246
+ if current_breakpoints and current_breakpoints is Dictionary:
1247
+ if current_breakpoints.has(script_path):
1248
+ var script_bps = current_breakpoints[script_path]
1249
+ if script_bps is Array:
1250
+ # Remove the target line if it exists
1251
+ var new_bps = []
1252
+ var found_and_removed = false
1253
+ for bp_line in script_bps:
1254
+ if bp_line != line:
1255
+ new_bps.append(bp_line)
1256
+ else:
1257
+ found_and_removed = true
1258
+
1259
+ if found_and_removed:
1260
+ # Update the breakpoints array
1261
+ current_breakpoints[script_path] = new_bps
1262
+ _trace("Removed line %d from breakpoints for %s, new list: %s" % [line, script_path, new_bps])
1263
+
1264
+ # Try to clear and re-set all breakpoints
1265
+ if breakpoints.has_method("clear"):
1266
+ breakpoints.clear()
1267
+ _trace("Cleared all breakpoints, will re-add")
1268
+
1269
+ # Re-add all remaining breakpoints
1270
+ for sp in current_breakpoints:
1271
+ var bps = current_breakpoints[sp]
1272
+ if bps is Array:
1273
+ for bp_line in bps:
1274
+ _set_native_breakpoint(session, sp, bp_line)
1275
+ return true
1276
+
1277
+ # Method 4: Last resort - try to access EditorInterface and modify breakpoints directly
1278
+ if Engine.has_singleton("EditorInterface"):
1279
+ var editor_interface = Engine.get_singleton("EditorInterface")
1280
+ if editor_interface and editor_interface.has_method("get_script_editor"):
1281
+ var script_editor = editor_interface.get_script_editor()
1282
+ if script_editor and script_editor.has_method("get_current_script"):
1283
+ # Try to find and remove the breakpoint through the script editor
1284
+ _trace("Attempting to remove breakpoint through script editor interface")
1285
+ # This is a fallback method and may not work in all Godot versions
1286
+
1287
+ _trace("All breakpoint removal methods attempted for %s:%d" % [script_path, line])
1288
+ return true # Return true to indicate we attempted removal
1289
+
1290
+ func _get_session_stack_info(session_id) -> Dictionary:
1291
+ session_id = _normalize_session_id(session_id)
1292
+ var session = _get_session_instance(session_id)
1293
+ if not session or not session.is_active():
1294
+ return {"frames": [], "total_frames": 0}
1295
+
1296
+ # Try to get stack info from the session
1297
+ var stack_frames := []
1298
+
1299
+ # Method 1: Try to get stack via send_message and wait for response
1300
+ if session.has_method("send_message"):
1301
+ session.send_message("get_stack_dump", [])
1302
+
1303
+ # Note: This is async - we'll need to handle the response in the capture method
1304
+ # For now, return basic info
1305
+ var current_script = _active_sessions.get(session_id, {}).get("current_script", "")
1306
+ var current_line = _active_sessions.get(session_id, {}).get("current_line", -1)
1307
+
1308
+ if not current_script.is_empty() and current_line >= 0:
1309
+ var frame_info := {
1310
+ "script": current_script,
1311
+ "line": current_line,
1312
+ "function": "_process",
1313
+ "file": current_script
1314
+ }
1315
+ stack_frames.append(frame_info)
1316
+
1317
+ return {
1318
+ "frames": stack_frames,
1319
+ "total_frames": stack_frames.size(),
1320
+ "current_frame": 0
1321
+ }
1322
+
1323
+ func _setup_session_breakpoints(session_id) -> void:
1324
+ session_id = _normalize_session_id(session_id)
1325
+ # When a new session starts, set up all existing breakpoints for it
1326
+ if not _session_breakpoints.has(session_id):
1327
+ return
1328
+
1329
+ var session = _get_session_instance(session_id)
1330
+ if not session or not session.is_active():
1331
+ return
1332
+
1333
+ var session_bps = _session_breakpoints[session_id]
1334
+ for script_path in session_bps:
1335
+ for line in session_bps[script_path]:
1336
+ _set_native_breakpoint(session, script_path, line)
1337
+ _trace("Restored breakpoint %s:%d for session %s" % [script_path, line, session_id])
1338
+
1339
+ func _diagnose_session_capabilities(session: EditorDebuggerSession) -> void:
1340
+ _trace("=== EditorDebuggerSession Capabilities ===")
1341
+ _trace("Session type: %s" % session.get_class())
1342
+ _trace("Session active: %s" % session.is_active())
1343
+
1344
+ # List all available methods
1345
+ var methods = session.get_method_list()
1346
+ _trace("Available methods:")
1347
+ for method in methods:
1348
+ _trace(" - %s" % method.name)
1349
+
1350
+ # Check for breakpoint-related properties
1351
+ if session.has_method("get_property_list"):
1352
+ var props = session.get_property_list()
1353
+ _trace("Available properties:")
1354
+ for prop in props:
1355
+ if prop is Dictionary and prop.has("name"):
1356
+ var prop_name = prop["name"]
1357
+ if "breakpoint" in prop_name.to_lower() or "debug" in prop_name.to_lower():
1358
+ _trace(" - %s: %s" % [prop_name, prop.get("type", "unknown")])
1359
+
1360
+ # Try to access breakpoints property if it exists
1361
+ if "breakpoints" in session:
1362
+ var breakpoints = session.breakpoints
1363
+ _trace("Breakpoints property type: %s" % breakpoints.get_class())
1364
+ if breakpoints.has_method("get_method_list"):
1365
+ var bp_methods = breakpoints.get_method_list()
1366
+ _trace("Breakpoints methods:")
1367
+ for method in bp_methods:
1368
+ _trace(" - %s" % method.name)
1369
+
1370
+ _trace("=== End Capabilities ===")
1371
+
1372
+ func _trace(text: String) -> void:
1373
+ if OS.is_stdout_verbose():
1374
+ print("[MCPDebugger] %s" % text)
1375
+
1376
+ func _build_frames_from_debug_output() -> Dictionary:
1377
+ var frames := _parse_frames_from_debug_output()
1378
+ if frames.is_empty():
1379
+ return {}
1380
+ return {
1381
+ "frames": frames,
1382
+ "total_frames": frames.size(),
1383
+ "current_frame": 0,
1384
+ "source": "debug_output"
1385
+ }
1386
+
1387
+ func _parse_frames_from_debug_output() -> Array:
1388
+ var publisher = _get_debug_output_publisher()
1389
+ if publisher == null or not publisher.has_method("get_full_log_text"):
1390
+ return []
1391
+ var text = publisher.get_full_log_text()
1392
+ if text.is_empty():
1393
+ return []
1394
+ var lines = text.split("\n", false)
1395
+ var frame_lines: Array = []
1396
+ var collecting := false
1397
+ for i in range(lines.size() - 1, -1, -1):
1398
+ var line := String(lines[i]).strip_edges()
1399
+ if line.begins_with("Frame "):
1400
+ collecting = true
1401
+ frame_lines.push_front(line)
1402
+ elif collecting:
1403
+ break
1404
+ if frame_lines.is_empty():
1405
+ return []
1406
+ var frames := []
1407
+ for line in frame_lines:
1408
+ var frame_dict := _parse_print_stack_line(line)
1409
+ if not frame_dict.is_empty():
1410
+ frames.append(frame_dict)
1411
+ return frames
1412
+
1413
+ func _parse_print_stack_line(line: String) -> Dictionary:
1414
+ var trimmed := line.strip_edges()
1415
+ if not trimmed.begins_with("Frame "):
1416
+ return {}
1417
+ var dash_index := trimmed.find(" - ")
1418
+ if dash_index == -1:
1419
+ return {}
1420
+ var index_text := trimmed.substr(6, dash_index - 6).strip_edges()
1421
+ var index_value := 0
1422
+ if index_text.is_valid_int():
1423
+ index_value = int(index_text)
1424
+ var rest := trimmed.substr(dash_index + 3).strip_edges()
1425
+ var func_name := ""
1426
+ var location := rest
1427
+ var at_index := rest.find("@")
1428
+ if at_index != -1:
1429
+ location = rest.substr(0, at_index).strip_edges()
1430
+ func_name = rest.substr(at_index + 1).strip_edges()
1431
+ if func_name.ends_with("()"):
1432
+ func_name = func_name.substr(0, func_name.length() - 2)
1433
+ var script_path := ""
1434
+ var line_number := -1
1435
+ var colon_index := location.rfind(":")
1436
+ if colon_index != -1:
1437
+ var line_text := location.substr(colon_index + 1).strip_edges()
1438
+ if line_text.is_valid_int():
1439
+ line_number = int(line_text)
1440
+ script_path = location.substr(0, colon_index).strip_edges()
1441
+ else:
1442
+ script_path = location
1443
+ return {
1444
+ "index": index_value,
1445
+ "function": func_name,
1446
+ "script": script_path,
1447
+ "file": script_path,
1448
+ "line": line_number,
1449
+ "location": location
1450
+ }
1451
+
1452
+ func _get_debug_output_publisher():
1453
+ if Engine.has_meta("MCPDebugOutputPublisher"):
1454
+ return Engine.get_meta("MCPDebugOutputPublisher")
1455
+ return null