breakpoint-mcp 1.19.1 → 1.23.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  The MCP **host** for [Breakpoint MCP](https://github.com/jlivingston-Cipher/godot-breakpoint-mcp) —
4
4
  a [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
5
5
  Godot game engine to AI coding assistants across four planes: headless CLI, the live
6
- editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **276 tools
6
+ editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **289 tools
7
7
  + 6 MCP resources**, built against the stable `@modelcontextprotocol/sdk` 1.x API.
8
8
  Developed and tested with **Claude**; because MCP is an open protocol, other clients can
9
9
  connect too (currently untested — reports welcome).
@@ -7,7 +7,7 @@ extends RefCounted
7
7
  ## wrapped in the EditorUndoRedoManager so a human can Ctrl-Z anything the assistant did.
8
8
 
9
9
  const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
10
- const ADDON_VERSION := "1.7.0"
10
+ const ADDON_VERSION := "1.9.1"
11
11
 
12
12
  var _plugin: EditorPlugin
13
13
 
@@ -3,5 +3,5 @@
3
3
  name="Breakpoint MCP"
4
4
  description="Exposes the live Godot editor and the running game to an MCP host so an AI assistant can drive them: scene/node/resource CRUD with full undo/redo, project settings, ClassDB, editor + in-game screenshots, and an in-game runtime bridge (live SceneTree, property get/set, method calls, signals, input injection, Performance monitors). Pairs with the breakpoint-mcp MCP host."
5
5
  author="James Livingston"
6
- version="1.7.0"
6
+ version="1.9.1"
7
7
  script="plugin.gd"
@@ -47,6 +47,10 @@ const MONITORS := {
47
47
 
48
48
  const BridgeSecret := preload("res://addons/breakpoint_mcp/bridge_secret.gd")
49
49
  const PauseLatch := preload("res://addons/breakpoint_mcp/pause_latch.gd")
50
+ # F4: methods dispatched on the async lane (they await frames); see _handle_line / _dispatch_async.
51
+ const ASYNC_METHODS := ["runtime.step_frames"]
52
+ # F4: default salient fields captured by runtime.state_digest when no explicit list is given.
53
+ const _DIGEST_DEFAULT_FIELDS := ["position", "global_position", "rotation", "scale", "visible", "modulate"]
50
54
 
51
55
  var _server: TCPServer
52
56
  var _clients: Array = [] # Array of {peer, buf}
@@ -219,6 +223,12 @@ func _handle_line(c: Dictionary, line: String) -> void:
219
223
  held.merge(PauseLatch.held_response(method))
220
224
  _send(peer, held)
221
225
  return
226
+ # F4: async lane — methods that await frames can't use the synchronous dispatch (which sends
227
+ # its response immediately). Start a coroutine that sends the id'd response when done; the host
228
+ # correlates responses by id, so an out-of-order async response is fine.
229
+ if method in ASYNC_METHODS:
230
+ _dispatch_async(c, id, method, params)
231
+ return
222
232
  var result := _dispatch(method, params)
223
233
  var response := {"id": id}
224
234
  response.merge(result)
@@ -324,6 +334,27 @@ func _dispatch(method: String, params: Dictionary) -> Dictionary:
324
334
  return _assert_screen_text(params)
325
335
  "runtime.screenshot_diff":
326
336
  return _screenshot_diff(params)
337
+ "runtime.anim_play":
338
+ return _anim_play(params)
339
+ "runtime.anim_stop":
340
+ return _anim_stop(params)
341
+ "runtime.anim_get_state":
342
+ return _anim_get_state(params)
343
+ "runtime.node_add":
344
+ return _node_add(params)
345
+ "runtime.node_remove":
346
+ return _node_remove(params)
347
+ "runtime.time_scale":
348
+ return _time_scale(params)
349
+ "runtime.state_digest":
350
+ return _state_digest(params)
351
+ "runtime.seed_rng":
352
+ return _seed_rng(params)
353
+ "runtime.step_frames":
354
+ # Dispatched on the async lane (see _handle_line / _dispatch_async); this placeholder
355
+ # keeps the static contract check (which scans _dispatch case labels) aware of the
356
+ # method. It is not reached in normal operation.
357
+ return _err("async_only", "runtime.step_frames is dispatched on the async lane")
327
358
  _:
328
359
  return _err("unknown_method", "No such method: %s" % method)
329
360
 
@@ -767,3 +798,215 @@ func _compare_images(frame: Image, reference_img: Image, params: Dictionary, ref
767
798
  "height": h,
768
799
  "reference": reference,
769
800
  })
801
+
802
+
803
+ # ----------------------------------------------------- F8: animation ---------
804
+
805
+ ## Resolve a node and require it to be an AnimationPlayer.
806
+ func _resolve_anim_player(path: String):
807
+ var node := _resolve(path)
808
+ if node == null:
809
+ return _err("bad_path", "Node not found: %s" % path)
810
+ if not (node is AnimationPlayer):
811
+ return _err("not_animation_player", "%s is not an AnimationPlayer" % node.get_class())
812
+ return node as AnimationPlayer
813
+
814
+
815
+ func _anim_state(ap: AnimationPlayer) -> Dictionary:
816
+ var anims: Array = []
817
+ for a in ap.get_animation_list():
818
+ anims.append(String(a))
819
+ return {
820
+ "playing": ap.is_playing(),
821
+ "current_animation": ap.current_animation,
822
+ "position": ap.current_animation_position,
823
+ "length": ap.current_animation_length,
824
+ "speed_scale": ap.speed_scale,
825
+ "animations": anims,
826
+ }
827
+
828
+
829
+ func _anim_play(params: Dictionary) -> Dictionary:
830
+ var res = _resolve_anim_player(String(params.get("path", "")))
831
+ if res is Dictionary:
832
+ return res
833
+ var ap: AnimationPlayer = res
834
+ var anim := String(params.get("animation", ""))
835
+ if anim != "" and not ap.has_animation(anim):
836
+ return _err("no_animation", "AnimationPlayer has no animation '%s'" % anim)
837
+ var speed := float(params.get("custom_speed", 1.0))
838
+ var from_end := bool(params.get("from_end", false))
839
+ ap.play(anim, -1.0, speed, from_end)
840
+ return _ok({
841
+ "playing": ap.is_playing(),
842
+ "current_animation": ap.current_animation,
843
+ "speed_scale": ap.speed_scale,
844
+ })
845
+
846
+
847
+ func _anim_stop(params: Dictionary) -> Dictionary:
848
+ var res = _resolve_anim_player(String(params.get("path", "")))
849
+ if res is Dictionary:
850
+ return res
851
+ var ap: AnimationPlayer = res
852
+ # keep_state:true pauses in place; default stops. pause()/stop() with no args are
853
+ # stable across Godot 4.2–4.5 (unlike stop()'s changing keep_state parameter).
854
+ if bool(params.get("keep_state", false)):
855
+ ap.pause()
856
+ else:
857
+ ap.stop()
858
+ return _ok({
859
+ "playing": ap.is_playing(),
860
+ "current_animation": ap.current_animation,
861
+ "position": ap.current_animation_position,
862
+ })
863
+
864
+
865
+ func _anim_get_state(params: Dictionary) -> Dictionary:
866
+ var res = _resolve_anim_player(String(params.get("path", "")))
867
+ if res is Dictionary:
868
+ return res
869
+ var ap: AnimationPlayer = res
870
+ return _ok(_anim_state(ap))
871
+
872
+
873
+ # ----------------------------------------------------- F8: node lifecycle ----
874
+
875
+ func _node_add(params: Dictionary) -> Dictionary:
876
+ var parent := _resolve(String(params.get("parent", "")))
877
+ if parent == null:
878
+ return _err("bad_path", "Parent not found: %s" % params.get("parent", ""))
879
+ var child: Node = null
880
+ var scene_path := String(params.get("scene", ""))
881
+ if scene_path != "":
882
+ var ps: Variant = load(scene_path)
883
+ if ps == null or not (ps is PackedScene):
884
+ return _err("bad_scene", "Could not load PackedScene: %s" % scene_path)
885
+ child = (ps as PackedScene).instantiate()
886
+ else:
887
+ var type_name := String(params.get("type", ""))
888
+ if type_name == "":
889
+ return _err("bad_args", "Provide either 'scene' or 'type'")
890
+ if not ClassDB.class_exists(type_name) or not ClassDB.can_instantiate(type_name):
891
+ return _err("bad_type", "Cannot instantiate class: %s" % type_name)
892
+ var obj: Variant = ClassDB.instantiate(type_name)
893
+ if not (obj is Node):
894
+ return _err("not_a_node", "%s is not a Node" % type_name)
895
+ child = obj
896
+ if child == null:
897
+ return _err("instantiate_failed", "Failed to instantiate node")
898
+ var nm := String(params.get("name", ""))
899
+ if nm != "":
900
+ child.name = nm
901
+ parent.add_child(child)
902
+ return _ok({"added": true, "path": _path_of(child), "type": child.get_class()})
903
+
904
+
905
+ func _node_remove(params: Dictionary) -> Dictionary:
906
+ var node := _resolve(String(params.get("path", "")))
907
+ if node == null:
908
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
909
+ if node == _base():
910
+ return _err("cannot_remove_root", "Refusing to remove the current scene root")
911
+ var p := _path_of(node)
912
+ node.queue_free()
913
+ return _ok({"removed": true, "path": p})
914
+
915
+
916
+ # ----------------------------------------------- F4: deterministic playtesting ----
917
+
918
+ ## Async dispatch lane: run a coroutine that awaits frames, then send the id'd response. Started
919
+ ## as a bare (un-awaited) call from _handle_line so _process never blocks; it resumes on the
920
+ ## engine's frame signals and sends when complete.
921
+ func _dispatch_async(c: Dictionary, id: Variant, method: String, params: Dictionary) -> void:
922
+ var result: Dictionary
923
+ match method:
924
+ "runtime.step_frames":
925
+ result = await _step_frames(params)
926
+ _:
927
+ result = _err("async_only", "not an async method: %s" % method)
928
+ var peer: StreamPeerTCP = c.get("peer")
929
+ if peer != null and peer.get_status() == StreamPeerTCP.STATUS_CONNECTED:
930
+ var response := {"id": id}
931
+ response.merge(result)
932
+ _send(peer, response)
933
+
934
+
935
+ ## Advance the game by an exact number of frames while otherwise frozen. Toggles
936
+ ## get_tree().paused false -> (await one frame) -> true per step (the standard Godot frame-step
937
+ ## idiom); the bridge keeps servicing the socket throughout because it is PROCESS_MODE_ALWAYS.
938
+ ## Restores the caller's prior pause state when done.
939
+ func _step_frames(params: Dictionary) -> Dictionary:
940
+ var tree := get_tree()
941
+ if tree == null:
942
+ return _err("no_tree", "No scene tree")
943
+ var frames := clampi(int(params.get("frames", 1)), 1, 100000)
944
+ var kind := String(params.get("kind", "idle"))
945
+ var prev_scale := Engine.time_scale
946
+ # Advance by exactly `frames`: restore a normal time scale for the step (the caller froze via
947
+ # runtime_time_scale{scale:0}, which zeroes delta), and count real frames off the ENGINE frame
948
+ # counters. Freeze/step are driven by time_scale (delta), NOT get_tree().paused: pause is a
949
+ # per-node processing gate that desyncs from the frame counter by one frame at the step boundary
950
+ # (a node advances N-1 for N counted frames), whereas with time_scale the node processes every
951
+ # frame and the counter matches its work 1:1. time_scale is restored after.
952
+ Engine.time_scale = 1.0
953
+ if kind == "physics" or kind == "both":
954
+ var target_p := Engine.get_physics_frames() + frames
955
+ while Engine.get_physics_frames() < target_p:
956
+ await tree.physics_frame
957
+ else:
958
+ var target_i := Engine.get_process_frames() + frames
959
+ while Engine.get_process_frames() < target_i:
960
+ await tree.process_frame
961
+ Engine.time_scale = prev_scale
962
+ return _ok({"frames_advanced": frames, "frame_index": Engine.get_process_frames()})
963
+
964
+
965
+ func _time_scale(params: Dictionary) -> Dictionary:
966
+ var previous := Engine.time_scale
967
+ var scale := maxf(0.0, float(params.get("scale", 1.0)))
968
+ Engine.time_scale = scale
969
+ # scale 0 freezes the game clock (delta -> 0): delta-based motion, tweens, timers and
970
+ # animations halt. runtime_step_frames temporarily restores a normal scale to advance an exact
971
+ # number of frames. Deliberately time_scale, not get_tree().paused — pause is a per-node
972
+ # processing gate that desyncs from the frame counter by one frame at the step boundary.
973
+ return _ok({"previous": previous, "current": Engine.time_scale})
974
+
975
+
976
+ func _seed_rng(params: Dictionary) -> Dictionary:
977
+ var s := int(params.get("seed", 0))
978
+ seed(s)
979
+ return _ok({"seed": s})
980
+
981
+
982
+ ## Read a compact, stable-ordered snapshot of a subtree's salient state (read-only). Default
983
+ ## fields are the common transform/visibility properties; a caller may pass an explicit list.
984
+ func _state_digest(params: Dictionary) -> Dictionary:
985
+ var root := _resolve(String(params.get("root", "")))
986
+ if root == null:
987
+ return _err("bad_path", "Node not found: %s" % params.get("root", ""))
988
+ var raw_fields: Variant = params.get("fields", [])
989
+ var fields: Array = raw_fields if typeof(raw_fields) == TYPE_ARRAY else []
990
+ var max_depth := int(params.get("max_depth", 8))
991
+ var digest := {}
992
+ var count := _digest_walk(root, 0, max_depth, fields, digest)
993
+ return _ok({"digest": digest, "node_count": count})
994
+
995
+
996
+ func _digest_walk(node: Node, depth: int, max_depth: int, fields: Array, digest: Dictionary) -> int:
997
+ var entry := {}
998
+ if fields.is_empty():
999
+ for f in _DIGEST_DEFAULT_FIELDS:
1000
+ var dv: Variant = node.get(f)
1001
+ if dv != null:
1002
+ entry[f] = Codec.encode(dv)
1003
+ else:
1004
+ for f in fields:
1005
+ var fn := String(f)
1006
+ entry[fn] = Codec.encode(node.get(fn))
1007
+ digest[_path_of(node)] = entry
1008
+ var count := 1
1009
+ if depth < max_depth:
1010
+ for ch in node.get_children():
1011
+ count += _digest_walk(ch, depth + 1, max_depth, fields, digest)
1012
+ return count
@@ -0,0 +1,225 @@
1
+ /** Tools that mutate nothing. */
2
+ const READ_ONLY = [
3
+ "anim_get_track_keys", "anim_list", "backend_detect", "class_reference", "classdb_get_class", "cs_code_action",
4
+ "cs_completion", "cs_dbg_scopes", "cs_dbg_stack_trace", "cs_dbg_variables", "cs_definition", "cs_diagnostics",
5
+ "cs_document_symbols", "cs_hover", "cs_references", "cs_signature_help", "cs_workspace_symbols", "dbg_scopes",
6
+ "dbg_stack_trace", "dbg_variables", "docs_search", "editor_get_state", "editor_ping", "example_snippet",
7
+ "filesystem_list", "find_symbol", "find_usages", "gd_call_hierarchy", "gd_code_action", "gd_completion",
8
+ "gd_declaration", "gd_definition", "gd_diagnostics", "gd_document_color", "gd_document_highlight",
9
+ "gd_document_link", "gd_document_symbols", "gd_folding_ranges", "gd_formatting", "gd_hover", "gd_implementation",
10
+ "gd_references", "gd_semantic_tokens", "gd_signature_help", "gd_type_definition", "gd_workspace_symbols",
11
+ "godot_output", "godot_version", "inputmap_list", "node_find", "node_get_children", "node_get_path",
12
+ "node_get_property", "node_list_groups", "node_list_properties", "project_get_info", "project_get_setting",
13
+ "project_list_settings", "project_search", "resource_get_import_settings", "resource_get_property",
14
+ "resource_load", "runtime_anim_get_state", "runtime_assert_node_state", "runtime_assert_perf",
15
+ "runtime_assert_scene_structure", "runtime_assert_screen_text", "runtime_await_condition", "runtime_get_log",
16
+ "runtime_get_monitors", "runtime_get_property", "runtime_get_tree", "runtime_screenshot",
17
+ "runtime_peers_digest", "runtime_screenshot_diff", "runtime_state_digest", "scene_get_dependencies",
18
+ "scene_get_tree", "scene_list_open", "screenshot_editor", "selection_get", "signal_list",
19
+ "signal_list_connections", "test_detect", "test_list", "tilemap_get_cell", "vcs_blame", "vcs_branch_list",
20
+ "vcs_diff", "vcs_log", "vcs_show", "vcs_status",
21
+ ];
22
+ /** Tools that may overwrite or discard state the caller did not supply. */
23
+ const DESTRUCTIVE = [
24
+ "anim_delete", "anim_remove_key", "asset_gen_audio_sfx", "asset_gen_icon", "asset_gen_model",
25
+ "asset_gen_placeholder", "asset_gen_sprite", "asset_gen_texture", "audio_bus_set_volume", "audio_set_bus_layout",
26
+ "auth_scaffold", "backend_configure", "board_create", "board_tile_create", "card_template_create",
27
+ "cloudsave_scaffold", "cs_dbg_evaluate", "cs_dbg_set_variable", "cs_rename", "dbg_evaluate", "dbg_goto",
28
+ "dbg_set_variable", "editorsettings_get_set", "environment_create", "environment_set_sky", "filesystem_move",
29
+ "gd_rename", "godot_run_headless_script", "godot_stop", "inputmap_add_action", "inputmap_erase_action",
30
+ "interact_add_drop_zone", "interact_make_draggable", "leaderboard_scaffold", "mp_scaffold_lobby",
31
+ "mp_setup_enet_peer", "mp_setup_webrtc_peer", "mp_wire_rpc", "node_call_method", "node_change_type",
32
+ "node_delete", "node_remove_from_group", "physics_set_gravity", "piece_template_create", "primitive_mesh_create",
33
+ "project_add_autoload", "project_remove_autoload", "project_set_main_scene", "project_set_setting",
34
+ "resource_create", "resource_duplicate", "resource_save", "resource_set_import_settings",
35
+ "resource_set_property", "runtime_anim_play", "runtime_anim_stop", "runtime_call_method", "runtime_emit_signal",
36
+ "runtime_inject_input", "runtime_node_remove", "runtime_peer_stop", "runtime_seed_rng", "runtime_set_property",
37
+ "runtime_step_frames", "runtime_time_scale", "scene_close", "scene_new", "scene_pack", "scene_reload", "scene_save_as", "shader_create",
38
+ "shader_set_code", "shadermaterial_create", "shadermaterial_set_param", "shadermaterial_set_shader",
39
+ "signal_disconnect", "signal_emit", "theme_create", "theme_set_color", "theme_set_constant", "theme_set_font",
40
+ "theme_set_stylebox", "tilemap_clear", "tilemap_set_cell", "tilemap_set_cells_rect", "tileset_create",
41
+ "vcs_restore", "vcs_stash",
42
+ ];
43
+ /** Tools where a repeat call with identical arguments adds no further effect. */
44
+ const IDEMPOTENT = [
45
+ "anim_create", "anim_delete", "anim_get_track_keys", "anim_insert_key", "anim_list", "anim_set_length",
46
+ "anim_set_loop", "anim_statemachine_add_state", "anim_statemachine_add_transition", "anim_tree_add_node",
47
+ "area_set_gravity", "area_set_monitoring", "asset_gen_configure", "asset_gen_placeholder",
48
+ "audio_bus_set_volume", "audio_set_bus_layout", "audio_set_stream", "auth_scaffold", "backend_configure",
49
+ "backend_detect", "board_create", "board_place", "board_tile_create", "board_tile_place",
50
+ "body_set_collision_layer", "body_set_collision_mask", "body_set_physics_material", "card_template_create",
51
+ "class_reference", "classdb_get_class", "cloudsave_scaffold", "control_set_anchors", "control_set_layout_preset",
52
+ "control_set_size_flags", "control_set_theme", "cs_code_action", "cs_completion", "cs_dbg_attach",
53
+ "cs_dbg_scopes", "cs_dbg_set_breakpoints", "cs_dbg_set_exception_breakpoints", "cs_dbg_set_variable",
54
+ "cs_dbg_stack_trace", "cs_dbg_variables", "cs_dbg_watch", "cs_definition", "cs_diagnostics",
55
+ "cs_document_symbols", "cs_hover", "cs_references", "cs_rename", "cs_signature_help", "cs_workspace_symbols",
56
+ "dbg_attach", "dbg_data_breakpoints", "dbg_goto", "dbg_scopes", "dbg_set_breakpoints",
57
+ "dbg_set_exception_breakpoints", "dbg_set_variable", "dbg_stack_trace", "dbg_variables", "dbg_watch",
58
+ "docs_search", "editor_get_state", "editor_ping", "editorsettings_get_set", "environment_create",
59
+ "environment_set_sky", "example_snippet", "filesystem_create_dir", "filesystem_list", "filesystem_move",
60
+ "filesystem_scan", "find_symbol", "find_usages", "gd_call_hierarchy", "gd_code_action", "gd_completion",
61
+ "gd_declaration", "gd_definition", "gd_diagnostics", "gd_document_color", "gd_document_highlight",
62
+ "gd_document_link", "gd_document_symbols", "gd_folding_ranges", "gd_formatting", "gd_hover", "gd_implementation",
63
+ "gd_references", "gd_rename", "gd_semantic_tokens", "gd_signature_help", "gd_type_definition",
64
+ "gd_workspace_symbols", "godot_export", "godot_import", "godot_output", "godot_stop", "godot_version",
65
+ "inputmap_add_action", "inputmap_erase_action", "inputmap_list", "joint_set_bodies", "leaderboard_scaffold",
66
+ "mesh_set_surface_material", "mp_scaffold_lobby", "mp_set_authority", "mp_setup_enet_peer",
67
+ "mp_setup_webrtc_peer", "mp_wire_rpc", "node_add_to_group", "node_change_type", "node_delete", "node_find",
68
+ "node_get_children", "node_get_path", "node_get_property", "node_list_groups", "node_list_properties",
69
+ "node_move_child", "node_remove_from_group", "node_rename", "node_reparent", "node_set_editable_instance",
70
+ "node_set_owner", "node_set_property", "particles_set_amount", "particles_set_emitting",
71
+ "particles_set_lifetime", "particles_set_process_material", "particles_set_texture", "physics_set_gravity",
72
+ "piece_template_create", "primitive_mesh_create", "project_add_autoload", "project_get_info",
73
+ "project_get_setting", "project_list_settings", "project_remove_autoload", "project_search",
74
+ "project_set_main_scene", "project_set_setting", "resource_create", "resource_duplicate",
75
+ "resource_get_import_settings", "resource_get_property", "resource_load", "resource_save",
76
+ "resource_set_import_settings", "resource_set_property", "rigidbody_set_properties", "runtime_anim_get_state",
77
+ "runtime_anim_stop", "runtime_assert_node_state", "runtime_assert_perf", "runtime_assert_scene_structure",
78
+ "runtime_assert_screen_text", "runtime_await_condition", "runtime_get_log", "runtime_get_monitors",
79
+ "runtime_get_property", "runtime_get_tree", "runtime_node_remove", "runtime_peer_stop", "runtime_peers_digest",
80
+ "runtime_screenshot", "runtime_screenshot_diff", "runtime_seed_rng", "runtime_set_property",
81
+ "runtime_state_digest", "runtime_time_scale", "scene_get_dependencies", "scene_get_tree", "scene_list_open", "scene_new", "scene_open",
82
+ "scene_pack", "scene_reload", "scene_save", "scene_save_as", "screenshot_editor", "selection_get",
83
+ "selection_set", "shader_create", "shader_set_code", "shadermaterial_create", "shadermaterial_set_param",
84
+ "shadermaterial_set_shader", "signal_add_user_signal", "signal_connect", "signal_disconnect", "signal_list",
85
+ "signal_list_connections", "test_detect", "test_list", "theme_create", "theme_set_color", "theme_set_constant",
86
+ "theme_set_font", "theme_set_stylebox", "tilemap_clear", "tilemap_get_cell", "tilemap_set_cell",
87
+ "tilemap_set_cells_rect", "tileset_add_tile", "tileset_create", "vcs_add", "vcs_blame", "vcs_branch_list",
88
+ "vcs_diff", "vcs_log", "vcs_restore", "vcs_show", "vcs_status", "vcs_switch",
89
+ ];
90
+ /**
91
+ * Tools that reach outside this machine + this project. Empty by design — every
92
+ * bridge is loopback-only. A future networked tool must be listed here.
93
+ */
94
+ const OPEN_WORLD = [];
95
+ /**
96
+ * The complete annotated roster — every registered tool, listed explicitly.
97
+ *
98
+ * This CANNOT be derived as the union of the four lists above: 52 tools are
99
+ * all-false (mutating, non-destructive, non-idempotent, local — e.g. `node_add`,
100
+ * `dbg_step`, `vcs_commit`), so a derived union would silently omit them and the
101
+ * totality check below would pass while they shipped unannotated. Listing the
102
+ * roster explicitly is what makes "every tool is annotated" an assertion rather
103
+ * than a tautology.
104
+ */
105
+ export const ALL_ANNOTATED = [
106
+ "anim_add_track", "anim_create", "anim_delete", "anim_get_track_keys", "anim_insert_key", "anim_list",
107
+ "anim_player_create", "anim_remove_key", "anim_set_length", "anim_set_loop", "anim_statemachine_add_state",
108
+ "anim_statemachine_add_transition", "anim_tree_add_node", "anim_tree_create", "area_set_gravity",
109
+ "area_set_monitoring", "asset_gen_audio_sfx", "asset_gen_configure", "asset_gen_icon", "asset_gen_model",
110
+ "asset_gen_placeholder", "asset_gen_sprite", "asset_gen_texture", "audio_bus_add", "audio_bus_add_effect",
111
+ "audio_bus_set_volume", "audio_player_create", "audio_set_bus_layout", "audio_set_stream", "auth_scaffold",
112
+ "backend_configure", "backend_detect", "board_create", "board_place", "board_tile_create", "board_tile_place",
113
+ "body_create", "body_set_collision_layer", "body_set_collision_mask", "body_set_physics_material",
114
+ "camera_create", "card_deck_from_table", "card_hand_layout", "card_instance", "card_set_face",
115
+ "card_template_create", "class_reference", "classdb_get_class", "cloudsave_scaffold", "collisionpolygon_add",
116
+ "collisionshape_add", "container_add_child", "control_create", "control_set_anchors",
117
+ "control_set_layout_preset", "control_set_size_flags", "control_set_theme", "cs_code_action", "cs_completion",
118
+ "cs_dbg_attach", "cs_dbg_continue", "cs_dbg_evaluate", "cs_dbg_launch", "cs_dbg_restart", "cs_dbg_scopes",
119
+ "cs_dbg_set_breakpoints", "cs_dbg_set_exception_breakpoints", "cs_dbg_set_variable", "cs_dbg_stack_trace",
120
+ "cs_dbg_step", "cs_dbg_variables", "cs_dbg_watch", "cs_definition", "cs_diagnostics", "cs_document_symbols",
121
+ "cs_hover", "cs_references", "cs_rename", "cs_signature_help", "cs_workspace_symbols", "csg_create",
122
+ "dbg_attach", "dbg_continue", "dbg_data_breakpoints", "dbg_evaluate", "dbg_goto", "dbg_launch", "dbg_restart",
123
+ "dbg_scopes", "dbg_set_breakpoints", "dbg_set_exception_breakpoints", "dbg_set_variable", "dbg_stack_trace",
124
+ "dbg_step", "dbg_variables", "dbg_watch", "docs_search", "editor_get_state", "editor_ping", "editor_redo",
125
+ "editor_undo", "editorsettings_get_set", "environment_create", "environment_set_sky", "example_snippet",
126
+ "filesystem_create_dir", "filesystem_list", "filesystem_move", "filesystem_scan", "find_symbol", "find_usages",
127
+ "gd_call_hierarchy", "gd_code_action", "gd_completion", "gd_declaration", "gd_definition", "gd_diagnostics",
128
+ "gd_document_color", "gd_document_highlight", "gd_document_link", "gd_document_symbols", "gd_folding_ranges",
129
+ "gd_formatting", "gd_hover", "gd_implementation", "gd_references", "gd_rename", "gd_semantic_tokens",
130
+ "gd_signature_help", "gd_type_definition", "gd_workspace_symbols", "godot_export", "godot_import",
131
+ "godot_launch_editor", "godot_output", "godot_run_headless_script", "godot_run_managed", "godot_run_project",
132
+ "godot_stop", "godot_version", "inputmap_add_action", "inputmap_add_event", "inputmap_erase_action",
133
+ "inputmap_list", "interact_add_drop_zone", "interact_make_draggable", "joint_create", "joint_set_bodies",
134
+ "leaderboard_scaffold", "light_create", "mesh_set_surface_material", "meshinstance_create", "mp_add_spawner",
135
+ "mp_add_synchronizer", "mp_scaffold_lobby", "mp_set_authority", "mp_setup_enet_peer", "mp_setup_webrtc_peer",
136
+ "mp_wire_rpc", "navagent_configure", "navregion_create", "node_add", "node_add_to_group", "node_call_method",
137
+ "node_change_type", "node_delete", "node_duplicate", "node_find", "node_get_children", "node_get_path",
138
+ "node_get_property", "node_instantiate_scene", "node_list_groups", "node_list_properties", "node_move_child",
139
+ "node_remove_from_group", "node_rename", "node_reparent", "node_set_editable_instance", "node_set_owner",
140
+ "node_set_property", "particles_create", "particles_set_amount", "particles_set_emitting",
141
+ "particles_set_lifetime", "particles_set_process_material", "particles_set_texture", "physics_set_gravity",
142
+ "piece_instance", "piece_move", "piece_template_create", "primitive_mesh_create", "project_add_autoload",
143
+ "project_add_export_preset", "project_get_info", "project_get_setting", "project_list_settings",
144
+ "project_remove_autoload", "project_search", "project_set_main_scene", "project_set_setting", "resource_create",
145
+ "resource_duplicate", "resource_get_import_settings", "resource_get_property", "resource_load", "resource_save",
146
+ "resource_set_import_settings", "resource_set_property", "rigidbody_set_properties", "runtime_anim_get_state",
147
+ "runtime_anim_play", "runtime_anim_stop", "runtime_assert_node_state", "runtime_assert_perf",
148
+ "runtime_assert_scene_structure", "runtime_assert_screen_text", "runtime_await_condition", "runtime_call_method",
149
+ "runtime_emit_signal", "runtime_get_log", "runtime_get_monitors", "runtime_get_property", "runtime_get_tree",
150
+ "runtime_inject_input", "runtime_node_add", "runtime_node_remove", "runtime_peer_stop", "runtime_peers_digest",
151
+ "runtime_screenshot", "runtime_screenshot_diff", "runtime_seed_rng", "runtime_set_property",
152
+ "runtime_spawn_peers", "runtime_state_digest", "runtime_step_frames", "runtime_time_scale", "scene_close",
153
+ "scene_get_dependencies", "scene_get_tree",
154
+ "scene_list_open", "scene_new", "scene_open", "scene_pack", "scene_reload", "scene_save", "scene_save_as",
155
+ "screenshot_editor", "selection_get", "selection_set", "shader_create", "shader_set_code",
156
+ "shadermaterial_create", "shadermaterial_set_param", "shadermaterial_set_shader", "signal_add_user_signal",
157
+ "signal_connect", "signal_disconnect", "signal_emit", "signal_list", "signal_list_connections", "test_detect",
158
+ "test_list", "theme_create", "theme_set_color", "theme_set_constant", "theme_set_font", "theme_set_stylebox",
159
+ "tilemap_clear", "tilemap_get_cell", "tilemap_set_cell", "tilemap_set_cells_rect", "tilemaplayer_create",
160
+ "tileset_add_source", "tileset_add_tile", "tileset_create", "tileset_set_tile_collision", "vcs_add", "vcs_blame",
161
+ "vcs_branch_create", "vcs_branch_list", "vcs_commit", "vcs_diff", "vcs_log", "vcs_restore", "vcs_show",
162
+ "vcs_stash", "vcs_status", "vcs_switch",
163
+ ];
164
+ const readOnly = new Set(READ_ONLY);
165
+ const destructive = new Set(DESTRUCTIVE);
166
+ const idempotent = new Set(IDEMPOTENT);
167
+ const openWorld = new Set(OPEN_WORLD);
168
+ /**
169
+ * Guard the one way this table can rot silently: a name typo'd into READ_ONLY /
170
+ * DESTRUCTIVE / IDEMPOTENT that is not on the roster would otherwise annotate
171
+ * nothing and fail no test. Throwing at module load turns that into an
172
+ * immediate, obvious failure in every consumer, tests included.
173
+ */
174
+ {
175
+ const roster = new Set(ALL_ANNOTATED);
176
+ const orphans = [...new Set([...READ_ONLY, ...DESTRUCTIVE, ...IDEMPOTENT, ...OPEN_WORLD])]
177
+ .filter((n) => !roster.has(n))
178
+ .sort();
179
+ if (orphans.length) {
180
+ throw new Error(`annotations.ts: name(s) in a hint list but absent from ALL_ANNOTATED: ${orphans.join(", ")}`);
181
+ }
182
+ }
183
+ /** Every tool name carrying an annotation entry — the totality set for the tests. */
184
+ export const ANNOTATED_TOOLS = Object.freeze([...ALL_ANNOTATED].sort());
185
+ /**
186
+ * The four hints for one tool. Every registered tool resolves to a complete
187
+ * block: absence from a list is a positive "false", never "unknown", so a
188
+ * consumer never has to fall back to guessing from the name.
189
+ */
190
+ export function annotationsFor(name) {
191
+ return {
192
+ readOnlyHint: readOnly.has(name),
193
+ destructiveHint: destructive.has(name),
194
+ idempotentHint: idempotent.has(name),
195
+ openWorldHint: openWorld.has(name),
196
+ };
197
+ }
198
+ /**
199
+ * Wrap `server.registerTool` to inject the annotations block at registration.
200
+ * Mirrors `applyOutputSchemas` / `applyCapabilities`; call once, after
201
+ * `applyOutputSchemas` and before any `register*Tools()`. An explicit
202
+ * `annotations` already on the config wins, so a call site can override.
203
+ *
204
+ * `title` is left alone — the SDK already promotes the top-level `title` to the
205
+ * annotation of the same name, and duplicating it here would fight that.
206
+ */
207
+ export function applyAnnotations(server) {
208
+ const inject = (raw) => (name, config, handler) => {
209
+ const cfg = config;
210
+ if (cfg && cfg.annotations === undefined) {
211
+ config = { ...cfg, annotations: annotationsFor(name) };
212
+ }
213
+ return raw(name, config, handler);
214
+ };
215
+ const s = server;
216
+ s.registerTool = inject(s.registerTool.bind(server));
217
+ // D2 task-model tools (godot_export / godot_import / godot_run_headless_script)
218
+ // register through experimental.tasks.registerToolTask, not registerTool —
219
+ // annotate that path too or three tools ship hint-less.
220
+ const tasks = s.experimental?.tasks;
221
+ if (tasks?.registerToolTask) {
222
+ tasks.registerToolTask = inject(tasks.registerToolTask.bind(tasks));
223
+ }
224
+ }
225
+ //# sourceMappingURL=annotations.js.map
@@ -2,7 +2,7 @@
2
2
  export const CAPABILITY_GROUPS = ["code-execution", "network"];
3
3
  /** One-line human description per group (shown by `doctor` and the resource). */
4
4
  export const GROUP_DESCRIBE = {
5
- "code-execution": "Run arbitrary GDScript, invoke arbitrary methods, evaluate an expression in a paused debug frame, or run a local asset-gen command backend.",
5
+ "code-execution": "Run arbitrary GDScript, invoke arbitrary methods, evaluate an expression in a paused debug frame, spawn headless child processes of the project, or run a local asset-gen command backend.",
6
6
  network: "Egress beyond loopback — the Group M backend SDK.",
7
7
  };
8
8
  /**
@@ -16,6 +16,10 @@ export const TOOL_CAPABILITIES = {
16
16
  godot_run_managed: ["code-execution"],
17
17
  node_call_method: ["code-execution"],
18
18
  runtime_call_method: ["code-execution"],
19
+ // F6: spawns headless Godot child processes of the project — the same class of
20
+ // trust as godot_run_managed, which is why it is privileged rather than merely
21
+ // confirmation-gated.
22
+ runtime_spawn_peers: ["code-execution"],
19
23
  dbg_evaluate: ["code-execution"],
20
24
  cs_dbg_evaluate: ["code-execution"],
21
25
  // asset generation — the local command backend (code-execution) is the only
@@ -193,7 +193,7 @@ export function checkCapabilities(config) {
193
193
  status: "ok",
194
194
  severity: "info",
195
195
  detail: dropped.length === 0
196
- ? `${state} — full 276-tool surface`
196
+ ? `${state} — full 289-tool surface`
197
197
  : `${state} (secure default) — ${dropped.length} higher-trust tool(s) dropped from the surface`,
198
198
  hint: dropped.length === 0
199
199
  ? undefined
package/dist/cli/init.js CHANGED
@@ -279,7 +279,7 @@ export async function runInit(argv, deps = {}) {
279
279
  // Trust groups — surface the secure default so even a bare `init` teaches it.
280
280
  say("");
281
281
  if (privilegedGroups) {
282
- say(`Higher-trust tool groups enabled: ${privilegedGroups} — the full 276-tool surface loads.`);
282
+ say(`Higher-trust tool groups enabled: ${privilegedGroups} — the full 289-tool surface loads.`);
283
283
  }
284
284
  else {
285
285
  say("Higher-trust tool groups are OFF by default (the secure default). The assistant loads the");