gopeak 2.3.7 → 2.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +279 -411
- package/build/addon/auto_reload/auto_reload.gd +126 -126
- package/build/addon/auto_reload/plugin.cfg +7 -7
- package/build/addon/godot_mcp_editor/mcp_client.gd +178 -178
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -6
- package/build/addon/godot_mcp_editor/plugin.gd +84 -84
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -114
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -502
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -425
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +761 -761
- package/build/addon/godot_mcp_runtime/godot_mcp_runtime.gd +33 -33
- package/build/addon/godot_mcp_runtime/mcp_runtime_autoload.gd +671 -619
- package/build/addon/godot_mcp_runtime/plugin.cfg +7 -7
- package/build/cli/notify.js +4 -3
- package/build/cli.js +18 -18
- package/build/godot-bridge.js +11 -11
- package/build/index.js +238 -123
- package/build/scripts/godot_operations.gd +6823 -6823
- package/build/visualizer/events.js +19 -19
- package/build/visualizer/panel.js +34 -34
- package/build/visualizer/usages.js +14 -14
- package/build/visualizer-server.js +11 -11
- package/build/visualizer.html +2596 -2596
- package/package.json +106 -107
- package/scripts/postinstall.mjs +29 -29
|
@@ -1,619 +1,671 @@
|
|
|
1
|
-
extends Node
|
|
2
|
-
|
|
3
|
-
## MCP Runtime Autoload
|
|
4
|
-
## This singleton runs in the game and provides runtime inspection capabilities.
|
|
5
|
-
## It starts a TCP server that the MCP server can connect to.
|
|
6
|
-
|
|
7
|
-
const DEFAULT_PORT = 7777
|
|
8
|
-
const PROTOCOL_VERSION = "1.0"
|
|
9
|
-
|
|
10
|
-
var _server: TCPServer
|
|
11
|
-
var _clients: Array[StreamPeerTCP] = []
|
|
12
|
-
var _port: int = DEFAULT_PORT
|
|
13
|
-
var _enabled: bool = true
|
|
14
|
-
var _watched_signals: Dictionary = {} # { "node_path:signal_name": callable }
|
|
15
|
-
|
|
16
|
-
signal client_connected
|
|
17
|
-
signal client_disconnected
|
|
18
|
-
signal command_received(command: String, params: Dictionary)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
func _ready() -> void:
|
|
22
|
-
name = "MCPRuntime"
|
|
23
|
-
_start_server()
|
|
24
|
-
print("[MCP Runtime] Autoload ready, server starting on port %d" % _port)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
func _process(_delta: float) -> void:
|
|
28
|
-
if not _enabled or _server == null:
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
# Accept new connections
|
|
32
|
-
if _server.is_connection_available():
|
|
33
|
-
var client = _server.take_connection()
|
|
34
|
-
if client:
|
|
35
|
-
_clients.append(client)
|
|
36
|
-
print("[MCP Runtime] Client connected")
|
|
37
|
-
client_connected.emit()
|
|
38
|
-
_send_welcome(client)
|
|
39
|
-
|
|
40
|
-
# Process client messages
|
|
41
|
-
var clients_to_remove: Array[StreamPeerTCP] = []
|
|
42
|
-
for client in _clients:
|
|
43
|
-
if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
44
|
-
clients_to_remove.append(client)
|
|
45
|
-
continue
|
|
46
|
-
|
|
47
|
-
client.poll()
|
|
48
|
-
if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
49
|
-
clients_to_remove.append(client)
|
|
50
|
-
continue
|
|
51
|
-
var available = client.get_available_bytes()
|
|
52
|
-
if available > 0:
|
|
53
|
-
var data = client.get_utf8_string(available)
|
|
54
|
-
_handle_message(client, data)
|
|
55
|
-
|
|
56
|
-
# Remove disconnected clients
|
|
57
|
-
for client in clients_to_remove:
|
|
58
|
-
_clients.erase(client)
|
|
59
|
-
print("[MCP Runtime] Client disconnected")
|
|
60
|
-
client_disconnected.emit()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
func _start_server() -> void:
|
|
64
|
-
_server = TCPServer.new()
|
|
65
|
-
var error = _server.listen(_port)
|
|
66
|
-
if error != OK:
|
|
67
|
-
push_error("[MCP Runtime] Failed to start server on port %d: %s" % [_port, error])
|
|
68
|
-
_enabled = false
|
|
69
|
-
else:
|
|
70
|
-
print("[MCP Runtime] Server listening on port %d" % _port)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
func _send_welcome(client: StreamPeerTCP) -> void:
|
|
74
|
-
var welcome = {
|
|
75
|
-
"type": "welcome",
|
|
76
|
-
"protocol_version": PROTOCOL_VERSION,
|
|
77
|
-
"godot_version": Engine.get_version_info(),
|
|
78
|
-
"project_name": ProjectSettings.get_setting("application/config/name", "Unknown")
|
|
79
|
-
}
|
|
80
|
-
_send_response(client, welcome)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
func _handle_message(client: StreamPeerTCP, data: String) -> void:
|
|
84
|
-
var json = JSON.new()
|
|
85
|
-
var error = json.parse(data)
|
|
86
|
-
if error != OK:
|
|
87
|
-
_send_error(client, "Invalid JSON: " + json.get_error_message())
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
var message = json.get_data()
|
|
91
|
-
if not message is Dictionary:
|
|
92
|
-
_send_error(client, "Message must be an object")
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
var command = message.get("command", "")
|
|
96
|
-
var params = message.get("params", {})
|
|
97
|
-
var request_id = message.get("id", null)
|
|
98
|
-
|
|
99
|
-
command_received.emit(command, params)
|
|
100
|
-
|
|
101
|
-
var result = _execute_command(command, params)
|
|
102
|
-
if request_id != null:
|
|
103
|
-
result["id"] = request_id
|
|
104
|
-
|
|
105
|
-
_send_response(client, result)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
func _execute_command(command: String, params: Dictionary) -> Dictionary:
|
|
109
|
-
match command:
|
|
110
|
-
"ping":
|
|
111
|
-
return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
|
|
112
|
-
|
|
113
|
-
"get_tree":
|
|
114
|
-
return _cmd_get_tree(params)
|
|
115
|
-
|
|
116
|
-
"get_node":
|
|
117
|
-
return _cmd_get_node(params)
|
|
118
|
-
|
|
119
|
-
"set_property":
|
|
120
|
-
return _cmd_set_property(params)
|
|
121
|
-
|
|
122
|
-
"call_method":
|
|
123
|
-
return _cmd_call_method(params)
|
|
124
|
-
|
|
125
|
-
"get_metrics":
|
|
126
|
-
return _cmd_get_metrics(params)
|
|
127
|
-
|
|
128
|
-
"capture_screenshot":
|
|
129
|
-
return _cmd_capture_screenshot(params)
|
|
130
|
-
|
|
131
|
-
"capture_viewport":
|
|
132
|
-
return _cmd_capture_viewport(params)
|
|
133
|
-
|
|
134
|
-
"inject_action":
|
|
135
|
-
return _cmd_inject_action(params)
|
|
136
|
-
|
|
137
|
-
"inject_key":
|
|
138
|
-
return _cmd_inject_key(params)
|
|
139
|
-
|
|
140
|
-
"inject_mouse_click":
|
|
141
|
-
return _cmd_inject_mouse_click(params)
|
|
142
|
-
|
|
143
|
-
"inject_mouse_motion":
|
|
144
|
-
return _cmd_inject_mouse_motion(params)
|
|
145
|
-
|
|
146
|
-
"watch_signal":
|
|
147
|
-
return _cmd_watch_signal(params)
|
|
148
|
-
|
|
149
|
-
"unwatch_signal":
|
|
150
|
-
return _cmd_unwatch_signal(params)
|
|
151
|
-
|
|
152
|
-
_:
|
|
153
|
-
return {"type": "error", "message": "Unknown command: " + command}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
func _cmd_get_tree(params: Dictionary) -> Dictionary:
|
|
157
|
-
var root_path = params.get("root", "/root")
|
|
158
|
-
var max_depth = params.get("depth", 3)
|
|
159
|
-
var include_properties = params.get("include_properties", false)
|
|
160
|
-
|
|
161
|
-
var root = get_tree().root.get_node_or_null(root_path)
|
|
162
|
-
if root == null:
|
|
163
|
-
return {"type": "error", "message": "Node not found: " + root_path}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
"type": "tree",
|
|
167
|
-
"root": _serialize_node_tree(root, 0, max_depth, include_properties)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
func _cmd_get_node(params: Dictionary) -> Dictionary:
|
|
172
|
-
var node_path = params.get("path", "")
|
|
173
|
-
if node_path.is_empty():
|
|
174
|
-
return {"type": "error", "message": "Node path required"}
|
|
175
|
-
|
|
176
|
-
var node = get_tree().root.get_node_or_null(node_path)
|
|
177
|
-
if node == null:
|
|
178
|
-
return {"type": "error", "message": "Node not found: " + node_path}
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
"type": "node",
|
|
182
|
-
"data": _serialize_node(node, true)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
func _cmd_set_property(params: Dictionary) -> Dictionary:
|
|
187
|
-
var node_path = params.get("path", "")
|
|
188
|
-
var property = params.get("property", "")
|
|
189
|
-
var value = params.get("value")
|
|
190
|
-
|
|
191
|
-
if node_path.is_empty() or property.is_empty():
|
|
192
|
-
return {"type": "error", "message": "Node path and property required"}
|
|
193
|
-
|
|
194
|
-
var node = get_tree().root.get_node_or_null(node_path)
|
|
195
|
-
if node == null:
|
|
196
|
-
return {"type": "error", "message": "Node not found: " + node_path}
|
|
197
|
-
|
|
198
|
-
var old_value = node.get(property)
|
|
199
|
-
node.set(property, _deserialize_value(value))
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
"type": "property_set",
|
|
203
|
-
"path": node_path,
|
|
204
|
-
"property": property,
|
|
205
|
-
"old_value": _serialize_value(old_value),
|
|
206
|
-
"new_value": _serialize_value(node.get(property))
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
func _cmd_call_method(params: Dictionary) -> Dictionary:
|
|
211
|
-
var node_path = params.get("path", "")
|
|
212
|
-
var method = params.get("method", "")
|
|
213
|
-
var args = params.get("args", [])
|
|
214
|
-
|
|
215
|
-
if node_path.is_empty() or method.is_empty():
|
|
216
|
-
return {"type": "error", "message": "Node path and method required"}
|
|
217
|
-
|
|
218
|
-
var node = get_tree().root.get_node_or_null(node_path)
|
|
219
|
-
if node == null:
|
|
220
|
-
return {"type": "error", "message": "Node not found: " + node_path}
|
|
221
|
-
|
|
222
|
-
if not node.has_method(method):
|
|
223
|
-
return {"type": "error", "message": "Method not found: " + method}
|
|
224
|
-
|
|
225
|
-
var deserialized_args = []
|
|
226
|
-
for arg in args:
|
|
227
|
-
deserialized_args.append(_deserialize_value(arg))
|
|
228
|
-
|
|
229
|
-
var result = node.callv(method, deserialized_args)
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
"type": "method_result",
|
|
233
|
-
"path": node_path,
|
|
234
|
-
"method": method,
|
|
235
|
-
"result": _serialize_value(result)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
func _cmd_get_metrics(params: Dictionary) -> Dictionary:
|
|
240
|
-
var metrics = params.get("metrics", [])
|
|
241
|
-
var result = {
|
|
242
|
-
"type": "metrics",
|
|
243
|
-
"data": {}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
# Always include basic metrics
|
|
247
|
-
result["data"]["fps"] = Engine.get_frames_per_second()
|
|
248
|
-
result["data"]["frame_time"] = Performance.get_monitor(Performance.TIME_PROCESS)
|
|
249
|
-
result["data"]["physics_time"] = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
|
|
250
|
-
|
|
251
|
-
# Memory metrics
|
|
252
|
-
result["data"]["memory_static"] = Performance.get_monitor(Performance.MEMORY_STATIC)
|
|
253
|
-
result["data"]["memory_static_max"] = Performance.get_monitor(Performance.MEMORY_STATIC_MAX)
|
|
254
|
-
|
|
255
|
-
# Object counts
|
|
256
|
-
result["data"]["object_count"] = Performance.get_monitor(Performance.OBJECT_COUNT)
|
|
257
|
-
result["data"]["object_resource_count"] = Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT)
|
|
258
|
-
result["data"]["object_node_count"] = Performance.get_monitor(Performance.OBJECT_NODE_COUNT)
|
|
259
|
-
result["data"]["object_orphan_node_count"] = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
|
|
260
|
-
|
|
261
|
-
# Render metrics
|
|
262
|
-
result["data"]["render_total_objects"] = Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)
|
|
263
|
-
result["data"]["render_total_primitives"] = Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)
|
|
264
|
-
result["data"]["render_total_draw_calls"] = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
|
|
265
|
-
|
|
266
|
-
return result
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
func _cmd_capture_screenshot(params: Dictionary) -> Dictionary:
|
|
270
|
-
var viewport = get_viewport()
|
|
271
|
-
if viewport == null:
|
|
272
|
-
return {"type": "error", "message": "No viewport available"}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
var
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
var
|
|
362
|
-
var pressed = bool(params.get("pressed", true))
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return {"type": "error", "message": "
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
event.
|
|
413
|
-
event
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
var
|
|
440
|
-
if
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
"
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1
|
+
extends Node
|
|
2
|
+
|
|
3
|
+
## MCP Runtime Autoload
|
|
4
|
+
## This singleton runs in the game and provides runtime inspection capabilities.
|
|
5
|
+
## It starts a TCP server that the MCP server can connect to.
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PORT = 7777
|
|
8
|
+
const PROTOCOL_VERSION = "1.0"
|
|
9
|
+
|
|
10
|
+
var _server: TCPServer
|
|
11
|
+
var _clients: Array[StreamPeerTCP] = []
|
|
12
|
+
var _port: int = DEFAULT_PORT
|
|
13
|
+
var _enabled: bool = true
|
|
14
|
+
var _watched_signals: Dictionary = {} # { "node_path:signal_name": callable }
|
|
15
|
+
|
|
16
|
+
signal client_connected
|
|
17
|
+
signal client_disconnected
|
|
18
|
+
signal command_received(command: String, params: Dictionary)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
func _ready() -> void:
|
|
22
|
+
name = "MCPRuntime"
|
|
23
|
+
_start_server()
|
|
24
|
+
print("[MCP Runtime] Autoload ready, server starting on port %d" % _port)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
func _process(_delta: float) -> void:
|
|
28
|
+
if not _enabled or _server == null:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Accept new connections
|
|
32
|
+
if _server.is_connection_available():
|
|
33
|
+
var client = _server.take_connection()
|
|
34
|
+
if client:
|
|
35
|
+
_clients.append(client)
|
|
36
|
+
print("[MCP Runtime] Client connected")
|
|
37
|
+
client_connected.emit()
|
|
38
|
+
_send_welcome(client)
|
|
39
|
+
|
|
40
|
+
# Process client messages
|
|
41
|
+
var clients_to_remove: Array[StreamPeerTCP] = []
|
|
42
|
+
for client in _clients:
|
|
43
|
+
if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
44
|
+
clients_to_remove.append(client)
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
client.poll()
|
|
48
|
+
if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
49
|
+
clients_to_remove.append(client)
|
|
50
|
+
continue
|
|
51
|
+
var available = client.get_available_bytes()
|
|
52
|
+
if available > 0:
|
|
53
|
+
var data = client.get_utf8_string(available)
|
|
54
|
+
_handle_message(client, data)
|
|
55
|
+
|
|
56
|
+
# Remove disconnected clients
|
|
57
|
+
for client in clients_to_remove:
|
|
58
|
+
_clients.erase(client)
|
|
59
|
+
print("[MCP Runtime] Client disconnected")
|
|
60
|
+
client_disconnected.emit()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
func _start_server() -> void:
|
|
64
|
+
_server = TCPServer.new()
|
|
65
|
+
var error = _server.listen(_port)
|
|
66
|
+
if error != OK:
|
|
67
|
+
push_error("[MCP Runtime] Failed to start server on port %d: %s" % [_port, error])
|
|
68
|
+
_enabled = false
|
|
69
|
+
else:
|
|
70
|
+
print("[MCP Runtime] Server listening on port %d" % _port)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
func _send_welcome(client: StreamPeerTCP) -> void:
|
|
74
|
+
var welcome = {
|
|
75
|
+
"type": "welcome",
|
|
76
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
77
|
+
"godot_version": Engine.get_version_info(),
|
|
78
|
+
"project_name": ProjectSettings.get_setting("application/config/name", "Unknown")
|
|
79
|
+
}
|
|
80
|
+
_send_response(client, welcome)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
func _handle_message(client: StreamPeerTCP, data: String) -> void:
|
|
84
|
+
var json = JSON.new()
|
|
85
|
+
var error = json.parse(data)
|
|
86
|
+
if error != OK:
|
|
87
|
+
_send_error(client, "Invalid JSON: " + json.get_error_message())
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
var message = json.get_data()
|
|
91
|
+
if not message is Dictionary:
|
|
92
|
+
_send_error(client, "Message must be an object")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
var command = message.get("command", "")
|
|
96
|
+
var params = message.get("params", {})
|
|
97
|
+
var request_id = message.get("id", null)
|
|
98
|
+
|
|
99
|
+
command_received.emit(command, params)
|
|
100
|
+
|
|
101
|
+
var result = _execute_command(command, params)
|
|
102
|
+
if request_id != null:
|
|
103
|
+
result["id"] = request_id
|
|
104
|
+
|
|
105
|
+
_send_response(client, result)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
func _execute_command(command: String, params: Dictionary) -> Dictionary:
|
|
109
|
+
match command:
|
|
110
|
+
"ping":
|
|
111
|
+
return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
|
|
112
|
+
|
|
113
|
+
"get_tree":
|
|
114
|
+
return _cmd_get_tree(params)
|
|
115
|
+
|
|
116
|
+
"get_node":
|
|
117
|
+
return _cmd_get_node(params)
|
|
118
|
+
|
|
119
|
+
"set_property":
|
|
120
|
+
return _cmd_set_property(params)
|
|
121
|
+
|
|
122
|
+
"call_method":
|
|
123
|
+
return _cmd_call_method(params)
|
|
124
|
+
|
|
125
|
+
"get_metrics":
|
|
126
|
+
return _cmd_get_metrics(params)
|
|
127
|
+
|
|
128
|
+
"capture_screenshot":
|
|
129
|
+
return _cmd_capture_screenshot(params)
|
|
130
|
+
|
|
131
|
+
"capture_viewport":
|
|
132
|
+
return _cmd_capture_viewport(params)
|
|
133
|
+
|
|
134
|
+
"inject_action":
|
|
135
|
+
return _cmd_inject_action(params)
|
|
136
|
+
|
|
137
|
+
"inject_key":
|
|
138
|
+
return _cmd_inject_key(params)
|
|
139
|
+
|
|
140
|
+
"inject_mouse_click":
|
|
141
|
+
return _cmd_inject_mouse_click(params)
|
|
142
|
+
|
|
143
|
+
"inject_mouse_motion":
|
|
144
|
+
return _cmd_inject_mouse_motion(params)
|
|
145
|
+
|
|
146
|
+
"watch_signal":
|
|
147
|
+
return _cmd_watch_signal(params)
|
|
148
|
+
|
|
149
|
+
"unwatch_signal":
|
|
150
|
+
return _cmd_unwatch_signal(params)
|
|
151
|
+
|
|
152
|
+
_:
|
|
153
|
+
return {"type": "error", "message": "Unknown command: " + command}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
func _cmd_get_tree(params: Dictionary) -> Dictionary:
|
|
157
|
+
var root_path = params.get("root", "/root")
|
|
158
|
+
var max_depth = params.get("depth", 3)
|
|
159
|
+
var include_properties = params.get("include_properties", false)
|
|
160
|
+
|
|
161
|
+
var root = get_tree().root.get_node_or_null(root_path)
|
|
162
|
+
if root == null:
|
|
163
|
+
return {"type": "error", "message": "Node not found: " + root_path}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"type": "tree",
|
|
167
|
+
"root": _serialize_node_tree(root, 0, max_depth, include_properties)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
func _cmd_get_node(params: Dictionary) -> Dictionary:
|
|
172
|
+
var node_path = params.get("path", "")
|
|
173
|
+
if node_path.is_empty():
|
|
174
|
+
return {"type": "error", "message": "Node path required"}
|
|
175
|
+
|
|
176
|
+
var node = get_tree().root.get_node_or_null(node_path)
|
|
177
|
+
if node == null:
|
|
178
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
"type": "node",
|
|
182
|
+
"data": _serialize_node(node, true)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
func _cmd_set_property(params: Dictionary) -> Dictionary:
|
|
187
|
+
var node_path = params.get("path", "")
|
|
188
|
+
var property = params.get("property", "")
|
|
189
|
+
var value = params.get("value")
|
|
190
|
+
|
|
191
|
+
if node_path.is_empty() or property.is_empty():
|
|
192
|
+
return {"type": "error", "message": "Node path and property required"}
|
|
193
|
+
|
|
194
|
+
var node = get_tree().root.get_node_or_null(node_path)
|
|
195
|
+
if node == null:
|
|
196
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
197
|
+
|
|
198
|
+
var old_value = node.get(property)
|
|
199
|
+
node.set(property, _deserialize_value(value))
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"type": "property_set",
|
|
203
|
+
"path": node_path,
|
|
204
|
+
"property": property,
|
|
205
|
+
"old_value": _serialize_value(old_value),
|
|
206
|
+
"new_value": _serialize_value(node.get(property))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
func _cmd_call_method(params: Dictionary) -> Dictionary:
|
|
211
|
+
var node_path = params.get("path", "")
|
|
212
|
+
var method = params.get("method", "")
|
|
213
|
+
var args = params.get("args", [])
|
|
214
|
+
|
|
215
|
+
if node_path.is_empty() or method.is_empty():
|
|
216
|
+
return {"type": "error", "message": "Node path and method required"}
|
|
217
|
+
|
|
218
|
+
var node = get_tree().root.get_node_or_null(node_path)
|
|
219
|
+
if node == null:
|
|
220
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
221
|
+
|
|
222
|
+
if not node.has_method(method):
|
|
223
|
+
return {"type": "error", "message": "Method not found: " + method}
|
|
224
|
+
|
|
225
|
+
var deserialized_args = []
|
|
226
|
+
for arg in args:
|
|
227
|
+
deserialized_args.append(_deserialize_value(arg))
|
|
228
|
+
|
|
229
|
+
var result = node.callv(method, deserialized_args)
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"type": "method_result",
|
|
233
|
+
"path": node_path,
|
|
234
|
+
"method": method,
|
|
235
|
+
"result": _serialize_value(result)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
func _cmd_get_metrics(params: Dictionary) -> Dictionary:
|
|
240
|
+
var metrics = params.get("metrics", [])
|
|
241
|
+
var result = {
|
|
242
|
+
"type": "metrics",
|
|
243
|
+
"data": {}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Always include basic metrics
|
|
247
|
+
result["data"]["fps"] = Engine.get_frames_per_second()
|
|
248
|
+
result["data"]["frame_time"] = Performance.get_monitor(Performance.TIME_PROCESS)
|
|
249
|
+
result["data"]["physics_time"] = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
|
|
250
|
+
|
|
251
|
+
# Memory metrics
|
|
252
|
+
result["data"]["memory_static"] = Performance.get_monitor(Performance.MEMORY_STATIC)
|
|
253
|
+
result["data"]["memory_static_max"] = Performance.get_monitor(Performance.MEMORY_STATIC_MAX)
|
|
254
|
+
|
|
255
|
+
# Object counts
|
|
256
|
+
result["data"]["object_count"] = Performance.get_monitor(Performance.OBJECT_COUNT)
|
|
257
|
+
result["data"]["object_resource_count"] = Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT)
|
|
258
|
+
result["data"]["object_node_count"] = Performance.get_monitor(Performance.OBJECT_NODE_COUNT)
|
|
259
|
+
result["data"]["object_orphan_node_count"] = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
|
|
260
|
+
|
|
261
|
+
# Render metrics
|
|
262
|
+
result["data"]["render_total_objects"] = Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)
|
|
263
|
+
result["data"]["render_total_primitives"] = Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)
|
|
264
|
+
result["data"]["render_total_draw_calls"] = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
|
|
265
|
+
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
func _cmd_capture_screenshot(params: Dictionary) -> Dictionary:
|
|
270
|
+
var viewport = get_viewport()
|
|
271
|
+
if viewport == null:
|
|
272
|
+
return {"type": "error", "message": "No viewport available"}
|
|
273
|
+
return _capture_viewport_image(viewport, params)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
func _cmd_capture_viewport(params: Dictionary) -> Dictionary:
|
|
277
|
+
var viewport_path = String(params.get("viewportPath", params.get("viewport_path", "")))
|
|
278
|
+
if viewport_path.is_empty():
|
|
279
|
+
return _cmd_capture_screenshot(params)
|
|
280
|
+
|
|
281
|
+
var node = get_tree().root.get_node_or_null(viewport_path)
|
|
282
|
+
if node == null:
|
|
283
|
+
return {"type": "error", "message": "Viewport not found: " + viewport_path}
|
|
284
|
+
if not node is Viewport:
|
|
285
|
+
return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
|
|
286
|
+
return _capture_viewport_image(node as Viewport, params)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
func _capture_viewport_image(viewport: Viewport, params: Dictionary) -> Dictionary:
|
|
290
|
+
var viewport_texture = viewport.get_texture()
|
|
291
|
+
if viewport_texture == null:
|
|
292
|
+
return {"type": "error", "message": "No viewport texture available"}
|
|
293
|
+
|
|
294
|
+
var image = viewport_texture.get_image()
|
|
295
|
+
if image == null:
|
|
296
|
+
return {"type": "error", "message": "Failed to capture viewport image"}
|
|
297
|
+
|
|
298
|
+
var width = int(params.get("width", 0))
|
|
299
|
+
var height = int(params.get("height", 0))
|
|
300
|
+
if width > 0 and height > 0:
|
|
301
|
+
image.resize(width, height)
|
|
302
|
+
|
|
303
|
+
var requested_path = String(params.get("output_path", params.get("outputPath", "")))
|
|
304
|
+
if requested_path.is_empty():
|
|
305
|
+
var png_bytes = image.save_png_to_buffer()
|
|
306
|
+
if png_bytes.is_empty():
|
|
307
|
+
return {"type": "error", "message": "Failed to encode screenshot as PNG"}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"type": "screenshot",
|
|
311
|
+
"format": "png",
|
|
312
|
+
"encoding": "base64",
|
|
313
|
+
"width": image.get_width(),
|
|
314
|
+
"height": image.get_height(),
|
|
315
|
+
"data": Marshalls.raw_to_base64(png_bytes)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
var screenshot_path = requested_path
|
|
319
|
+
if screenshot_path.begins_with("user://") or screenshot_path.begins_with("res://"):
|
|
320
|
+
screenshot_path = ProjectSettings.globalize_path(screenshot_path)
|
|
321
|
+
var save_error = image.save_png(screenshot_path)
|
|
322
|
+
if save_error != OK:
|
|
323
|
+
return {"type": "error", "message": "Failed to save screenshot as PNG: " + str(save_error)}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
"type": "screenshot_file",
|
|
327
|
+
"format": "png",
|
|
328
|
+
"encoding": "file",
|
|
329
|
+
"width": image.get_width(),
|
|
330
|
+
"height": image.get_height(),
|
|
331
|
+
"path": screenshot_path
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
func _cmd_inject_action(params: Dictionary) -> Dictionary:
|
|
336
|
+
var action = String(params.get("action", ""))
|
|
337
|
+
var pressed = bool(params.get("pressed", true))
|
|
338
|
+
var strength = float(params.get("strength", 1.0))
|
|
339
|
+
|
|
340
|
+
if action.is_empty():
|
|
341
|
+
return {"type": "error", "message": "Action name required"}
|
|
342
|
+
|
|
343
|
+
if not InputMap.has_action(action):
|
|
344
|
+
return {"type": "error", "message": "Action not found: " + action}
|
|
345
|
+
|
|
346
|
+
var event = InputEventAction.new()
|
|
347
|
+
event.action = action
|
|
348
|
+
event.pressed = pressed
|
|
349
|
+
event.strength = strength
|
|
350
|
+
Input.parse_input_event(event)
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"type": "input_injected",
|
|
354
|
+
"input_type": "action",
|
|
355
|
+
"action": action,
|
|
356
|
+
"pressed": pressed
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
func _cmd_inject_key(params: Dictionary) -> Dictionary:
|
|
361
|
+
var keycode_raw: Variant = params.get("keycode", 0)
|
|
362
|
+
var pressed = bool(params.get("pressed", true))
|
|
363
|
+
var key_label = String(params.get("key_label", ""))
|
|
364
|
+
|
|
365
|
+
if keycode_raw is String and not (keycode_raw as String).is_empty() and key_label.is_empty():
|
|
366
|
+
key_label = keycode_raw as String
|
|
367
|
+
var keycode: int = 0 if keycode_raw is String else int(keycode_raw)
|
|
368
|
+
|
|
369
|
+
var event = InputEventKey.new()
|
|
370
|
+
event.pressed = pressed
|
|
371
|
+
|
|
372
|
+
if not key_label.is_empty():
|
|
373
|
+
event.keycode = OS.find_keycode_from_string(key_label)
|
|
374
|
+
if event.keycode == KEY_NONE:
|
|
375
|
+
return {"type": "error", "message": "Invalid key_label: " + key_label}
|
|
376
|
+
elif keycode > 0:
|
|
377
|
+
event.keycode = keycode
|
|
378
|
+
else:
|
|
379
|
+
return {"type": "error", "message": "keycode or key_label required"}
|
|
380
|
+
|
|
381
|
+
Input.parse_input_event(event)
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
"type": "input_injected",
|
|
385
|
+
"input_type": "key",
|
|
386
|
+
"keycode": event.keycode,
|
|
387
|
+
"pressed": pressed
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
func _cmd_inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
392
|
+
var position: Vector2
|
|
393
|
+
if params.has("x") and params.has("y"):
|
|
394
|
+
position = Vector2(float(params["x"]), float(params["y"]))
|
|
395
|
+
else:
|
|
396
|
+
var pos_raw = params.get("position", Vector2.ZERO)
|
|
397
|
+
if pos_raw is Array:
|
|
398
|
+
if pos_raw.size() < 2:
|
|
399
|
+
return {"type": "error", "message": "position array must contain [x, y]"}
|
|
400
|
+
position = Vector2(float(pos_raw[0]), float(pos_raw[1]))
|
|
401
|
+
elif pos_raw is Vector2:
|
|
402
|
+
position = pos_raw
|
|
403
|
+
else:
|
|
404
|
+
return {"type": "error", "message": "position must be Vector2 or [x, y]"}
|
|
405
|
+
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
406
|
+
var pressed = bool(params.get("pressed", true))
|
|
407
|
+
|
|
408
|
+
var event = InputEventMouseButton.new()
|
|
409
|
+
event.position = position
|
|
410
|
+
event.global_position = position
|
|
411
|
+
event.button_index = button
|
|
412
|
+
event.pressed = pressed
|
|
413
|
+
Input.parse_input_event(event)
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
"type": "input_injected",
|
|
417
|
+
"input_type": "mouse_click",
|
|
418
|
+
"position": [position.x, position.y],
|
|
419
|
+
"button": button,
|
|
420
|
+
"pressed": pressed
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
func _cmd_inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
425
|
+
var position: Vector2
|
|
426
|
+
if params.has("x") and params.has("y"):
|
|
427
|
+
position = Vector2(float(params["x"]), float(params["y"]))
|
|
428
|
+
else:
|
|
429
|
+
var pos_raw = params.get("position", Vector2.ZERO)
|
|
430
|
+
if pos_raw is Array:
|
|
431
|
+
if pos_raw.size() < 2:
|
|
432
|
+
return {"type": "error", "message": "position array must contain [x, y]"}
|
|
433
|
+
position = Vector2(float(pos_raw[0]), float(pos_raw[1]))
|
|
434
|
+
elif pos_raw is Vector2:
|
|
435
|
+
position = pos_raw
|
|
436
|
+
else:
|
|
437
|
+
return {"type": "error", "message": "position must be Vector2 or [x, y]"}
|
|
438
|
+
var rel_raw = params.get("relative", Vector2.ZERO)
|
|
439
|
+
var relative: Vector2
|
|
440
|
+
if params.has("relativeX") and params.has("relativeY"):
|
|
441
|
+
relative = Vector2(float(params["relativeX"]), float(params["relativeY"]))
|
|
442
|
+
elif rel_raw is Array:
|
|
443
|
+
if rel_raw.size() < 2:
|
|
444
|
+
return {"type": "error", "message": "relative array must contain [x, y]"}
|
|
445
|
+
relative = Vector2(float(rel_raw[0]), float(rel_raw[1]))
|
|
446
|
+
elif rel_raw is Vector2:
|
|
447
|
+
relative = rel_raw
|
|
448
|
+
else:
|
|
449
|
+
return {"type": "error", "message": "relative must be Vector2 or [x, y]"}
|
|
450
|
+
|
|
451
|
+
var event = InputEventMouseMotion.new()
|
|
452
|
+
event.position = position
|
|
453
|
+
event.global_position = position
|
|
454
|
+
event.relative = relative
|
|
455
|
+
Input.parse_input_event(event)
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
"type": "input_injected",
|
|
459
|
+
"input_type": "mouse_motion",
|
|
460
|
+
"position": [position.x, position.y],
|
|
461
|
+
"relative": [relative.x, relative.y]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
func _cmd_watch_signal(params: Dictionary) -> Dictionary:
|
|
466
|
+
var node_path = params.get("path", "")
|
|
467
|
+
var signal_name = params.get("signal", "")
|
|
468
|
+
|
|
469
|
+
if node_path.is_empty() or signal_name.is_empty():
|
|
470
|
+
return {"type": "error", "message": "Node path and signal name required"}
|
|
471
|
+
|
|
472
|
+
var node = get_tree().root.get_node_or_null(node_path)
|
|
473
|
+
if node == null:
|
|
474
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
475
|
+
|
|
476
|
+
if not node.has_signal(signal_name):
|
|
477
|
+
return {"type": "error", "message": "Signal not found: " + signal_name}
|
|
478
|
+
|
|
479
|
+
var key = node_path + ":" + signal_name
|
|
480
|
+
if _watched_signals.has(key):
|
|
481
|
+
return {"type": "error", "message": "Signal already being watched"}
|
|
482
|
+
|
|
483
|
+
var callable = func(args = []):
|
|
484
|
+
_broadcast_signal_event(node_path, signal_name, args)
|
|
485
|
+
|
|
486
|
+
node.connect(signal_name, callable)
|
|
487
|
+
_watched_signals[key] = callable
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
"type": "signal_watched",
|
|
491
|
+
"path": node_path,
|
|
492
|
+
"signal": signal_name
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
func _cmd_unwatch_signal(params: Dictionary) -> Dictionary:
|
|
497
|
+
var node_path = params.get("path", "")
|
|
498
|
+
var signal_name = params.get("signal", "")
|
|
499
|
+
|
|
500
|
+
var key = node_path + ":" + signal_name
|
|
501
|
+
if not _watched_signals.has(key):
|
|
502
|
+
return {"type": "error", "message": "Signal not being watched"}
|
|
503
|
+
|
|
504
|
+
var node = get_tree().root.get_node_or_null(node_path)
|
|
505
|
+
if node != null:
|
|
506
|
+
node.disconnect(signal_name, _watched_signals[key])
|
|
507
|
+
|
|
508
|
+
_watched_signals.erase(key)
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
"type": "signal_unwatched",
|
|
512
|
+
"path": node_path,
|
|
513
|
+
"signal": signal_name
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
func _broadcast_signal_event(node_path: String, signal_name: String, args: Array) -> void:
|
|
518
|
+
var event = {
|
|
519
|
+
"type": "signal_event",
|
|
520
|
+
"path": node_path,
|
|
521
|
+
"signal": signal_name,
|
|
522
|
+
"args": []
|
|
523
|
+
}
|
|
524
|
+
for arg in args:
|
|
525
|
+
event["args"].append(_serialize_value(arg))
|
|
526
|
+
|
|
527
|
+
for client in _clients:
|
|
528
|
+
if client.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
|
529
|
+
_send_response(client, event)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_properties: bool) -> Dictionary:
|
|
533
|
+
var result = _serialize_node(node, include_properties)
|
|
534
|
+
|
|
535
|
+
if depth < max_depth:
|
|
536
|
+
var children = []
|
|
537
|
+
for child in node.get_children():
|
|
538
|
+
children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
|
|
539
|
+
result["children"] = children
|
|
540
|
+
|
|
541
|
+
return result
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
func _serialize_node(node: Node, include_properties: bool) -> Dictionary:
|
|
545
|
+
var result = {
|
|
546
|
+
"name": node.name,
|
|
547
|
+
"type": node.get_class(),
|
|
548
|
+
"path": str(node.get_path())
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if node.get_script():
|
|
552
|
+
result["script"] = node.get_script().resource_path
|
|
553
|
+
|
|
554
|
+
if include_properties:
|
|
555
|
+
result["properties"] = {}
|
|
556
|
+
for prop in node.get_property_list():
|
|
557
|
+
if prop["usage"] & PROPERTY_USAGE_STORAGE:
|
|
558
|
+
var name = prop["name"]
|
|
559
|
+
if not name.begins_with("_"):
|
|
560
|
+
result["properties"][name] = _serialize_value(node.get(name))
|
|
561
|
+
|
|
562
|
+
return result
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
func _serialize_value(value) -> Variant:
|
|
566
|
+
if value == null:
|
|
567
|
+
return null
|
|
568
|
+
elif value is Vector2:
|
|
569
|
+
return {"_type": "Vector2", "x": value.x, "y": value.y}
|
|
570
|
+
elif value is Vector3:
|
|
571
|
+
return {"_type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
|
|
572
|
+
elif value is Vector2i:
|
|
573
|
+
return {"_type": "Vector2i", "x": value.x, "y": value.y}
|
|
574
|
+
elif value is Vector3i:
|
|
575
|
+
return {"_type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
|
|
576
|
+
elif value is Color:
|
|
577
|
+
return {"_type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
578
|
+
elif value is Rect2:
|
|
579
|
+
return {"_type": "Rect2", "position": _serialize_value(value.position), "size": _serialize_value(value.size)}
|
|
580
|
+
elif value is Transform2D:
|
|
581
|
+
return {"_type": "Transform2D", "origin": _serialize_value(value.origin), "x": _serialize_value(value.x), "y": _serialize_value(value.y)}
|
|
582
|
+
elif value is NodePath:
|
|
583
|
+
return {"_type": "NodePath", "path": str(value)}
|
|
584
|
+
elif value is Resource:
|
|
585
|
+
return {"_type": "Resource", "path": value.resource_path, "class": value.get_class()}
|
|
586
|
+
elif value is Array:
|
|
587
|
+
var arr = []
|
|
588
|
+
for item in value:
|
|
589
|
+
arr.append(_serialize_value(item))
|
|
590
|
+
return arr
|
|
591
|
+
elif value is Dictionary:
|
|
592
|
+
var dict = {}
|
|
593
|
+
for key in value:
|
|
594
|
+
dict[str(key)] = _serialize_value(value[key])
|
|
595
|
+
return dict
|
|
596
|
+
elif value is Object:
|
|
597
|
+
return {"_type": "Object", "class": value.get_class()}
|
|
598
|
+
else:
|
|
599
|
+
return value
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
func _deserialize_value(value) -> Variant:
|
|
603
|
+
if value == null:
|
|
604
|
+
return null
|
|
605
|
+
elif value is Dictionary:
|
|
606
|
+
if value.has("_type"):
|
|
607
|
+
match value["_type"]:
|
|
608
|
+
"Vector2":
|
|
609
|
+
return Vector2(value.get("x", 0), value.get("y", 0))
|
|
610
|
+
"Vector3":
|
|
611
|
+
return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
612
|
+
"Vector2i":
|
|
613
|
+
return Vector2i(value.get("x", 0), value.get("y", 0))
|
|
614
|
+
"Vector3i":
|
|
615
|
+
return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
|
|
616
|
+
"Color":
|
|
617
|
+
return Color(value.get("r", 0), value.get("g", 0), value.get("b", 0), value.get("a", 1))
|
|
618
|
+
"NodePath":
|
|
619
|
+
return NodePath(value.get("path", ""))
|
|
620
|
+
_:
|
|
621
|
+
return value
|
|
622
|
+
else:
|
|
623
|
+
var dict = {}
|
|
624
|
+
for key in value:
|
|
625
|
+
dict[key] = _deserialize_value(value[key])
|
|
626
|
+
return dict
|
|
627
|
+
elif value is Array:
|
|
628
|
+
var arr = []
|
|
629
|
+
for item in value:
|
|
630
|
+
arr.append(_deserialize_value(item))
|
|
631
|
+
return arr
|
|
632
|
+
else:
|
|
633
|
+
return value
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
func _resolve_mouse_button(raw: Variant) -> int:
|
|
637
|
+
if raw is String:
|
|
638
|
+
match (raw as String).to_lower():
|
|
639
|
+
"left": return MOUSE_BUTTON_LEFT
|
|
640
|
+
"right": return MOUSE_BUTTON_RIGHT
|
|
641
|
+
"middle": return MOUSE_BUTTON_MIDDLE
|
|
642
|
+
"wheel_up", "wheelup": return MOUSE_BUTTON_WHEEL_UP
|
|
643
|
+
"wheel_down", "wheeldown": return MOUSE_BUTTON_WHEEL_DOWN
|
|
644
|
+
_: return MOUSE_BUTTON_LEFT
|
|
645
|
+
return int(raw)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
|
|
649
|
+
var json_str = JSON.stringify(data) + "\n"
|
|
650
|
+
client.put_utf8_string(json_str)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
func _send_error(client: StreamPeerTCP, message: String) -> void:
|
|
654
|
+
_send_response(client, {"type": "error", "message": message})
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
func _notification(what: int) -> void:
|
|
658
|
+
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
|
659
|
+
_cleanup()
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
func _cleanup() -> void:
|
|
663
|
+
for client in _clients:
|
|
664
|
+
client.disconnect_from_host()
|
|
665
|
+
_clients.clear()
|
|
666
|
+
|
|
667
|
+
if _server:
|
|
668
|
+
_server.stop()
|
|
669
|
+
_server = null
|
|
670
|
+
|
|
671
|
+
print("[MCP Runtime] Cleanup complete")
|