@satelliteoflove/godot-mcp 2.16.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/addon/command_router.gd +1 -0
  2. package/addon/commands/debug_commands.gd +0 -36
  3. package/addon/commands/profiler_commands.gd +143 -0
  4. package/addon/core/mcp_debugger_plugin.gd +52 -0
  5. package/addon/game_bridge/mcp_frame_profiler.gd +61 -0
  6. package/addon/game_bridge/mcp_game_bridge.gd +170 -0
  7. package/addon/plugin.cfg +1 -1
  8. package/addon/websocket_server.gd +6 -78
  9. package/dist/__tests__/tools/profiler-actions.test.d.ts +2 -0
  10. package/dist/__tests__/tools/profiler-actions.test.d.ts.map +1 -0
  11. package/dist/__tests__/tools/profiler-actions.test.js +95 -0
  12. package/dist/__tests__/tools/profiler-actions.test.js.map +1 -0
  13. package/dist/__tests__/tools/profiler.test.d.ts +2 -0
  14. package/dist/__tests__/tools/profiler.test.d.ts.map +1 -0
  15. package/dist/__tests__/tools/profiler.test.js +163 -0
  16. package/dist/__tests__/tools/profiler.test.js.map +1 -0
  17. package/dist/connection/websocket.d.ts +1 -1
  18. package/dist/connection/websocket.d.ts.map +1 -1
  19. package/dist/connection/websocket.js +14 -0
  20. package/dist/connection/websocket.js.map +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +18 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/tools/editor.d.ts.map +1 -1
  25. package/dist/tools/editor.js +2 -1
  26. package/dist/tools/editor.js.map +1 -1
  27. package/dist/tools/index.d.ts +1 -0
  28. package/dist/tools/index.d.ts.map +1 -1
  29. package/dist/tools/index.js +3 -0
  30. package/dist/tools/index.js.map +1 -1
  31. package/dist/tools/profiler.d.ts +54 -0
  32. package/dist/tools/profiler.d.ts.map +1 -0
  33. package/dist/tools/profiler.js +204 -0
  34. package/dist/tools/profiler.js.map +1 -0
  35. package/package.json +1 -1
@@ -20,6 +20,7 @@ func setup(plugin: EditorPlugin) -> void:
20
20
  _register_handler(MCPResourceCommands.new(), plugin)
21
21
  _register_handler(MCPScene3DCommands.new(), plugin)
22
22
  _register_handler(MCPInputCommands.new(), plugin)
23
+ _register_handler(MCPProfilerCommands.new(), plugin)
23
24
 
24
25
 
25
26
  func _register_handler(handler: MCPBaseCommand, plugin: EditorPlugin) -> void:
@@ -3,21 +3,16 @@ extends MCPBaseCommand
3
3
  class_name MCPDebugCommands
4
4
 
5
5
  const DEBUG_OUTPUT_TIMEOUT := 5.0
6
- const PERFORMANCE_METRICS_TIMEOUT := 5.0
7
6
 
8
7
  var _debug_output_result: PackedStringArray = []
9
8
  var _debug_output_pending: bool = false
10
9
 
11
- var _performance_metrics_result: Dictionary = {}
12
- var _performance_metrics_pending: bool = false
13
-
14
10
 
15
11
  func get_commands() -> Dictionary:
16
12
  return {
17
13
  "run_project": run_project,
18
14
  "stop_project": stop_project,
19
15
  "get_debug_output": get_debug_output,
20
- "get_performance_metrics": get_performance_metrics,
21
16
  "get_log_messages": get_log_messages,
22
17
  "get_errors": get_errors,
23
18
  "get_stack_trace": get_stack_trace,
@@ -100,37 +95,6 @@ func _on_debug_output_received(output: PackedStringArray) -> void:
100
95
  _debug_output_result = output
101
96
 
102
97
 
103
- func get_performance_metrics(_params: Dictionary) -> Dictionary:
104
- if not EditorInterface.is_playing_scene():
105
- return _error("NOT_RUNNING", "No game is currently running")
106
-
107
- var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
108
- if debugger_plugin == null or not debugger_plugin.has_active_session():
109
- return _error("NO_SESSION", "No active debug session")
110
-
111
- _performance_metrics_pending = true
112
- _performance_metrics_result = {}
113
-
114
- debugger_plugin.performance_metrics_received.connect(_on_performance_metrics_received, CONNECT_ONE_SHOT)
115
- debugger_plugin.request_performance_metrics()
116
-
117
- var start_time := Time.get_ticks_msec()
118
- while _performance_metrics_pending:
119
- await Engine.get_main_loop().process_frame
120
- if (Time.get_ticks_msec() - start_time) / 1000.0 > PERFORMANCE_METRICS_TIMEOUT:
121
- _performance_metrics_pending = false
122
- if debugger_plugin.performance_metrics_received.is_connected(_on_performance_metrics_received):
123
- debugger_plugin.performance_metrics_received.disconnect(_on_performance_metrics_received)
124
- return _error("TIMEOUT", "Timed out waiting for performance metrics")
125
-
126
- return _success(_performance_metrics_result)
127
-
128
-
129
- func _on_performance_metrics_received(metrics: Dictionary) -> void:
130
- _performance_metrics_pending = false
131
- _performance_metrics_result = metrics
132
-
133
-
134
98
  func get_log_messages(params: Dictionary) -> Dictionary:
135
99
  var clear: bool = params.get("clear", false)
136
100
  var limit: int = params.get("limit", 50)
@@ -0,0 +1,143 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPProfilerCommands
4
+
5
+ const PROFILER_TIMEOUT := 5.0
6
+ const GENERIC_TIMEOUT := 5.0
7
+
8
+ var _performance_metrics_pending: bool = false
9
+ var _performance_metrics_result: Dictionary = {}
10
+
11
+
12
+ func get_commands() -> Dictionary:
13
+ return {
14
+ "get_performance_metrics": get_performance_metrics,
15
+ "start_profiler": start_profiler,
16
+ "stop_profiler": stop_profiler,
17
+ "get_profiler_data": get_profiler_data,
18
+ "get_active_processes": get_active_processes,
19
+ "get_signal_connections": get_signal_connections,
20
+ }
21
+
22
+
23
+ func get_performance_metrics(_params: Dictionary) -> Dictionary:
24
+ if not EditorInterface.is_playing_scene():
25
+ return _error("NOT_RUNNING", "No game is currently running")
26
+
27
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
28
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
29
+ return _error("NO_SESSION", "No active debug session")
30
+
31
+ _performance_metrics_pending = true
32
+ _performance_metrics_result = {}
33
+
34
+ debugger_plugin.performance_metrics_received.connect(_on_performance_metrics_received, CONNECT_ONE_SHOT)
35
+ debugger_plugin.request_performance_metrics()
36
+
37
+ var start_time := Time.get_ticks_msec()
38
+ while _performance_metrics_pending:
39
+ await Engine.get_main_loop().process_frame
40
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > PROFILER_TIMEOUT:
41
+ _performance_metrics_pending = false
42
+ if debugger_plugin.performance_metrics_received.is_connected(_on_performance_metrics_received):
43
+ debugger_plugin.performance_metrics_received.disconnect(_on_performance_metrics_received)
44
+ return _error("TIMEOUT", "Timed out waiting for performance metrics")
45
+
46
+ return _success(_performance_metrics_result)
47
+
48
+
49
+ func _on_performance_metrics_received(metrics: Dictionary) -> void:
50
+ _performance_metrics_pending = false
51
+ _performance_metrics_result = metrics
52
+
53
+
54
+ func start_profiler(_params: Dictionary) -> Dictionary:
55
+ if not EditorInterface.is_playing_scene():
56
+ return _error("NOT_RUNNING", "No game is currently running")
57
+
58
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
59
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
60
+ return _error("NO_SESSION", "No active debug session")
61
+
62
+ debugger_plugin.toggle_frame_profiler(true)
63
+ return _success({"message": "Frame profiler started"})
64
+
65
+
66
+ func stop_profiler(_params: Dictionary) -> Dictionary:
67
+ if not EditorInterface.is_playing_scene():
68
+ return _error("NOT_RUNNING", "No game is currently running")
69
+
70
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
71
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
72
+ return _error("NO_SESSION", "No active debug session")
73
+
74
+ debugger_plugin.toggle_frame_profiler(false)
75
+ return _success({"message": "Frame profiler stopped"})
76
+
77
+
78
+ func get_profiler_data(_params: Dictionary) -> Dictionary:
79
+ var result = await _send_and_wait("get_profiler_data")
80
+ if result == null:
81
+ return _last_error
82
+ var result_dict: Dictionary
83
+ if result is Dictionary:
84
+ result_dict = result
85
+ else:
86
+ result_dict = {"data": result}
87
+ return _success(result_dict)
88
+
89
+
90
+ func get_active_processes(_params: Dictionary) -> Dictionary:
91
+ var result = await _send_and_wait("get_active_processes")
92
+ if result == null:
93
+ return _last_error
94
+ var result_dict: Dictionary
95
+ if result is Dictionary:
96
+ result_dict = result
97
+ else:
98
+ result_dict = {"data": result}
99
+ return _success(result_dict)
100
+
101
+
102
+ func get_signal_connections(params: Dictionary) -> Dictionary:
103
+ var node_path: String = params.get("node_path", "")
104
+ var result = await _send_and_wait("get_signal_connections", [node_path])
105
+ if result == null:
106
+ return _last_error
107
+ var result_dict: Dictionary
108
+ if result is Dictionary:
109
+ result_dict = result
110
+ else:
111
+ result_dict = {"data": result}
112
+ return _success(result_dict)
113
+
114
+
115
+ var _last_error: Dictionary = {}
116
+
117
+
118
+ func _send_and_wait(msg_type: String, args: Array = []):
119
+ if not EditorInterface.is_playing_scene():
120
+ _last_error = _error("NOT_RUNNING", "No game is currently running")
121
+ return null
122
+
123
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
124
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
125
+ _last_error = _error("NO_SESSION", "No active debug session")
126
+ return null
127
+
128
+ var sent: bool = debugger_plugin.send_game_message(msg_type, args)
129
+ if not sent:
130
+ _last_error = _error("SEND_FAILED", "Failed to send message to game")
131
+ return null
132
+
133
+ var start_time := Time.get_ticks_msec()
134
+ while not debugger_plugin.has_response(msg_type):
135
+ await Engine.get_main_loop().process_frame
136
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > GENERIC_TIMEOUT:
137
+ debugger_plugin.clear_response(msg_type)
138
+ _last_error = _error("TIMEOUT", "Timed out waiting for %s response" % msg_type)
139
+ return null
140
+
141
+ var response = debugger_plugin.get_response(msg_type)
142
+ debugger_plugin.clear_response(msg_type)
143
+ return response
@@ -9,6 +9,7 @@ signal find_nodes_received(matches: Array, count: int, error: String)
9
9
  signal input_map_received(actions: Array, error: String)
10
10
  signal input_sequence_completed(result: Dictionary)
11
11
  signal type_text_completed(result: Dictionary)
12
+ signal game_response(message_type: String, data: Variant)
12
13
 
13
14
  var _active_session_id: int = -1
14
15
  var _pending_screenshot: bool = false
@@ -18,6 +19,8 @@ var _pending_find_nodes: bool = false
18
19
  var _pending_input_map: bool = false
19
20
  var _pending_input_sequence: bool = false
20
21
  var _pending_type_text: bool = false
22
+ var _pending_requests: Dictionary = {}
23
+ var _responses: Dictionary = {}
21
24
 
22
25
 
23
26
  func _has_capture(prefix: String) -> bool:
@@ -47,6 +50,9 @@ func _capture(message: String, data: Array, session_id: int) -> bool:
47
50
  "godot_mcp:type_text_result":
48
51
  _handle_type_text_result(data)
49
52
  return true
53
+ "godot_mcp:game_response":
54
+ _handle_game_response(data)
55
+ return true
50
56
  return false
51
57
 
52
58
 
@@ -77,6 +83,9 @@ func _session_stopped() -> void:
77
83
  if _pending_type_text:
78
84
  _pending_type_text = false
79
85
  type_text_completed.emit({"error": "Game session ended"})
86
+ for msg_type in _pending_requests:
87
+ _responses[msg_type] = {}
88
+ _pending_requests.clear()
80
89
 
81
90
 
82
91
  func has_active_session() -> bool:
@@ -229,3 +238,46 @@ func _handle_type_text_result(data: Array) -> void:
229
238
  _pending_type_text = false
230
239
  var result: Dictionary = data[0] if data.size() > 0 else {}
231
240
  type_text_completed.emit(result)
241
+
242
+
243
+ func send_game_message(msg_type: String, args: Array = []) -> bool:
244
+ if _active_session_id < 0:
245
+ return false
246
+ var session := get_session(_active_session_id)
247
+ if not session:
248
+ return false
249
+ _pending_requests[msg_type] = true
250
+ _responses.erase(msg_type)
251
+ session.send_message("godot_mcp:" + msg_type, args)
252
+ return true
253
+
254
+
255
+ func has_response(msg_type: String) -> bool:
256
+ return _responses.has(msg_type)
257
+
258
+
259
+ func get_response(msg_type: String) -> Variant:
260
+ return _responses.get(msg_type)
261
+
262
+
263
+ func clear_response(msg_type: String) -> void:
264
+ _responses.erase(msg_type)
265
+ _pending_requests.erase(msg_type)
266
+
267
+
268
+ func _handle_game_response(data: Array) -> void:
269
+ if data.size() < 2:
270
+ return
271
+ var msg_type: String = data[0]
272
+ var response_data: Variant = data[1]
273
+ _pending_requests.erase(msg_type)
274
+ _responses[msg_type] = response_data
275
+ game_response.emit(msg_type, response_data)
276
+
277
+
278
+ func toggle_frame_profiler(enable: bool) -> void:
279
+ if _active_session_id < 0:
280
+ return
281
+ var session := get_session(_active_session_id)
282
+ if session:
283
+ session.toggle_profiler("mcp_frame_profiler", enable)
@@ -0,0 +1,61 @@
1
+ extends EngineProfiler
2
+ class_name MCPFrameProfiler
3
+
4
+ const MAX_FRAMES := 300
5
+ const MONITOR_SAMPLE_INTERVAL := 10
6
+
7
+ var _active := false
8
+ var _buffer: Array[Dictionary] = []
9
+ var _frame_index := 0
10
+
11
+
12
+ func _toggle(enable: bool, _options: Array) -> void:
13
+ _active = enable
14
+ if enable:
15
+ _buffer.clear()
16
+ _frame_index = 0
17
+
18
+
19
+ func _tick(frame_time: float, process_time: float, physics_time: float, physics_frame_time: float) -> void:
20
+ if not _active:
21
+ return
22
+
23
+ var entry := {
24
+ "ft": frame_time,
25
+ "pt": process_time,
26
+ "pht": physics_time,
27
+ "pft": physics_frame_time,
28
+ "i": _frame_index,
29
+ }
30
+
31
+ if _frame_index % MONITOR_SAMPLE_INTERVAL == 0:
32
+ entry["m"] = _snapshot_monitors()
33
+
34
+ _buffer.append(entry)
35
+ if _buffer.size() > MAX_FRAMES:
36
+ _buffer.pop_front()
37
+
38
+ _frame_index += 1
39
+
40
+
41
+ func get_buffer_data() -> Dictionary:
42
+ return {
43
+ "active": _active,
44
+ "frame_count": _buffer.size(),
45
+ "total_frames_collected": _frame_index,
46
+ "max_fps": Engine.max_fps,
47
+ "frames": _buffer.duplicate(),
48
+ }
49
+
50
+
51
+ func _snapshot_monitors() -> Dictionary:
52
+ return {
53
+ "fps": Performance.get_monitor(Performance.TIME_FPS),
54
+ "obj_count": int(Performance.get_monitor(Performance.OBJECT_COUNT)),
55
+ "node_count": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
56
+ "orphan_nodes": int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)),
57
+ "mem_static": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
58
+ "render_objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
59
+ "render_draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
60
+ "render_primitives": int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
61
+ }
@@ -4,6 +4,7 @@ class_name MCPGameBridge
4
4
  const DEFAULT_MAX_WIDTH := 1920
5
5
 
6
6
  var _logger: _MCPGameLogger
7
+ var _profiler: MCPFrameProfiler
7
8
 
8
9
 
9
10
  func _ready() -> void:
@@ -11,6 +12,8 @@ func _ready() -> void:
11
12
  return
12
13
  _logger = _MCPGameLogger.new()
13
14
  OS.add_logger(_logger)
15
+ _profiler = MCPFrameProfiler.new()
16
+ EngineDebugger.register_profiler("mcp_frame_profiler", _profiler)
14
17
  EngineDebugger.register_message_capture("godot_mcp", _on_debugger_message)
15
18
  MCPLog.info("Game bridge initialized")
16
19
 
@@ -18,6 +21,8 @@ func _ready() -> void:
18
21
  func _exit_tree() -> void:
19
22
  if EngineDebugger.is_active():
20
23
  EngineDebugger.unregister_message_capture("godot_mcp")
24
+ if _profiler:
25
+ EngineDebugger.unregister_profiler("mcp_frame_profiler")
21
26
 
22
27
 
23
28
  func _process(_delta: float) -> void:
@@ -75,6 +80,15 @@ func _on_debugger_message(message: String, data: Array) -> bool:
75
80
  "type_text":
76
81
  _handle_type_text(data)
77
82
  return true
83
+ "get_profiler_data":
84
+ _handle_get_profiler_data()
85
+ return true
86
+ "get_active_processes":
87
+ _handle_get_active_processes()
88
+ return true
89
+ "get_signal_connections":
90
+ _handle_get_signal_connections(data)
91
+ return true
78
92
  return false
79
93
 
80
94
 
@@ -191,19 +205,175 @@ func _handle_get_performance_metrics() -> void:
191
205
  "render_objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
192
206
  "render_draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
193
207
  "render_primitives": int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
208
+ "render_video_mem": int(Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED)),
209
+ "render_texture_mem": int(Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED)),
210
+ "render_buffer_mem": int(Performance.get_monitor(Performance.RENDER_BUFFER_MEM_USED)),
194
211
  "physics_2d_active_objects": int(Performance.get_monitor(Performance.PHYSICS_2D_ACTIVE_OBJECTS)),
195
212
  "physics_2d_collision_pairs": int(Performance.get_monitor(Performance.PHYSICS_2D_COLLISION_PAIRS)),
196
213
  "physics_2d_island_count": int(Performance.get_monitor(Performance.PHYSICS_2D_ISLAND_COUNT)),
214
+ "physics_3d_active_objects": int(Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS)),
215
+ "physics_3d_collision_pairs": int(Performance.get_monitor(Performance.PHYSICS_3D_COLLISION_PAIRS)),
216
+ "physics_3d_island_count": int(Performance.get_monitor(Performance.PHYSICS_3D_ISLAND_COUNT)),
217
+ "audio_output_latency": Performance.get_monitor(Performance.AUDIO_OUTPUT_LATENCY),
197
218
  "object_count": int(Performance.get_monitor(Performance.OBJECT_COUNT)),
198
219
  "object_resource_count": int(Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT)),
199
220
  "object_node_count": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
200
221
  "object_orphan_node_count": int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)),
201
222
  "memory_static": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
202
223
  "memory_static_max": int(Performance.get_monitor(Performance.MEMORY_STATIC_MAX)),
224
+ "memory_msg_buffer_max": int(Performance.get_monitor(Performance.MEMORY_MESSAGE_BUFFER_MAX)),
225
+ "navigation_active_maps": int(Performance.get_monitor(Performance.NAVIGATION_ACTIVE_MAPS)),
226
+ "navigation_region_count": int(Performance.get_monitor(Performance.NAVIGATION_REGION_COUNT)),
227
+ "navigation_agent_count": int(Performance.get_monitor(Performance.NAVIGATION_AGENT_COUNT)),
228
+ "navigation_link_count": int(Performance.get_monitor(Performance.NAVIGATION_LINK_COUNT)),
229
+ "navigation_polygon_count": int(Performance.get_monitor(Performance.NAVIGATION_POLYGON_COUNT)),
230
+ "navigation_edge_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_COUNT)),
231
+ "navigation_edge_merge_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_MERGE_COUNT)),
232
+ "navigation_edge_connection_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_CONNECTION_COUNT)),
233
+ "navigation_edge_free_count": int(Performance.get_monitor(Performance.NAVIGATION_EDGE_FREE_COUNT)),
234
+ "navigation_obstacle_count": int(Performance.get_monitor(Performance.NAVIGATION_OBSTACLE_COUNT)),
235
+ "pipeline_compilations_canvas": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_CANVAS)),
236
+ "pipeline_compilations_mesh": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_MESH)),
237
+ "pipeline_compilations_surface": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_SURFACE)),
238
+ "pipeline_compilations_draw": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_DRAW)),
239
+ "pipeline_compilations_specialization": int(Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_SPECIALIZATION)),
203
240
  }
241
+
242
+ var rid := get_viewport().get_viewport_rid()
243
+ metrics["viewport_render_cpu_ms"] = RenderingServer.viewport_get_measured_render_time_cpu(rid) + RenderingServer.viewport_get_measured_render_time_gpu(rid)
244
+ metrics["viewport_render_gpu_ms"] = RenderingServer.viewport_get_measured_render_time_gpu(rid)
245
+
204
246
  EngineDebugger.send_message("godot_mcp:performance_metrics_result", [metrics])
205
247
 
206
248
 
249
+ func _handle_get_profiler_data() -> void:
250
+ var data := _profiler.get_buffer_data() if _profiler else {}
251
+ EngineDebugger.send_message("godot_mcp:game_response", ["get_profiler_data", data])
252
+
253
+
254
+ func _handle_get_active_processes() -> void:
255
+ var tree := get_tree()
256
+ var scene_root := tree.current_scene if tree else null
257
+ if not scene_root:
258
+ EngineDebugger.send_message("godot_mcp:game_response", ["get_active_processes", {"processes": []}])
259
+ return
260
+
261
+ var script_map: Dictionary = {}
262
+ _collect_processes(scene_root, scene_root, script_map)
263
+
264
+ var processes: Array = []
265
+ for script_path in script_map:
266
+ processes.append(script_map[script_path])
267
+
268
+ processes.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
269
+ return a.instance_count > b.instance_count
270
+ )
271
+
272
+ EngineDebugger.send_message("godot_mcp:game_response", ["get_active_processes", {"processes": processes}])
273
+
274
+
275
+ func _collect_processes(node: Node, scene_root: Node, script_map: Dictionary) -> void:
276
+ var is_proc := node.is_processing()
277
+ var is_phys := node.is_physics_processing()
278
+
279
+ if is_proc or is_phys:
280
+ var script_path := ""
281
+ var script := node.get_script()
282
+ if script and script is Script:
283
+ script_path = script.resource_path
284
+ if script_path.is_empty():
285
+ script_path = node.get_class()
286
+
287
+ if not script_map.has(script_path):
288
+ script_map[script_path] = {
289
+ "script_path": script_path,
290
+ "has_process": false,
291
+ "has_physics_process": false,
292
+ "instance_count": 0,
293
+ "example_paths": [],
294
+ }
295
+
296
+ var entry: Dictionary = script_map[script_path]
297
+ if is_proc:
298
+ entry.has_process = true
299
+ if is_phys:
300
+ entry.has_physics_process = true
301
+ entry.instance_count += 1
302
+ if entry.example_paths.size() < 3:
303
+ var path := "/root/" + scene_root.name
304
+ var relative := scene_root.get_path_to(node)
305
+ if relative != NodePath("."):
306
+ path += "/" + str(relative)
307
+ entry.example_paths.append(path)
308
+
309
+ for child in node.get_children():
310
+ _collect_processes(child, scene_root, script_map)
311
+
312
+
313
+ func _handle_get_signal_connections(data: Array) -> void:
314
+ var node_path: String = data[0] if data.size() > 0 else ""
315
+
316
+ var tree := get_tree()
317
+ var scene_root := tree.current_scene if tree else null
318
+ if not scene_root:
319
+ EngineDebugger.send_message("godot_mcp:game_response", ["get_signal_connections", {"connections": []}])
320
+ return
321
+
322
+ var search_root: Node = scene_root
323
+ if not node_path.is_empty():
324
+ search_root = _get_node_from_path(node_path, scene_root)
325
+ if not search_root:
326
+ EngineDebugger.send_message("godot_mcp:game_response", ["get_signal_connections", {"connections": [], "error": "Node not found: " + node_path}])
327
+ return
328
+
329
+ var connections: Array = []
330
+ _collect_signal_connections(search_root, scene_root, connections, 0)
331
+
332
+ EngineDebugger.send_message("godot_mcp:game_response", ["get_signal_connections", {"connections": connections}])
333
+
334
+
335
+ const MAX_SIGNAL_CONNECTIONS := 200
336
+ const MAX_SIGNAL_DEPTH := 20
337
+
338
+
339
+ func _collect_signal_connections(node: Node, scene_root: Node, connections: Array, depth: int) -> void:
340
+ if connections.size() >= MAX_SIGNAL_CONNECTIONS or depth > MAX_SIGNAL_DEPTH:
341
+ return
342
+
343
+ var source_path := _node_path_string(node, scene_root)
344
+
345
+ for sig_info in node.get_signal_list():
346
+ var sig_name: String = sig_info.name
347
+ for conn in node.get_signal_connection_list(sig_name):
348
+ if connections.size() >= MAX_SIGNAL_CONNECTIONS:
349
+ return
350
+ var target: Object = conn.callable.get_object()
351
+ var target_path := ""
352
+ if target is Node:
353
+ target_path = _node_path_string(target as Node, scene_root)
354
+ else:
355
+ target_path = str(target)
356
+ connections.append({
357
+ "source_path": source_path,
358
+ "signal_name": sig_name,
359
+ "target_path": target_path,
360
+ "method_name": conn.callable.get_method(),
361
+ })
362
+
363
+ for child in node.get_children():
364
+ if connections.size() >= MAX_SIGNAL_CONNECTIONS:
365
+ return
366
+ _collect_signal_connections(child, scene_root, connections, depth + 1)
367
+
368
+
369
+ func _node_path_string(node: Node, scene_root: Node) -> String:
370
+ var path := "/root/" + scene_root.name
371
+ var relative := scene_root.get_path_to(node)
372
+ if relative != NodePath("."):
373
+ path += "/" + str(relative)
374
+ return path
375
+
376
+
207
377
  class _MCPGameLogger extends Logger:
208
378
  var _output: PackedStringArray = []
209
379
  var _max_lines := 1000
package/addon/plugin.cfg CHANGED
@@ -3,6 +3,6 @@
3
3
  name="Godot MCP"
4
4
  description="Model Context Protocol server for AI assistant integration"
5
5
  author="godot-mcp"
6
- version="2.16.0"
6
+ version="2.17.0"
7
7
  script="plugin.gd"
8
8
  godot_version_min="4.5"