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 +1 -1
- package/addon/breakpoint_mcp/operations.gd +1 -1
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/runtime_bridge.gd +243 -0
- package/dist/annotations.js +225 -0
- package/dist/capabilities.js +5 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/tools.js +193 -0
- package/dist/index.js +26 -4
- package/dist/peers.js +238 -0
- package/dist/recipes.js +65 -1
- package/dist/schemas.js +36 -0
- package/dist/secret.js +49 -0
- package/dist/tools/processes.js +25 -1
- package/dist/tools/runtime.js +467 -36
- package/dist/toolsets.js +7 -1
- package/package.json +1 -1
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. **
|
|
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.
|
|
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.
|
|
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
|
package/dist/capabilities.js
CHANGED
|
@@ -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
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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
|
|
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
|
|
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");
|