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.
@@ -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
- var viewport_texture = viewport.get_texture()
275
- if viewport_texture == null:
276
- return {"type": "error", "message": "No viewport texture available"}
277
-
278
- var image = viewport_texture.get_image()
279
- if image == null:
280
- return {"type": "error", "message": "Failed to capture viewport image"}
281
-
282
- var width = int(params.get("width", 0))
283
- var height = int(params.get("height", 0))
284
- if width > 0 and height > 0:
285
- image.resize(width, height)
286
-
287
- var png_bytes = image.save_png_to_buffer()
288
- if png_bytes.is_empty():
289
- return {"type": "error", "message": "Failed to encode screenshot as PNG"}
290
-
291
- var base64_str = Marshalls.raw_to_base64(png_bytes)
292
-
293
- return {
294
- "type": "screenshot",
295
- "format": "png",
296
- "encoding": "base64",
297
- "width": image.get_width(),
298
- "height": image.get_height(),
299
- "data": base64_str
300
- }
301
-
302
-
303
- func _cmd_capture_viewport(params: Dictionary) -> Dictionary:
304
- return _cmd_capture_screenshot(params)
305
-
306
-
307
- func _cmd_inject_action(params: Dictionary) -> Dictionary:
308
- var action = String(params.get("action", ""))
309
- var pressed = bool(params.get("pressed", true))
310
- var strength = float(params.get("strength", 1.0))
311
-
312
- if action.is_empty():
313
- return {"type": "error", "message": "Action name required"}
314
-
315
- if not InputMap.has_action(action):
316
- return {"type": "error", "message": "Action not found: " + action}
317
-
318
- var event = InputEventAction.new()
319
- event.action = action
320
- event.pressed = pressed
321
- event.strength = strength
322
- Input.parse_input_event(event)
323
-
324
- return {
325
- "type": "input_injected",
326
- "input_type": "action",
327
- "action": action,
328
- "pressed": pressed
329
- }
330
-
331
-
332
- func _cmd_inject_key(params: Dictionary) -> Dictionary:
333
- var keycode = int(params.get("keycode", 0))
334
- var pressed = bool(params.get("pressed", true))
335
- var key_label = String(params.get("key_label", ""))
336
-
337
- var event = InputEventKey.new()
338
- event.pressed = pressed
339
-
340
- if not key_label.is_empty():
341
- event.keycode = OS.find_keycode_from_string(key_label)
342
- if event.keycode == KEY_NONE:
343
- return {"type": "error", "message": "Invalid key_label: " + key_label}
344
- elif keycode > 0:
345
- event.keycode = keycode
346
- else:
347
- return {"type": "error", "message": "keycode or key_label required"}
348
-
349
- Input.parse_input_event(event)
350
-
351
- return {
352
- "type": "input_injected",
353
- "input_type": "key",
354
- "keycode": event.keycode,
355
- "pressed": pressed
356
- }
357
-
358
-
359
- func _cmd_inject_mouse_click(params: Dictionary) -> Dictionary:
360
- var position = params.get("position", Vector2.ZERO)
361
- var button = int(params.get("button", MOUSE_BUTTON_LEFT))
362
- var pressed = bool(params.get("pressed", true))
363
-
364
- if position is Array:
365
- if position.size() < 2:
366
- return {"type": "error", "message": "position array must contain [x, y]"}
367
- position = Vector2(float(position[0]), float(position[1]))
368
- elif position is Vector2:
369
- position = position
370
- else:
371
- return {"type": "error", "message": "position must be Vector2 or [x, y]"}
372
-
373
- var event = InputEventMouseButton.new()
374
- event.position = position
375
- event.global_position = position
376
- event.button_index = button
377
- event.pressed = pressed
378
- Input.parse_input_event(event)
379
-
380
- return {
381
- "type": "input_injected",
382
- "input_type": "mouse_click",
383
- "position": [position.x, position.y],
384
- "button": button,
385
- "pressed": pressed
386
- }
387
-
388
-
389
- func _cmd_inject_mouse_motion(params: Dictionary) -> Dictionary:
390
- var position = params.get("position", Vector2.ZERO)
391
- var relative = params.get("relative", Vector2.ZERO)
392
-
393
- if position is Array:
394
- if position.size() < 2:
395
- return {"type": "error", "message": "position array must contain [x, y]"}
396
- position = Vector2(float(position[0]), float(position[1]))
397
- elif position is Vector2:
398
- position = position
399
- else:
400
- return {"type": "error", "message": "position must be Vector2 or [x, y]"}
401
-
402
- if relative is Array:
403
- if relative.size() < 2:
404
- return {"type": "error", "message": "relative array must contain [x, y]"}
405
- relative = Vector2(float(relative[0]), float(relative[1]))
406
- elif relative is Vector2:
407
- relative = relative
408
- else:
409
- return {"type": "error", "message": "relative must be Vector2 or [x, y]"}
410
-
411
- var event = InputEventMouseMotion.new()
412
- event.position = position
413
- event.global_position = position
414
- event.relative = relative
415
- Input.parse_input_event(event)
416
-
417
- return {
418
- "type": "input_injected",
419
- "input_type": "mouse_motion",
420
- "position": [position.x, position.y],
421
- "relative": [relative.x, relative.y]
422
- }
423
-
424
-
425
- func _cmd_watch_signal(params: Dictionary) -> Dictionary:
426
- var node_path = params.get("path", "")
427
- var signal_name = params.get("signal", "")
428
-
429
- if node_path.is_empty() or signal_name.is_empty():
430
- return {"type": "error", "message": "Node path and signal name required"}
431
-
432
- var node = get_tree().root.get_node_or_null(node_path)
433
- if node == null:
434
- return {"type": "error", "message": "Node not found: " + node_path}
435
-
436
- if not node.has_signal(signal_name):
437
- return {"type": "error", "message": "Signal not found: " + signal_name}
438
-
439
- var key = node_path + ":" + signal_name
440
- if _watched_signals.has(key):
441
- return {"type": "error", "message": "Signal already being watched"}
442
-
443
- var callable = func(args = []):
444
- _broadcast_signal_event(node_path, signal_name, args)
445
-
446
- node.connect(signal_name, callable)
447
- _watched_signals[key] = callable
448
-
449
- return {
450
- "type": "signal_watched",
451
- "path": node_path,
452
- "signal": signal_name
453
- }
454
-
455
-
456
- func _cmd_unwatch_signal(params: Dictionary) -> Dictionary:
457
- var node_path = params.get("path", "")
458
- var signal_name = params.get("signal", "")
459
-
460
- var key = node_path + ":" + signal_name
461
- if not _watched_signals.has(key):
462
- return {"type": "error", "message": "Signal not being watched"}
463
-
464
- var node = get_tree().root.get_node_or_null(node_path)
465
- if node != null:
466
- node.disconnect(signal_name, _watched_signals[key])
467
-
468
- _watched_signals.erase(key)
469
-
470
- return {
471
- "type": "signal_unwatched",
472
- "path": node_path,
473
- "signal": signal_name
474
- }
475
-
476
-
477
- func _broadcast_signal_event(node_path: String, signal_name: String, args: Array) -> void:
478
- var event = {
479
- "type": "signal_event",
480
- "path": node_path,
481
- "signal": signal_name,
482
- "args": []
483
- }
484
- for arg in args:
485
- event["args"].append(_serialize_value(arg))
486
-
487
- for client in _clients:
488
- if client.get_status() == StreamPeerTCP.STATUS_CONNECTED:
489
- _send_response(client, event)
490
-
491
-
492
- func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_properties: bool) -> Dictionary:
493
- var result = _serialize_node(node, include_properties)
494
-
495
- if depth < max_depth:
496
- var children = []
497
- for child in node.get_children():
498
- children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
499
- result["children"] = children
500
-
501
- return result
502
-
503
-
504
- func _serialize_node(node: Node, include_properties: bool) -> Dictionary:
505
- var result = {
506
- "name": node.name,
507
- "type": node.get_class(),
508
- "path": str(node.get_path())
509
- }
510
-
511
- if node.get_script():
512
- result["script"] = node.get_script().resource_path
513
-
514
- if include_properties:
515
- result["properties"] = {}
516
- for prop in node.get_property_list():
517
- if prop["usage"] & PROPERTY_USAGE_STORAGE:
518
- var name = prop["name"]
519
- if not name.begins_with("_"):
520
- result["properties"][name] = _serialize_value(node.get(name))
521
-
522
- return result
523
-
524
-
525
- func _serialize_value(value) -> Variant:
526
- if value == null:
527
- return null
528
- elif value is Vector2:
529
- return {"_type": "Vector2", "x": value.x, "y": value.y}
530
- elif value is Vector3:
531
- return {"_type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
532
- elif value is Vector2i:
533
- return {"_type": "Vector2i", "x": value.x, "y": value.y}
534
- elif value is Vector3i:
535
- return {"_type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
536
- elif value is Color:
537
- return {"_type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
538
- elif value is Rect2:
539
- return {"_type": "Rect2", "position": _serialize_value(value.position), "size": _serialize_value(value.size)}
540
- elif value is Transform2D:
541
- return {"_type": "Transform2D", "origin": _serialize_value(value.origin), "x": _serialize_value(value.x), "y": _serialize_value(value.y)}
542
- elif value is NodePath:
543
- return {"_type": "NodePath", "path": str(value)}
544
- elif value is Resource:
545
- return {"_type": "Resource", "path": value.resource_path, "class": value.get_class()}
546
- elif value is Array:
547
- var arr = []
548
- for item in value:
549
- arr.append(_serialize_value(item))
550
- return arr
551
- elif value is Dictionary:
552
- var dict = {}
553
- for key in value:
554
- dict[str(key)] = _serialize_value(value[key])
555
- return dict
556
- elif value is Object:
557
- return {"_type": "Object", "class": value.get_class()}
558
- else:
559
- return value
560
-
561
-
562
- func _deserialize_value(value) -> Variant:
563
- if value == null:
564
- return null
565
- elif value is Dictionary:
566
- if value.has("_type"):
567
- match value["_type"]:
568
- "Vector2":
569
- return Vector2(value.get("x", 0), value.get("y", 0))
570
- "Vector3":
571
- return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
572
- "Vector2i":
573
- return Vector2i(value.get("x", 0), value.get("y", 0))
574
- "Vector3i":
575
- return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
576
- "Color":
577
- return Color(value.get("r", 0), value.get("g", 0), value.get("b", 0), value.get("a", 1))
578
- "NodePath":
579
- return NodePath(value.get("path", ""))
580
- _:
581
- return value
582
- else:
583
- var dict = {}
584
- for key in value:
585
- dict[key] = _deserialize_value(value[key])
586
- return dict
587
- elif value is Array:
588
- var arr = []
589
- for item in value:
590
- arr.append(_deserialize_value(item))
591
- return arr
592
- else:
593
- return value
594
-
595
-
596
- func _send_response(client: StreamPeerTCP, data: Dictionary) -> void:
597
- var json_str = JSON.stringify(data) + "\n"
598
- client.put_utf8_string(json_str)
599
-
600
-
601
- func _send_error(client: StreamPeerTCP, message: String) -> void:
602
- _send_response(client, {"type": "error", "message": message})
603
-
604
-
605
- func _notification(what: int) -> void:
606
- if what == NOTIFICATION_WM_CLOSE_REQUEST:
607
- _cleanup()
608
-
609
-
610
- func _cleanup() -> void:
611
- for client in _clients:
612
- client.disconnect_from_host()
613
- _clients.clear()
614
-
615
- if _server:
616
- _server.stop()
617
- _server = null
618
-
619
- print("[MCP Runtime] Cleanup complete")
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")