godot-mcp-runtime 3.0.0 → 3.1.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.
- package/README.md +44 -67
- package/dist/dispatch.js +27 -27
- package/dist/dispatch.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/scripts/godot_operations.gd +150 -115
- package/dist/scripts/mcp_bridge.gd +19 -24
- package/dist/tools/autoload-tools.d.ts +8 -8
- package/dist/tools/autoload-tools.d.ts.map +1 -1
- package/dist/tools/autoload-tools.js +15 -11
- package/dist/tools/autoload-tools.js.map +1 -1
- package/dist/tools/node-tools.d.ts.map +1 -1
- package/dist/tools/node-tools.js +105 -25
- package/dist/tools/node-tools.js.map +1 -1
- package/dist/tools/project-tools.d.ts.map +1 -1
- package/dist/tools/project-tools.js +52 -15
- package/dist/tools/project-tools.js.map +1 -1
- package/dist/tools/runtime-tools.d.ts.map +1 -1
- package/dist/tools/runtime-tools.js +214 -28
- package/dist/tools/runtime-tools.js.map +1 -1
- package/dist/tools/scene-tools.d.ts.map +1 -1
- package/dist/tools/scene-tools.js +30 -12
- package/dist/tools/scene-tools.js.map +1 -1
- package/dist/tools/validate-tools.d.ts.map +1 -1
- package/dist/tools/validate-tools.js +89 -30
- package/dist/tools/validate-tools.js.map +1 -1
- package/dist/utils/autoload-ini.d.ts +6 -0
- package/dist/utils/autoload-ini.d.ts.map +1 -1
- package/dist/utils/autoload-ini.js +13 -0
- package/dist/utils/autoload-ini.js.map +1 -1
- package/dist/utils/bridge-manager.d.ts +18 -1
- package/dist/utils/bridge-manager.d.ts.map +1 -1
- package/dist/utils/bridge-manager.js +61 -11
- package/dist/utils/bridge-manager.js.map +1 -1
- package/dist/utils/bridge-protocol.d.ts +6 -3
- package/dist/utils/bridge-protocol.d.ts.map +1 -1
- package/dist/utils/bridge-protocol.js +28 -15
- package/dist/utils/bridge-protocol.js.map +1 -1
- package/dist/utils/godot-runner.d.ts +47 -3
- package/dist/utils/godot-runner.d.ts.map +1 -1
- package/dist/utils/godot-runner.js +221 -40
- package/dist/utils/godot-runner.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +2 -1
|
@@ -10,11 +10,15 @@ func _init():
|
|
|
10
10
|
# Check for debug flag
|
|
11
11
|
debug_mode = "--debug-godot" in args
|
|
12
12
|
|
|
13
|
+
# SceneTree.quit(n) only schedules a quit for end-of-frame in Godot 4.
|
|
14
|
+
# Every quit(1) must be followed by `return` to halt the failing path,
|
|
15
|
+
# otherwise control falls through into success-print + scene save.
|
|
13
16
|
# Find the script argument and determine the positions of operation and params
|
|
14
17
|
var script_index = args.find("--script")
|
|
15
18
|
if script_index == -1:
|
|
16
19
|
log_error("Could not find --script argument")
|
|
17
20
|
quit(1)
|
|
21
|
+
return
|
|
18
22
|
|
|
19
23
|
var operation_index = script_index + 2
|
|
20
24
|
var params_index = script_index + 3
|
|
@@ -23,6 +27,7 @@ func _init():
|
|
|
23
27
|
log_error("Usage: godot --headless --script godot_operations.gd <operation> <json_params>")
|
|
24
28
|
log_error("Not enough command-line arguments provided.")
|
|
25
29
|
quit(1)
|
|
30
|
+
return
|
|
26
31
|
|
|
27
32
|
log_debug("All arguments: " + str(args))
|
|
28
33
|
|
|
@@ -42,10 +47,12 @@ func _init():
|
|
|
42
47
|
log_error("Failed to parse JSON parameters: " + params_json)
|
|
43
48
|
log_error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line()))
|
|
44
49
|
quit(1)
|
|
50
|
+
return
|
|
45
51
|
|
|
46
52
|
if not params:
|
|
47
53
|
log_error("Failed to parse JSON parameters: " + params_json)
|
|
48
54
|
quit(1)
|
|
55
|
+
return
|
|
49
56
|
|
|
50
57
|
log_info("Executing operation: " + operation)
|
|
51
58
|
|
|
@@ -90,8 +97,10 @@ func _init():
|
|
|
90
97
|
_:
|
|
91
98
|
log_error("Unknown operation: " + operation)
|
|
92
99
|
quit(1)
|
|
100
|
+
return
|
|
93
101
|
|
|
94
102
|
quit()
|
|
103
|
+
return
|
|
95
104
|
|
|
96
105
|
# Logging functions
|
|
97
106
|
func log_debug(message):
|
|
@@ -280,6 +289,7 @@ func create_scene(params):
|
|
|
280
289
|
if not scene_root:
|
|
281
290
|
log_error("Failed to instantiate node of type: " + root_node_type)
|
|
282
291
|
quit(1)
|
|
292
|
+
return
|
|
283
293
|
|
|
284
294
|
scene_root.name = "root"
|
|
285
295
|
scene_root.owner = scene_root
|
|
@@ -287,51 +297,86 @@ func create_scene(params):
|
|
|
287
297
|
if not _ensure_res_dir(full_scene_path):
|
|
288
298
|
log_error("Failed to create directory for scene: " + full_scene_path)
|
|
289
299
|
quit(1)
|
|
300
|
+
return
|
|
290
301
|
|
|
291
302
|
if save_scene_to_path(scene_root, full_scene_path):
|
|
292
303
|
print("Scene created successfully at: " + params.scene_path)
|
|
293
304
|
else:
|
|
294
305
|
log_error("Failed to create scene: " + params.scene_path)
|
|
295
306
|
quit(1)
|
|
307
|
+
return
|
|
296
308
|
|
|
297
309
|
# Add a node to an existing scene
|
|
310
|
+
# Apply an add_node mutation without saving. Shared by standalone add_node
|
|
311
|
+
# and batch_scene_operations so both paths validate identically.
|
|
312
|
+
# Returns {"ok": bool, "error": String}; error is empty on success.
|
|
313
|
+
func _apply_add_node(scene_root: Node, op: Dictionary) -> Dictionary:
|
|
314
|
+
var parent_path = "root"
|
|
315
|
+
if op.has("parent_node_path"):
|
|
316
|
+
parent_path = op.parent_node_path
|
|
317
|
+
var parent = find_node_by_path(scene_root, parent_path)
|
|
318
|
+
if not parent:
|
|
319
|
+
return {"ok": false, "error": "Parent node not found: " + parent_path}
|
|
320
|
+
if not op.has("node_type") or op.node_type == "":
|
|
321
|
+
return {"ok": false, "error": "node_type is required for add_node"}
|
|
322
|
+
if not op.has("node_name") or op.node_name == "":
|
|
323
|
+
return {"ok": false, "error": "node_name is required for add_node"}
|
|
324
|
+
var new_node = instantiate_class(op.node_type)
|
|
325
|
+
if not new_node:
|
|
326
|
+
return {"ok": false, "error": "Failed to instantiate node of type: " + op.node_type}
|
|
327
|
+
new_node.name = op.node_name
|
|
328
|
+
if op.has("properties"):
|
|
329
|
+
for property in op.properties:
|
|
330
|
+
new_node.set(property, _coerce_property_value(op.properties[property]))
|
|
331
|
+
parent.add_child(new_node)
|
|
332
|
+
new_node.owner = scene_root
|
|
333
|
+
return {"ok": true, "error": ""}
|
|
334
|
+
|
|
335
|
+
# Apply a load_sprite mutation without saving. Shared by standalone load_sprite
|
|
336
|
+
# and batch_scene_operations.
|
|
337
|
+
func _apply_load_sprite(scene_root: Node, op: Dictionary) -> Dictionary:
|
|
338
|
+
if not op.has("node_path") or op.node_path == "":
|
|
339
|
+
return {"ok": false, "error": "node_path is required for load_sprite"}
|
|
340
|
+
if not op.has("texture_path") or op.texture_path == "":
|
|
341
|
+
return {"ok": false, "error": "texture_path is required for load_sprite"}
|
|
342
|
+
var sprite_node = find_node_by_path(scene_root, op.node_path)
|
|
343
|
+
if not sprite_node:
|
|
344
|
+
return {"ok": false, "error": "Node not found: " + op.node_path}
|
|
345
|
+
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
346
|
+
return {"ok": false, "error": "Node is not a sprite-compatible type: " + sprite_node.get_class()}
|
|
347
|
+
var full_texture_path = normalize_scene_path(op.texture_path)
|
|
348
|
+
var texture = load(full_texture_path)
|
|
349
|
+
if not texture:
|
|
350
|
+
return {"ok": false, "error": "Failed to load texture: " + full_texture_path}
|
|
351
|
+
if not (texture is Texture2D):
|
|
352
|
+
return {"ok": false, "error": "Loaded resource is not a Texture2D: " + full_texture_path}
|
|
353
|
+
# A texture without a resource_path is a runtime-only object — PackedScene.pack()
|
|
354
|
+
# cannot serialize it, so the assignment would silently vanish on save.
|
|
355
|
+
if texture.resource_path == "":
|
|
356
|
+
return {"ok": false, "error": "Texture has no resource_path — likely not imported. Open project in Godot editor once, or run 'godot --headless --editor --quit' to import assets."}
|
|
357
|
+
sprite_node.texture = texture
|
|
358
|
+
return {"ok": true, "error": ""}
|
|
359
|
+
|
|
298
360
|
func add_node(params):
|
|
299
361
|
printerr("Adding node to scene: " + params.scene_path)
|
|
300
362
|
|
|
301
363
|
var scene_root = load_scene_instance(params.scene_path)
|
|
302
364
|
if not scene_root:
|
|
303
365
|
quit(1)
|
|
366
|
+
return
|
|
304
367
|
|
|
305
|
-
var
|
|
306
|
-
if
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
var parent = find_node_by_path(scene_root, parent_path)
|
|
310
|
-
if not parent:
|
|
311
|
-
log_error("Parent node not found: " + parent_path)
|
|
312
|
-
quit(1)
|
|
313
|
-
|
|
314
|
-
var new_node = instantiate_class(params.node_type)
|
|
315
|
-
if not new_node:
|
|
316
|
-
log_error("Failed to instantiate node of type: " + params.node_type)
|
|
368
|
+
var result = _apply_add_node(scene_root, params)
|
|
369
|
+
if not result.ok:
|
|
370
|
+
log_error(result.error)
|
|
317
371
|
quit(1)
|
|
318
|
-
|
|
319
|
-
new_node.name = params.node_name
|
|
320
|
-
|
|
321
|
-
if params.has("properties"):
|
|
322
|
-
var properties = params.properties
|
|
323
|
-
for property in properties:
|
|
324
|
-
log_debug("Setting property: " + property + " = " + str(properties[property]))
|
|
325
|
-
new_node.set(property, _coerce_property_value(properties[property]))
|
|
326
|
-
|
|
327
|
-
parent.add_child(new_node)
|
|
328
|
-
new_node.owner = scene_root
|
|
372
|
+
return
|
|
329
373
|
|
|
330
374
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
331
375
|
print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully")
|
|
332
376
|
else:
|
|
333
377
|
log_error("Failed to save scene after adding node")
|
|
334
378
|
quit(1)
|
|
379
|
+
return
|
|
335
380
|
|
|
336
381
|
# Load a sprite into a Sprite2D node
|
|
337
382
|
func load_sprite(params):
|
|
@@ -340,37 +385,20 @@ func load_sprite(params):
|
|
|
340
385
|
var scene_root = load_scene_instance(params.scene_path)
|
|
341
386
|
if not scene_root:
|
|
342
387
|
quit(1)
|
|
388
|
+
return
|
|
343
389
|
|
|
344
|
-
var
|
|
345
|
-
if not
|
|
346
|
-
log_error(
|
|
347
|
-
quit(1)
|
|
348
|
-
|
|
349
|
-
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
350
|
-
log_error("Node is not a sprite-compatible type: " + sprite_node.get_class())
|
|
351
|
-
quit(1)
|
|
352
|
-
|
|
353
|
-
var full_texture_path = normalize_scene_path(params.texture_path)
|
|
354
|
-
var texture = load(full_texture_path)
|
|
355
|
-
if not texture:
|
|
356
|
-
log_error("Failed to load texture: " + full_texture_path)
|
|
357
|
-
quit(1)
|
|
358
|
-
if not (texture is Texture2D):
|
|
359
|
-
log_error("Loaded resource is not a Texture2D: " + full_texture_path)
|
|
360
|
-
quit(1)
|
|
361
|
-
# A texture without a resource_path is a runtime-only object — PackedScene.pack()
|
|
362
|
-
# cannot serialize it, so the assignment would silently vanish on save.
|
|
363
|
-
if texture.resource_path == "":
|
|
364
|
-
log_error("Texture has no resource_path — likely not imported. Open project in Godot editor once, or run 'godot --headless --editor --quit' to import assets.")
|
|
390
|
+
var result = _apply_load_sprite(scene_root, params)
|
|
391
|
+
if not result.ok:
|
|
392
|
+
log_error(result.error)
|
|
365
393
|
quit(1)
|
|
366
|
-
|
|
367
|
-
sprite_node.texture = texture
|
|
394
|
+
return
|
|
368
395
|
|
|
369
396
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
370
397
|
print("Sprite loaded successfully with texture: " + params.texture_path)
|
|
371
398
|
else:
|
|
372
399
|
log_error("Failed to save scene after loading sprite")
|
|
373
400
|
quit(1)
|
|
401
|
+
return
|
|
374
402
|
|
|
375
403
|
# Export a scene as a MeshLibrary resource
|
|
376
404
|
func export_mesh_library(params):
|
|
@@ -379,6 +407,7 @@ func export_mesh_library(params):
|
|
|
379
407
|
var scene_root = load_scene_instance(params.scene_path)
|
|
380
408
|
if not scene_root:
|
|
381
409
|
quit(1)
|
|
410
|
+
return
|
|
382
411
|
|
|
383
412
|
var mesh_library = MeshLibrary.new()
|
|
384
413
|
|
|
@@ -420,6 +449,7 @@ func export_mesh_library(params):
|
|
|
420
449
|
if not _ensure_res_dir(full_output_path):
|
|
421
450
|
log_error("Failed to create directory for MeshLibrary: " + full_output_path)
|
|
422
451
|
quit(1)
|
|
452
|
+
return
|
|
423
453
|
|
|
424
454
|
var error = ResourceSaver.save(mesh_library, full_output_path)
|
|
425
455
|
if error == OK:
|
|
@@ -427,9 +457,11 @@ func export_mesh_library(params):
|
|
|
427
457
|
else:
|
|
428
458
|
log_error("Failed to save MeshLibrary: " + str(error))
|
|
429
459
|
quit(1)
|
|
460
|
+
return
|
|
430
461
|
else:
|
|
431
462
|
log_error("No valid meshes found in the scene")
|
|
432
463
|
quit(1)
|
|
464
|
+
return
|
|
433
465
|
|
|
434
466
|
# Save changes to a scene file
|
|
435
467
|
func save_scene(params):
|
|
@@ -438,6 +470,7 @@ func save_scene(params):
|
|
|
438
470
|
var scene_root = load_scene_instance(params.scene_path)
|
|
439
471
|
if not scene_root:
|
|
440
472
|
quit(1)
|
|
473
|
+
return
|
|
441
474
|
|
|
442
475
|
var save_path = params.new_path if params.has("new_path") else params.scene_path
|
|
443
476
|
|
|
@@ -446,6 +479,7 @@ func save_scene(params):
|
|
|
446
479
|
else:
|
|
447
480
|
log_error("Failed to save scene")
|
|
448
481
|
quit(1)
|
|
482
|
+
return
|
|
449
483
|
|
|
450
484
|
# ============================================
|
|
451
485
|
# NODE OPERATIONS
|
|
@@ -458,6 +492,7 @@ func delete_nodes(params):
|
|
|
458
492
|
var scene_root = load_scene_instance(params.scene_path)
|
|
459
493
|
if not scene_root:
|
|
460
494
|
quit(1)
|
|
495
|
+
return
|
|
461
496
|
|
|
462
497
|
var node_paths: Array = params.node_paths
|
|
463
498
|
var results: Array = []
|
|
@@ -526,6 +561,9 @@ func get_node_properties(params: Dictionary) -> void:
|
|
|
526
561
|
return
|
|
527
562
|
|
|
528
563
|
var results: Array = []
|
|
564
|
+
# Class-name → default-instance cache. Reused across all nodes in this call
|
|
565
|
+
# so we don't instantiate a fresh default per node when changed_only is true.
|
|
566
|
+
var defaults_cache: Dictionary = {}
|
|
529
567
|
|
|
530
568
|
for node_spec in params.nodes:
|
|
531
569
|
var node_path = node_spec.get("node_path", "")
|
|
@@ -534,9 +572,15 @@ func get_node_properties(params: Dictionary) -> void:
|
|
|
534
572
|
if node == null:
|
|
535
573
|
results.append({"nodePath": node_path, "error": "Node not found"})
|
|
536
574
|
else:
|
|
537
|
-
var props = _collect_node_properties(node, changed_only)
|
|
575
|
+
var props = _collect_node_properties(node, changed_only, defaults_cache)
|
|
538
576
|
results.append({"nodePath": node_path, "nodeType": node.get_class(), "properties": props})
|
|
539
577
|
|
|
578
|
+
# Free cached default instances; they were created via instantiate_class.
|
|
579
|
+
for klass in defaults_cache:
|
|
580
|
+
var inst = defaults_cache[klass]
|
|
581
|
+
if inst:
|
|
582
|
+
inst.free()
|
|
583
|
+
|
|
540
584
|
print(JSON.stringify({"results": results}))
|
|
541
585
|
|
|
542
586
|
# Get full hierarchical tree structure of a scene
|
|
@@ -546,6 +590,7 @@ func get_scene_tree(params):
|
|
|
546
590
|
var scene_root = load_scene_instance(params.scene_path)
|
|
547
591
|
if not scene_root:
|
|
548
592
|
quit(1)
|
|
593
|
+
return
|
|
549
594
|
|
|
550
595
|
var tree_root = scene_root
|
|
551
596
|
if params.has("parent_path") and params.parent_path:
|
|
@@ -553,6 +598,7 @@ func get_scene_tree(params):
|
|
|
553
598
|
if not tree_root:
|
|
554
599
|
log_error("Parent node not found: " + str(params.parent_path))
|
|
555
600
|
quit(1)
|
|
601
|
+
return
|
|
556
602
|
|
|
557
603
|
var max_depth = -1
|
|
558
604
|
if params.has("max_depth"):
|
|
@@ -589,22 +635,26 @@ func attach_script(params):
|
|
|
589
635
|
var scene_root = load_scene_instance(params.scene_path)
|
|
590
636
|
if not scene_root:
|
|
591
637
|
quit(1)
|
|
638
|
+
return
|
|
592
639
|
|
|
593
640
|
var node = find_node_by_path(scene_root, params.node_path)
|
|
594
641
|
if not node:
|
|
595
642
|
log_error("Node not found: " + params.node_path)
|
|
596
643
|
quit(1)
|
|
644
|
+
return
|
|
597
645
|
|
|
598
646
|
var full_script_path = normalize_scene_path(params.script_path)
|
|
599
647
|
|
|
600
648
|
if not FileAccess.file_exists(full_script_path):
|
|
601
649
|
log_error("Script file does not exist: " + full_script_path)
|
|
602
650
|
quit(1)
|
|
651
|
+
return
|
|
603
652
|
|
|
604
653
|
var script = load(full_script_path)
|
|
605
654
|
if not script:
|
|
606
655
|
log_error("Failed to load script: " + full_script_path)
|
|
607
656
|
quit(1)
|
|
657
|
+
return
|
|
608
658
|
|
|
609
659
|
node.set_script(script)
|
|
610
660
|
|
|
@@ -613,6 +663,7 @@ func attach_script(params):
|
|
|
613
663
|
else:
|
|
614
664
|
log_error("Failed to save scene after attaching script")
|
|
615
665
|
quit(1)
|
|
666
|
+
return
|
|
616
667
|
|
|
617
668
|
# ============================================
|
|
618
669
|
# SIGNAL AND DUPLICATE OPERATIONS
|
|
@@ -621,15 +672,19 @@ func attach_script(params):
|
|
|
621
672
|
# Duplicate a node and its children within a scene
|
|
622
673
|
func duplicate_node(params):
|
|
623
674
|
var scene_root = load_scene_instance(params.scene_path)
|
|
624
|
-
if not scene_root:
|
|
675
|
+
if not scene_root:
|
|
676
|
+
quit(1)
|
|
677
|
+
return
|
|
625
678
|
|
|
626
679
|
var node = find_node_by_path(scene_root, params.node_path)
|
|
627
680
|
if not node:
|
|
628
681
|
log_error("Node not found: " + params.node_path)
|
|
629
682
|
quit(1)
|
|
683
|
+
return
|
|
630
684
|
if node == scene_root:
|
|
631
685
|
log_error("Cannot duplicate the root node")
|
|
632
686
|
quit(1)
|
|
687
|
+
return
|
|
633
688
|
|
|
634
689
|
var duplicate = node.duplicate()
|
|
635
690
|
if params.has("new_name"):
|
|
@@ -643,33 +698,36 @@ func duplicate_node(params):
|
|
|
643
698
|
if not parent:
|
|
644
699
|
log_error("Target parent not found: " + params.target_parent_path)
|
|
645
700
|
quit(1)
|
|
701
|
+
return
|
|
646
702
|
|
|
647
703
|
parent.add_child(duplicate)
|
|
648
704
|
duplicate.owner = scene_root
|
|
649
|
-
#
|
|
650
|
-
|
|
651
|
-
|
|
705
|
+
# Iterative BFS to set owner on all descendants — avoids recursion depth.
|
|
706
|
+
var queue: Array = duplicate.get_children()
|
|
707
|
+
while not queue.is_empty():
|
|
708
|
+
var current = queue.pop_front()
|
|
709
|
+
current.owner = scene_root
|
|
710
|
+
queue.append_array(current.get_children())
|
|
652
711
|
|
|
653
712
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
654
713
|
print("Node duplicated successfully as '" + duplicate.name + "'")
|
|
655
714
|
else:
|
|
656
715
|
log_error("Failed to save scene after duplicating node")
|
|
657
716
|
quit(1)
|
|
658
|
-
|
|
659
|
-
func set_owner_recursive(node: Node, owner: Node):
|
|
660
|
-
node.owner = owner
|
|
661
|
-
for child in node.get_children():
|
|
662
|
-
set_owner_recursive(child, owner)
|
|
717
|
+
return
|
|
663
718
|
|
|
664
719
|
# List signals defined on a node and their current connections
|
|
665
720
|
func get_node_signals(params):
|
|
666
721
|
var scene_root = load_scene_instance(params.scene_path)
|
|
667
|
-
if not scene_root:
|
|
722
|
+
if not scene_root:
|
|
723
|
+
quit(1)
|
|
724
|
+
return
|
|
668
725
|
|
|
669
726
|
var node = find_node_by_path(scene_root, params.node_path)
|
|
670
727
|
if not node:
|
|
671
728
|
log_error("Node not found: " + params.node_path)
|
|
672
729
|
quit(1)
|
|
730
|
+
return
|
|
673
731
|
|
|
674
732
|
var signals = []
|
|
675
733
|
for sig in node.get_signal_list():
|
|
@@ -695,25 +753,31 @@ func get_node_signals(params):
|
|
|
695
753
|
# Connect a signal from one node to a method on another node
|
|
696
754
|
func connect_signal(params):
|
|
697
755
|
var scene_root = load_scene_instance(params.scene_path)
|
|
698
|
-
if not scene_root:
|
|
756
|
+
if not scene_root:
|
|
757
|
+
quit(1)
|
|
758
|
+
return
|
|
699
759
|
|
|
700
760
|
var source = find_node_by_path(scene_root, params.node_path)
|
|
701
761
|
if not source:
|
|
702
762
|
log_error("Source node not found: " + params.node_path)
|
|
703
763
|
quit(1)
|
|
764
|
+
return
|
|
704
765
|
|
|
705
766
|
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
706
767
|
if not target:
|
|
707
768
|
log_error("Target node not found: " + params.target_node_path)
|
|
708
769
|
quit(1)
|
|
770
|
+
return
|
|
709
771
|
|
|
710
772
|
if not source.has_signal(params.signal):
|
|
711
773
|
log_error("Signal does not exist: " + params.signal + " on " + source.get_class())
|
|
712
774
|
quit(1)
|
|
775
|
+
return
|
|
713
776
|
|
|
714
777
|
if not target.has_method(params.method):
|
|
715
778
|
log_error("Method does not exist: " + params.method + " on " + target.get_class())
|
|
716
779
|
quit(1)
|
|
780
|
+
return
|
|
717
781
|
|
|
718
782
|
# CONNECT_PERSIST is required for the connection to be serialized into the
|
|
719
783
|
# packed scene; without it the connection is runtime-only and disappears on save.
|
|
@@ -721,31 +785,38 @@ func connect_signal(params):
|
|
|
721
785
|
if err != OK:
|
|
722
786
|
log_error("Failed to connect signal: " + str(err))
|
|
723
787
|
quit(1)
|
|
788
|
+
return
|
|
724
789
|
|
|
725
790
|
if save_scene_to_path(scene_root, params.scene_path):
|
|
726
791
|
print("Signal '" + params.signal + "' connected from '" + params.node_path + "' to '" + params.target_node_path + "." + params.method + "'")
|
|
727
792
|
else:
|
|
728
793
|
log_error("Failed to save scene after connecting signal")
|
|
729
794
|
quit(1)
|
|
795
|
+
return
|
|
730
796
|
|
|
731
797
|
# Disconnect a signal connection between two nodes
|
|
732
798
|
func disconnect_signal(params):
|
|
733
799
|
var scene_root = load_scene_instance(params.scene_path)
|
|
734
|
-
if not scene_root:
|
|
800
|
+
if not scene_root:
|
|
801
|
+
quit(1)
|
|
802
|
+
return
|
|
735
803
|
|
|
736
804
|
var source = find_node_by_path(scene_root, params.node_path)
|
|
737
805
|
if not source:
|
|
738
806
|
log_error("Source node not found: " + params.node_path)
|
|
739
807
|
quit(1)
|
|
808
|
+
return
|
|
740
809
|
|
|
741
810
|
var target = find_node_by_path(scene_root, params.target_node_path)
|
|
742
811
|
if not target:
|
|
743
812
|
log_error("Target node not found: " + params.target_node_path)
|
|
744
813
|
quit(1)
|
|
814
|
+
return
|
|
745
815
|
|
|
746
816
|
if not source.is_connected(params.signal, Callable(target, params.method)):
|
|
747
817
|
log_error("Signal connection does not exist")
|
|
748
818
|
quit(1)
|
|
819
|
+
return
|
|
749
820
|
|
|
750
821
|
source.disconnect(params.signal, Callable(target, params.method))
|
|
751
822
|
|
|
@@ -754,6 +825,7 @@ func disconnect_signal(params):
|
|
|
754
825
|
else:
|
|
755
826
|
log_error("Failed to save scene after disconnecting signal")
|
|
756
827
|
quit(1)
|
|
828
|
+
return
|
|
757
829
|
|
|
758
830
|
# ============================================
|
|
759
831
|
# VALIDATE OPERATION
|
|
@@ -764,6 +836,7 @@ func validate_resource(params):
|
|
|
764
836
|
if not (params.has("script_path") or params.has("scene_path")):
|
|
765
837
|
log_error("validate_resource requires script_path or scene_path")
|
|
766
838
|
quit(1)
|
|
839
|
+
return
|
|
767
840
|
var result = _validate_single(params)
|
|
768
841
|
print(JSON.stringify({"valid": result.valid, "errors": result.errors}))
|
|
769
842
|
|
|
@@ -784,11 +857,20 @@ func _coerce_property_value(value):
|
|
|
784
857
|
return Color(value.r, value.g, value.b, a)
|
|
785
858
|
return value
|
|
786
859
|
|
|
787
|
-
# Helper: collect node properties into a serializable Dictionary
|
|
788
|
-
|
|
860
|
+
# Helper: collect node properties into a serializable Dictionary. When
|
|
861
|
+
# changed_only is true, compares each property against a default instance of
|
|
862
|
+
# the node's class. The defaults_cache dict is keyed by class name so the
|
|
863
|
+
# caller can reuse default instances across many nodes (caller is responsible
|
|
864
|
+
# for freeing the cache when done).
|
|
865
|
+
func _collect_node_properties(node: Node, changed_only: bool, defaults_cache: Dictionary) -> Dictionary:
|
|
789
866
|
var default_node = null
|
|
790
867
|
if changed_only:
|
|
791
|
-
|
|
868
|
+
var klass = node.get_class()
|
|
869
|
+
if defaults_cache.has(klass):
|
|
870
|
+
default_node = defaults_cache[klass]
|
|
871
|
+
else:
|
|
872
|
+
default_node = instantiate_class(klass)
|
|
873
|
+
defaults_cache[klass] = default_node
|
|
792
874
|
|
|
793
875
|
var properties = {}
|
|
794
876
|
var property_list = node.get_property_list()
|
|
@@ -823,9 +905,6 @@ func _collect_node_properties(node: Node, changed_only: bool) -> Dictionary:
|
|
|
823
905
|
else:
|
|
824
906
|
properties[prop_name] = str(value)
|
|
825
907
|
|
|
826
|
-
if default_node:
|
|
827
|
-
default_node.free()
|
|
828
|
-
|
|
829
908
|
return properties
|
|
830
909
|
|
|
831
910
|
# Helper: validate a single target dict (script_path or scene_path)
|
|
@@ -853,50 +932,6 @@ func validate_batch(params: Dictionary) -> void:
|
|
|
853
932
|
results.append(_validate_single(target))
|
|
854
933
|
print(JSON.stringify({"results": results}))
|
|
855
934
|
|
|
856
|
-
# Helper: add a node to a scene root without saving (returns error string or "")
|
|
857
|
-
func _batch_add_node(scene_root: Node, op: Dictionary) -> String:
|
|
858
|
-
var parent_path = "root"
|
|
859
|
-
if op.has("parent_node_path"):
|
|
860
|
-
parent_path = op.parent_node_path
|
|
861
|
-
var parent = find_node_by_path(scene_root, parent_path)
|
|
862
|
-
if not parent:
|
|
863
|
-
return "Parent node not found: " + parent_path
|
|
864
|
-
if not op.has("node_type") or op.node_type == "":
|
|
865
|
-
return "node_type is required for add_node"
|
|
866
|
-
if not op.has("node_name") or op.node_name == "":
|
|
867
|
-
return "node_name is required for add_node"
|
|
868
|
-
var new_node = instantiate_class(op.node_type)
|
|
869
|
-
if not new_node:
|
|
870
|
-
return "Failed to instantiate node of type: " + op.node_type
|
|
871
|
-
new_node.name = op.node_name
|
|
872
|
-
if op.has("properties"):
|
|
873
|
-
for property in op.properties:
|
|
874
|
-
new_node.set(property, _coerce_property_value(op.properties[property]))
|
|
875
|
-
parent.add_child(new_node)
|
|
876
|
-
new_node.owner = scene_root
|
|
877
|
-
return ""
|
|
878
|
-
|
|
879
|
-
# Helper: set a sprite texture without saving (returns error string or "")
|
|
880
|
-
func _batch_load_sprite(scene_root: Node, op: Dictionary) -> String:
|
|
881
|
-
if not op.has("node_path") or op.node_path == "":
|
|
882
|
-
return "node_path is required for load_sprite"
|
|
883
|
-
if not op.has("texture_path") or op.texture_path == "":
|
|
884
|
-
return "texture_path is required for load_sprite"
|
|
885
|
-
var sprite_node = find_node_by_path(scene_root, op.node_path)
|
|
886
|
-
if not sprite_node:
|
|
887
|
-
return "Node not found: " + op.node_path
|
|
888
|
-
if not (sprite_node is Sprite2D or sprite_node is Sprite3D or sprite_node is TextureRect):
|
|
889
|
-
return "Node is not sprite-compatible: " + sprite_node.get_class()
|
|
890
|
-
var texture = load(normalize_scene_path(op.texture_path))
|
|
891
|
-
if not texture:
|
|
892
|
-
return "Failed to load texture: " + op.texture_path
|
|
893
|
-
if not (texture is Texture2D):
|
|
894
|
-
return "Loaded resource is not a Texture2D: " + op.texture_path
|
|
895
|
-
if texture.resource_path == "":
|
|
896
|
-
return "Texture has no resource_path — likely not imported. Open project in Godot editor once, or run 'godot --headless --editor --quit' to import assets."
|
|
897
|
-
sprite_node.texture = texture
|
|
898
|
-
return ""
|
|
899
|
-
|
|
900
935
|
# Execute multiple scene operations in a single headless process
|
|
901
936
|
# Scenes are loaded once and cached in memory; mutations accumulate until a save op
|
|
902
937
|
func batch_scene_operations(params: Dictionary) -> void:
|
|
@@ -927,18 +962,18 @@ func batch_scene_operations(params: Dictionary) -> void:
|
|
|
927
962
|
if scene_root == null:
|
|
928
963
|
result["error"] = "scene_path required for add_node"
|
|
929
964
|
else:
|
|
930
|
-
var
|
|
931
|
-
if
|
|
932
|
-
result["error"] =
|
|
965
|
+
var apply_result = _apply_add_node(scene_root, op)
|
|
966
|
+
if not apply_result.ok:
|
|
967
|
+
result["error"] = apply_result.error
|
|
933
968
|
else:
|
|
934
969
|
result["success"] = true
|
|
935
970
|
"load_sprite":
|
|
936
971
|
if scene_root == null:
|
|
937
972
|
result["error"] = "scene_path required for load_sprite"
|
|
938
973
|
else:
|
|
939
|
-
var
|
|
940
|
-
if
|
|
941
|
-
result["error"] =
|
|
974
|
+
var apply_result = _apply_load_sprite(scene_root, op)
|
|
975
|
+
if not apply_result.ok:
|
|
976
|
+
result["error"] = apply_result.error
|
|
942
977
|
else:
|
|
943
978
|
result["success"] = true
|
|
944
979
|
"save":
|
|
@@ -6,7 +6,11 @@ extends Node
|
|
|
6
6
|
# Wire format: 4-byte big-endian length prefix + UTF-8 JSON payload.
|
|
7
7
|
# Max frame size 16 MiB; oversize frames close the offending peer.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# Port is baked into this script at inject time by BridgeManager.inject — the
|
|
10
|
+
# integer literal below is rewritten in the project copy. The 9900 here is the
|
|
11
|
+
# source-of-truth default that ships with the script so it remains runnable
|
|
12
|
+
# standalone (e.g. validate, manual debugging).
|
|
13
|
+
const PORT := 9900 # MCP_BRIDGE_PORT_BAKED
|
|
10
14
|
const MAX_FRAME_BYTES := 16 * 1024 * 1024
|
|
11
15
|
const FRAME_HEADER_BYTES := 4
|
|
12
16
|
|
|
@@ -18,31 +22,19 @@ class PeerState:
|
|
|
18
22
|
var handling: bool = false # true while a command is awaiting a response
|
|
19
23
|
|
|
20
24
|
var tcp_server: TCPServer
|
|
21
|
-
var port: int = DEFAULT_BRIDGE_PORT
|
|
22
25
|
var session_token: String = ""
|
|
23
26
|
var _peers: Array = [] # Array[PeerState]
|
|
24
|
-
|
|
25
|
-
func _resolve_port() -> int:
|
|
26
|
-
var raw := OS.get_environment("MCP_BRIDGE_PORT")
|
|
27
|
-
if raw == "":
|
|
28
|
-
return DEFAULT_BRIDGE_PORT
|
|
29
|
-
if not raw.is_valid_int():
|
|
30
|
-
return DEFAULT_BRIDGE_PORT
|
|
31
|
-
var parsed := int(raw)
|
|
32
|
-
if parsed <= 0 or parsed > 65535:
|
|
33
|
-
return DEFAULT_BRIDGE_PORT
|
|
34
|
-
return parsed
|
|
27
|
+
var _shutting_down: bool = false # One-shot: set true in shutdown(); never reset (autoload is recreated on next session)
|
|
35
28
|
|
|
36
29
|
func _ready() -> void:
|
|
37
30
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
38
31
|
session_token = OS.get_environment("MCP_SESSION_TOKEN")
|
|
39
|
-
port = _resolve_port()
|
|
40
32
|
tcp_server = TCPServer.new()
|
|
41
|
-
var err = tcp_server.listen(
|
|
33
|
+
var err = tcp_server.listen(PORT, "127.0.0.1")
|
|
42
34
|
if err != OK:
|
|
43
|
-
push_error("McpBridge: Failed to listen on port %d (error %d)" % [
|
|
35
|
+
push_error("McpBridge: Failed to listen on port %d (error %d)" % [PORT, err])
|
|
44
36
|
else:
|
|
45
|
-
print("McpBridge: Listening on TCP port %d" %
|
|
37
|
+
print("McpBridge: Listening on TCP port %d" % PORT)
|
|
46
38
|
|
|
47
39
|
if OS.get_environment("MCP_BACKGROUND") == "1":
|
|
48
40
|
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_NO_FOCUS, true)
|
|
@@ -64,15 +56,15 @@ func _process(_delta: float) -> void:
|
|
|
64
56
|
peer.stream = stream
|
|
65
57
|
_peers.append(peer)
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
# Backwards iteration so remove_at() doesn't shift entries we haven't seen
|
|
60
|
+
# yet, and avoids the O(n) cost of Array.erase() per removal.
|
|
61
|
+
var i := _peers.size()
|
|
62
|
+
while i > 0:
|
|
63
|
+
i -= 1
|
|
64
|
+
var peer = _peers[i]
|
|
69
65
|
_poll_peer(peer)
|
|
70
66
|
if peer.stream == null or peer.stream.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if not dead.is_empty():
|
|
74
|
-
for peer in dead:
|
|
75
|
-
_peers.erase(peer)
|
|
67
|
+
_peers.remove_at(i)
|
|
76
68
|
|
|
77
69
|
func _poll_peer(peer: PeerState) -> void:
|
|
78
70
|
peer.stream.poll()
|
|
@@ -591,6 +583,7 @@ func _serialize_value(value: Variant) -> Variant:
|
|
|
591
583
|
# --- Shutdown ---
|
|
592
584
|
|
|
593
585
|
func _handle_shutdown(peer: PeerState) -> void:
|
|
586
|
+
_shutting_down = true
|
|
594
587
|
_send_response(peer, {"status": "shutting_down"})
|
|
595
588
|
# Let the response flush before we tear the listener down. A new command
|
|
596
589
|
# arriving in this 2-frame window would dispatch against a peer that's
|
|
@@ -633,6 +626,8 @@ func _close_all_peers() -> void:
|
|
|
633
626
|
_peers.clear()
|
|
634
627
|
|
|
635
628
|
func _exit_tree() -> void:
|
|
629
|
+
if not _shutting_down:
|
|
630
|
+
push_warning("McpBridge: removed from tree without shutdown — bridge connection will be lost")
|
|
636
631
|
_close_all_peers()
|
|
637
632
|
if tcp_server != null:
|
|
638
633
|
tcp_server.stop()
|