godot-mcp-runtime 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +55 -67
  2. package/dist/dispatch.js +27 -27
  3. package/dist/dispatch.js.map +1 -1
  4. package/dist/index.js +5 -5
  5. package/dist/index.js.map +1 -1
  6. package/dist/scripts/godot_operations.gd +150 -115
  7. package/dist/scripts/mcp_bridge.gd +19 -24
  8. package/dist/tools/autoload-tools.d.ts +8 -8
  9. package/dist/tools/autoload-tools.d.ts.map +1 -1
  10. package/dist/tools/autoload-tools.js +15 -11
  11. package/dist/tools/autoload-tools.js.map +1 -1
  12. package/dist/tools/node-tools.d.ts.map +1 -1
  13. package/dist/tools/node-tools.js +105 -25
  14. package/dist/tools/node-tools.js.map +1 -1
  15. package/dist/tools/project-tools.d.ts.map +1 -1
  16. package/dist/tools/project-tools.js +52 -15
  17. package/dist/tools/project-tools.js.map +1 -1
  18. package/dist/tools/runtime-tools.d.ts.map +1 -1
  19. package/dist/tools/runtime-tools.js +224 -28
  20. package/dist/tools/runtime-tools.js.map +1 -1
  21. package/dist/tools/scene-tools.d.ts.map +1 -1
  22. package/dist/tools/scene-tools.js +30 -12
  23. package/dist/tools/scene-tools.js.map +1 -1
  24. package/dist/tools/validate-tools.d.ts.map +1 -1
  25. package/dist/tools/validate-tools.js +89 -30
  26. package/dist/tools/validate-tools.js.map +1 -1
  27. package/dist/utils/autoload-ini.d.ts +6 -0
  28. package/dist/utils/autoload-ini.d.ts.map +1 -1
  29. package/dist/utils/autoload-ini.js +13 -0
  30. package/dist/utils/autoload-ini.js.map +1 -1
  31. package/dist/utils/bridge-manager.d.ts +18 -1
  32. package/dist/utils/bridge-manager.d.ts.map +1 -1
  33. package/dist/utils/bridge-manager.js +61 -11
  34. package/dist/utils/bridge-manager.js.map +1 -1
  35. package/dist/utils/bridge-protocol.d.ts +6 -3
  36. package/dist/utils/bridge-protocol.d.ts.map +1 -1
  37. package/dist/utils/bridge-protocol.js +28 -15
  38. package/dist/utils/bridge-protocol.js.map +1 -1
  39. package/dist/utils/godot-runner.d.ts +47 -3
  40. package/dist/utils/godot-runner.d.ts.map +1 -1
  41. package/dist/utils/godot-runner.js +239 -56
  42. package/dist/utils/godot-runner.js.map +1 -1
  43. package/dist/utils/logger.d.ts +1 -0
  44. package/dist/utils/logger.d.ts.map +1 -1
  45. package/dist/utils/logger.js +1 -1
  46. package/dist/utils/logger.js.map +1 -1
  47. 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 parent_path = "root"
306
- if params.has("parent_node_path"):
307
- parent_path = params.parent_node_path
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 sprite_node = find_node_by_path(scene_root, params.node_path)
345
- if not sprite_node:
346
- log_error("Node not found: " + params.node_path)
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: quit(1)
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
- # Recursively set owner on all descendants
650
- for child in duplicate.get_children():
651
- set_owner_recursive(child, scene_root)
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: quit(1)
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: quit(1)
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: quit(1)
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
- func _collect_node_properties(node: Node, changed_only: bool) -> Dictionary:
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
- default_node = instantiate_class(node.get_class())
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 err = _batch_add_node(scene_root, op)
931
- if err != "":
932
- result["error"] = err
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 err = _batch_load_sprite(scene_root, op)
940
- if err != "":
941
- result["error"] = err
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
- const DEFAULT_BRIDGE_PORT := 9900
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(port, "127.0.0.1")
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)" % [port, err])
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" % port)
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
- var dead: Array = []
68
- for peer in _peers:
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
- dead.append(peer)
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()