breakpoint-mcp 1.81.0 → 1.82.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.
- package/addon/breakpoint_mcp/error_remedies.gd +6 -0
- package/addon/breakpoint_mcp/operations.gd +73 -10
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/runtime_bridge.gd +117 -13
- package/addon/breakpoint_mcp/variant_json.gd +90 -0
- package/dist/dap.js +7 -1
- package/dist/index.js +9 -1
- package/dist/schemas.js +32 -2
- package/dist/tools/dap.js +18 -9
- package/dist/tools/editor/animation.js +2 -1
- package/dist/tools/editor/core.js +2 -1
- package/dist/tools/editor/node.js +2 -1
- package/dist/tools/editor/resource.js +2 -1
- package/dist/tools/editor/shader.js +2 -1
- package/dist/tools/runtime.js +3 -2
- package/package.json +1 -1
|
@@ -54,6 +54,8 @@ const EDITOR_REMEDIES := {
|
|
|
54
54
|
"create_failed": "Check the parent and the name in the message, then call again.",
|
|
55
55
|
"mkdir_failed": "Check the parent directory is inside the project and writable; `filesystem_create_dir` creates intermediate directories, and the number is a Godot Error code.",
|
|
56
56
|
"bad_source": "Create an atlas source first with `tileset_add_source`; the id above is not one.",
|
|
57
|
+
"set_mismatch": "Send the value tagged — `{\"__type__\": \"Vector2\", \"x\": .., \"y\": ..}` — then call again; `node_get_property` reports the type the property currently holds.",
|
|
58
|
+
"set_ignored": "Check the property exists on this node with `node_get_property`, and that its setter accepts the value — nothing changed.",
|
|
57
59
|
"unknown_method": "Re-run `breakpoint-mcp init --force`, then reopen the project — the addon installed here is older than the host and plain `init` skips an addon that is already present.",
|
|
58
60
|
"bad_request": "Send the request again with the field the message names — it was empty or unparseable.",
|
|
59
61
|
"no_viewport": "Switch the editor to the tab that owns that viewport with `main_screen_set`, then call again.",
|
|
@@ -100,6 +102,10 @@ const RUNTIME_REMEDIES := {
|
|
|
100
102
|
"instantiate_failed": "Check the class or scene loads in the editor first — it would not instantiate in the running game.",
|
|
101
103
|
"cannot_remove_root": "Call `runtime_node_remove` on a child instead, or `godot_stop` to end the run — the running scene root cannot be removed.",
|
|
102
104
|
"no_tree": "Call `godot_run_project` and wait for it to bind, then call again — the game is not running.",
|
|
105
|
+
"set_mismatch": "Send the value tagged — `{\"__type__\": \"Vector2\", \"x\": .., \"y\": ..}` — then call again; `runtime_get_property` reports the type the property currently holds.",
|
|
106
|
+
"set_ignored": "Check the property exists on this node with `runtime_get_property`, and that its setter accepts the value — nothing changed.",
|
|
107
|
+
"bad_event_field": "Send the field tagged — `{\"__type__\": \"Vector2\", \"x\": .., \"y\": ..}` — or omit it and let the event land at its default position.",
|
|
108
|
+
"bad_params": "Fix the parameter named above and call again — the message carries the accepted shape.",
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
|
|
@@ -8,7 +8,7 @@ extends RefCounted
|
|
|
8
8
|
|
|
9
9
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
10
10
|
const Remedies := preload("res://addons/breakpoint_mcp/error_remedies.gd")
|
|
11
|
-
const ADDON_VERSION := "1.
|
|
11
|
+
const ADDON_VERSION := "1.12.0"
|
|
12
12
|
|
|
13
13
|
var _plugin: EditorPlugin
|
|
14
14
|
|
|
@@ -648,14 +648,77 @@ func _node_set_property(params: Dictionary) -> Dictionary:
|
|
|
648
648
|
var prop := String(params.get("property", ""))
|
|
649
649
|
if prop == "":
|
|
650
650
|
return _err("bad_params", "Missing 'property'")
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
651
|
+
# 🔴 SAME FAMILY AS THE RUNTIME PLANE'S `_set_property` (issue #327) — an absent
|
|
652
|
+
# `value` is not a null one, and letting it through wrote the property type's zero
|
|
653
|
+
# over whatever was there, under a success envelope. See runtime_bridge.gd for the
|
|
654
|
+
# measurement; this plane reached it by the same route and through UndoRedo, so the
|
|
655
|
+
# wipe landed in the undo history looking like a deliberate edit.
|
|
656
|
+
if not params.has("value"):
|
|
657
|
+
return _err(
|
|
658
|
+
"bad_params",
|
|
659
|
+
"Missing 'value'. Pass it explicitly — an absent value is not the same request as a null one, and writing null here would set %s to its type's zero." % prop
|
|
660
|
+
)
|
|
661
|
+
var old_value: Variant = Codec.read_property(node, prop)
|
|
662
|
+
var new_value: Variant = Codec.decode(params["value"])
|
|
663
|
+
# 🔴 TRIAL FIRST, HISTORY ONLY IF IT LANDS. The engine coerces at the moment of the
|
|
664
|
+
# write, so the outcome cannot be known before it happens — and a refused write must
|
|
665
|
+
# leave neither a changed property NOR an undo entry. Committing first and undoing
|
|
666
|
+
# afterwards would put a "Breakpoint: set .." action in the editor's history for an
|
|
667
|
+
# edit that never took, which is a worse artifact than the one being fixed.
|
|
668
|
+
Codec.write_property(node, prop, new_value)
|
|
669
|
+
var after: Variant = Codec.read_property(node, prop)
|
|
670
|
+
var outcome := _set_outcome(new_value, old_value, after)
|
|
671
|
+
Codec.write_property(node, prop, old_value)
|
|
672
|
+
if outcome == "applied" or outcome == "coerced":
|
|
673
|
+
var ur := _plugin.get_undo_redo()
|
|
674
|
+
ur.create_action("Breakpoint: set %s.%s" % [node.name, prop])
|
|
675
|
+
if Codec.is_indexed(prop):
|
|
676
|
+
# UndoRedo's property methods take a property NAME; a sub-property has to go
|
|
677
|
+
# through the indexed setter as a method call, or the action records an edit
|
|
678
|
+
# to a property called "position:x" that no node has.
|
|
679
|
+
ur.add_do_method(node, "set_indexed", NodePath(prop), new_value)
|
|
680
|
+
ur.add_undo_method(node, "set_indexed", NodePath(prop), old_value)
|
|
681
|
+
else:
|
|
682
|
+
ur.add_do_property(node, prop, new_value)
|
|
683
|
+
ur.add_undo_property(node, prop, old_value)
|
|
684
|
+
ur.commit_action()
|
|
685
|
+
after = Codec.read_property(node, prop)
|
|
686
|
+
if outcome == "mismatch":
|
|
687
|
+
return _err(
|
|
688
|
+
"set_mismatch",
|
|
689
|
+
"%s.%s holds %s after the write and %s was asked for. Nothing the caller wrote survived: rich types cross this wire as {\"__type__\": \"Vector2\", \"x\": .., \"y\": ..} and a bare map, array or string is not that."
|
|
690
|
+
% [_path_of(root, node), prop, _describe(after), _describe(new_value)]
|
|
691
|
+
)
|
|
692
|
+
if outcome == "ignored":
|
|
693
|
+
return _err(
|
|
694
|
+
"set_ignored",
|
|
695
|
+
"%s.%s is unchanged at %s after being asked for %s. The property may not exist on this node, or its setter refused the value."
|
|
696
|
+
% [_path_of(root, node), prop, _describe(after), _describe(new_value)]
|
|
697
|
+
)
|
|
698
|
+
var out := {"path": _path_of(root, node), "property": prop, "value": Codec.encode(after)}
|
|
699
|
+
if outcome == "coerced":
|
|
700
|
+
out["coerced"] = true
|
|
701
|
+
out["requested"] = Codec.encode(new_value)
|
|
702
|
+
return _ok(out)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
## The three outcomes of a write. Deliberately duplicated across the two planes rather
|
|
706
|
+
## than hoisted into the codec: 254's per-plane split is the house rule and these two
|
|
707
|
+
## scripts have separate lifetimes. See `runtime_bridge.gd::_set_outcome` for the
|
|
708
|
+
## argument the three names carry.
|
|
709
|
+
static func _set_outcome(asked: Variant, before: Variant, after: Variant) -> String:
|
|
710
|
+
if Codec.values_equal(asked, after):
|
|
711
|
+
return "applied"
|
|
712
|
+
if not Codec.types_compatible(typeof(asked), typeof(after)):
|
|
713
|
+
return "mismatch"
|
|
714
|
+
if Codec.values_equal(before, after):
|
|
715
|
+
return "ignored"
|
|
716
|
+
return "coerced"
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
## `type_string(typeof(v))` plus the value, for a message a reader can act on.
|
|
720
|
+
static func _describe(v: Variant) -> String:
|
|
721
|
+
return "%s %s" % [type_string(typeof(v)), JSON.stringify(Codec.encode(v))]
|
|
659
722
|
|
|
660
723
|
|
|
661
724
|
func _node_get_property(params: Dictionary) -> Dictionary:
|
|
@@ -668,7 +731,7 @@ func _node_get_property(params: Dictionary) -> Dictionary:
|
|
|
668
731
|
var prop := String(params.get("property", ""))
|
|
669
732
|
if prop == "":
|
|
670
733
|
return _err("bad_params", "Missing 'property'")
|
|
671
|
-
return _ok({"path": _path_of(root, node), "property": prop, "value": Codec.encode(
|
|
734
|
+
return _ok({"path": _path_of(root, node), "property": prop, "value": Codec.encode(Codec.read_property(node, prop))})
|
|
672
735
|
|
|
673
736
|
|
|
674
737
|
func _selection_get() -> Dictionary:
|
|
@@ -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.12.0"
|
|
7
7
|
script="plugin.gd"
|
|
@@ -20,7 +20,7 @@ extends Node
|
|
|
20
20
|
## and the process that is already running keeps the addon it loaded. Held in lockstep
|
|
21
21
|
## with `plugin.cfg` and with `operations.gd`'s copy by contract_check check 14, which
|
|
22
22
|
## exists because two of those literals disagreed for two releases.
|
|
23
|
-
const ADDON_VERSION := "1.
|
|
23
|
+
const ADDON_VERSION := "1.12.0"
|
|
24
24
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
25
25
|
const Remedies := preload("res://addons/breakpoint_mcp/error_remedies.gd")
|
|
26
26
|
const DEFAULT_PORT := 9081
|
|
@@ -502,7 +502,35 @@ func _get_property(params: Dictionary) -> Dictionary:
|
|
|
502
502
|
if node == null:
|
|
503
503
|
return _err("bad_path", "Node not found: %s" % params.get("path", ""))
|
|
504
504
|
var prop := String(params.get("property", ""))
|
|
505
|
-
|
|
505
|
+
if prop == "":
|
|
506
|
+
return _err("bad_params", "Missing 'property'")
|
|
507
|
+
return _ok({"path": _path_of(node), "property": prop, "value": Codec.encode(Codec.read_property(node, prop))})
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
## 🔴 THIS FUNCTION USED TO ANSWER WITH A READ-BACK IT NEVER COMPARED TO WHAT IT WAS
|
|
511
|
+
## ASKED FOR (issue #327). Every way a write can fail to land — a String coerced into
|
|
512
|
+
## a bool, an untagged `{"x":..,"y":..}` landing in a Vector2 as (0,0), an indexed path
|
|
513
|
+
## read with the wrong method, a `value` key that never arrived — produced the same
|
|
514
|
+
## `ok` envelope carrying the value the property ALREADY HAD. The caller's next action
|
|
515
|
+
## was therefore taken against a state it believed it had set, arbitrarily far from the
|
|
516
|
+
## point where nothing happened.
|
|
517
|
+
##
|
|
518
|
+
## 🔴 THE THREE OUTCOMES, AND ONLY ONE OF THEM IS A FAILURE OF THE WRITE:
|
|
519
|
+
##
|
|
520
|
+
## APPLIED the engine holds what was asked for -> ok
|
|
521
|
+
## COERCED same type, different value — a setter clamped, snapped
|
|
522
|
+
## or normalised it. Ordinary and its own business, but the
|
|
523
|
+
## caller must be TOLD, because it is not what it asked for -> ok, coerced
|
|
524
|
+
## REFUSED the property is unchanged, or holds an incompatible
|
|
525
|
+
## type: nothing the caller wrote survived -> error
|
|
526
|
+
static func _set_outcome(asked: Variant, before: Variant, after: Variant) -> String:
|
|
527
|
+
if Codec.values_equal(asked, after):
|
|
528
|
+
return "applied"
|
|
529
|
+
if not Codec.types_compatible(typeof(asked), typeof(after)):
|
|
530
|
+
return "mismatch"
|
|
531
|
+
if Codec.values_equal(before, after):
|
|
532
|
+
return "ignored"
|
|
533
|
+
return "coerced"
|
|
506
534
|
|
|
507
535
|
|
|
508
536
|
func _set_property(params: Dictionary) -> Dictionary:
|
|
@@ -510,8 +538,57 @@ func _set_property(params: Dictionary) -> Dictionary:
|
|
|
510
538
|
if node == null:
|
|
511
539
|
return _err("bad_path", "Node not found: %s" % params.get("path", ""))
|
|
512
540
|
var prop := String(params.get("property", ""))
|
|
513
|
-
|
|
514
|
-
|
|
541
|
+
if prop == "":
|
|
542
|
+
return _err("bad_params", "Missing 'property'")
|
|
543
|
+
# 🔴 A MISSING `value` IS NOT A NULL VALUE, and conflating them WIPED PROPERTIES.
|
|
544
|
+
# `Dictionary.get("value")` answers null for a key that is not there, and
|
|
545
|
+
# `Object.set(prop, null)` does not refuse — the engine coerces null to the
|
|
546
|
+
# property type's zero. Measured on 4.7: rotation 1.25 -> 0.0, position
|
|
547
|
+
# (123, 456) -> (0, 0), reported as success both times. The host's own published
|
|
548
|
+
# input schema left `value` OPTIONAL (`z.any()` is optional in zod), so a client
|
|
549
|
+
# omitting it was doing exactly what it was told it could do.
|
|
550
|
+
if not params.has("value"):
|
|
551
|
+
return _err(
|
|
552
|
+
"bad_params",
|
|
553
|
+
"Missing 'value'. Pass it explicitly — an absent value is not the same request as a null one, and writing null here would set %s to its type's zero." % prop
|
|
554
|
+
)
|
|
555
|
+
var asked: Variant = Codec.decode(params["value"])
|
|
556
|
+
var before: Variant = Codec.read_property(node, prop)
|
|
557
|
+
Codec.write_property(node, prop, asked)
|
|
558
|
+
var after: Variant = Codec.read_property(node, prop)
|
|
559
|
+
var outcome := _set_outcome(asked, before, after)
|
|
560
|
+
# 🔴 A REFUSED WRITE PUTS THE PROPERTY BACK, and the first draft of this fix did not.
|
|
561
|
+
# The engine has already coerced by the time we can compare, so `{"x":.., "y":..}`
|
|
562
|
+
# landing in a Vector2 had ALREADY overwritten (123, 456) with (0, 0) — and reporting
|
|
563
|
+
# that honestly would have left the caller with an accurate message and a broken
|
|
564
|
+
# scene. An error the tool raises about its own write must not also BE a write.
|
|
565
|
+
if outcome == "mismatch" or outcome == "ignored":
|
|
566
|
+
Codec.write_property(node, prop, before)
|
|
567
|
+
if outcome == "mismatch":
|
|
568
|
+
return _err(
|
|
569
|
+
"set_mismatch",
|
|
570
|
+
"%s.%s holds %s after the write and %s was asked for. Nothing the caller wrote survived: rich types cross this wire as {\"__type__\": \"Vector2\", \"x\": .., \"y\": ..} and a bare map, array or string is not that."
|
|
571
|
+
% [_path_of(node), prop, _describe(after), _describe(asked)]
|
|
572
|
+
)
|
|
573
|
+
if outcome == "ignored":
|
|
574
|
+
return _err(
|
|
575
|
+
"set_ignored",
|
|
576
|
+
"%s.%s is unchanged at %s after being asked for %s. The property may not exist on this node, or its setter refused the value."
|
|
577
|
+
% [_path_of(node), prop, _describe(after), _describe(asked)]
|
|
578
|
+
)
|
|
579
|
+
var out := {"path": _path_of(node), "property": prop, "value": Codec.encode(after)}
|
|
580
|
+
if outcome == "coerced":
|
|
581
|
+
# The write LANDED and the engine changed it — a clamp, a snap, a normalise.
|
|
582
|
+
# Saying so is the difference between a tool that is honest about what it did
|
|
583
|
+
# and one that lets the caller assume.
|
|
584
|
+
out["coerced"] = true
|
|
585
|
+
out["requested"] = Codec.encode(asked)
|
|
586
|
+
return _ok(out)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
## `type_string(typeof(v))` plus the value, for a message a reader can act on.
|
|
590
|
+
static func _describe(v: Variant) -> String:
|
|
591
|
+
return "%s %s" % [type_string(typeof(v)), JSON.stringify(Codec.encode(v))]
|
|
515
592
|
|
|
516
593
|
|
|
517
594
|
func _call_method(params: Dictionary) -> Dictionary:
|
|
@@ -600,25 +677,52 @@ func _inject_input(params: Dictionary) -> Dictionary:
|
|
|
600
677
|
var mb := InputEventMouseButton.new()
|
|
601
678
|
mb.button_index = int(ev.get("button", 1))
|
|
602
679
|
mb.pressed = bool(ev.get("pressed", true))
|
|
603
|
-
var
|
|
604
|
-
if
|
|
605
|
-
|
|
680
|
+
var pos_err := _apply_vector2(ev, "position", func(v: Vector2) -> void: mb.position = v)
|
|
681
|
+
if not pos_err.is_empty():
|
|
682
|
+
return pos_err
|
|
606
683
|
Input.parse_input_event(mb)
|
|
607
684
|
return _ok({"injected": true, "kind": kind})
|
|
608
685
|
"mouse_motion":
|
|
609
686
|
var mm := InputEventMouseMotion.new()
|
|
610
|
-
var
|
|
611
|
-
if
|
|
612
|
-
|
|
613
|
-
var
|
|
614
|
-
if
|
|
615
|
-
|
|
687
|
+
var mpos_err := _apply_vector2(ev, "position", func(v: Vector2) -> void: mm.position = v)
|
|
688
|
+
if not mpos_err.is_empty():
|
|
689
|
+
return mpos_err
|
|
690
|
+
var rel_err := _apply_vector2(ev, "relative", func(v: Vector2) -> void: mm.relative = v)
|
|
691
|
+
if not rel_err.is_empty():
|
|
692
|
+
return rel_err
|
|
616
693
|
Input.parse_input_event(mm)
|
|
617
694
|
return _ok({"injected": true, "kind": kind})
|
|
618
695
|
_:
|
|
619
696
|
return _err("bad_kind", "Unknown input kind: %s" % kind)
|
|
620
697
|
|
|
621
698
|
|
|
699
|
+
## Apply an OPTIONAL Vector2 field of a synthetic input event, or say why not.
|
|
700
|
+
##
|
|
701
|
+
## 🔴 THIS WAS `if pos is Vector2:` WITH NO ELSE, and it is the one place in the addon
|
|
702
|
+
## where something really did discard silently — the shape issue #327 reported as
|
|
703
|
+
## *Vector2 in mouse-motion events makes mouse-driven controls unreachable*. An
|
|
704
|
+
## untagged `{"x": .., "y": ..}` decodes to a Dictionary, fails the type test, and the
|
|
705
|
+
## event was injected AT THE ORIGIN with `{"injected": true}` on the way back. The
|
|
706
|
+
## caller then read a game that had been clicked at (0, 0).
|
|
707
|
+
##
|
|
708
|
+
## Absent stays absent — these fields are genuinely optional and an omitted position
|
|
709
|
+
## means *wherever the event lands by default*. PRESENT AND UNUSABLE is the error:
|
|
710
|
+
## the caller aimed at something and missed by the width of the screen.
|
|
711
|
+
## Returns {} when there is nothing to report, an `_err` envelope otherwise.
|
|
712
|
+
func _apply_vector2(ev: Dictionary, field: String, apply: Callable) -> Dictionary:
|
|
713
|
+
if not ev.has(field):
|
|
714
|
+
return {}
|
|
715
|
+
var decoded: Variant = Codec.decode(ev[field])
|
|
716
|
+
if decoded is Vector2:
|
|
717
|
+
apply.call(decoded as Vector2)
|
|
718
|
+
return {}
|
|
719
|
+
return _err(
|
|
720
|
+
"bad_event_field",
|
|
721
|
+
"event.%s is %s and a Vector2 is required. Rich types cross this wire tagged: {\"__type__\": \"Vector2\", \"x\": .., \"y\": ..} — a bare map, array or string is not that, and injecting the event without it would have aimed at the origin."
|
|
722
|
+
% [field, _describe(decoded)]
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
|
|
622
726
|
func _get_monitors(params: Dictionary) -> Dictionary:
|
|
623
727
|
var out := {}
|
|
624
728
|
var keys: Array = params.get("keys", [])
|
|
@@ -106,3 +106,93 @@ static func decode(j: Variant) -> Variant:
|
|
|
106
106
|
arr.append(decode(item))
|
|
107
107
|
return arr
|
|
108
108
|
return j
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## 🔴 WHY THE FOUR MEMBERS BELOW EXIST — issue #327, and the reporter's own diagnosis
|
|
112
|
+
## was wrong in a way worth writing down. They read *vectors dropped, falsy scalars
|
|
113
|
+
## dropped, indexed paths null* as one truthiness guard in this file's decode path.
|
|
114
|
+
## Measured live on Godot 4.7, both sides of the wire driven separately: real `false`,
|
|
115
|
+
## `0` and `0.0` round-trip correctly and always have. What they actually hit was a
|
|
116
|
+
## String reaching a bool property — Godot coerces any non-empty String to `true`, so
|
|
117
|
+
## `"false"` reads back as `true` — and `set_property` had no way to notice, because it
|
|
118
|
+
## answered with a FRESH READ-BACK it never compared to what was asked.
|
|
119
|
+
##
|
|
120
|
+
## 🔴 SO THE REPAIR IS A COMPARISON AND NOT A GUARD. A guard has to enumerate the ways
|
|
121
|
+
## a write can fail to land; a comparison asks the only question that matters — is the
|
|
122
|
+
## value the caller asked for the value the engine now holds — and is therefore correct
|
|
123
|
+
## for the failure modes nobody has met yet.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## True when `prop` names a SUB-property rather than a property: `position:x`,
|
|
127
|
+
## `material_override:shader_parameter/albedo`.
|
|
128
|
+
##
|
|
129
|
+
## 🔴 THE COLON FORM IS `get_indexed`'s VOCABULARY, NOT `get`'s, and the two are
|
|
130
|
+
## different methods rather than two spellings of one. `Object.get("position:x")`
|
|
131
|
+
## answers `null` — not an error, not a warning — which is why #327 read this as the
|
|
132
|
+
## property being unreadable rather than as the wrong method being called.
|
|
133
|
+
static func is_indexed(prop: String) -> bool:
|
|
134
|
+
return prop.contains(":")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## Read a property that may or may not be indexed.
|
|
138
|
+
static func read_property(o: Object, prop: String) -> Variant:
|
|
139
|
+
if is_indexed(prop):
|
|
140
|
+
return o.get_indexed(NodePath(prop))
|
|
141
|
+
return o.get(prop)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## Write a property that may or may not be indexed.
|
|
145
|
+
static func write_property(o: Object, prop: String, v: Variant) -> void:
|
|
146
|
+
if is_indexed(prop):
|
|
147
|
+
o.set_indexed(NodePath(prop), v)
|
|
148
|
+
else:
|
|
149
|
+
o.set(prop, v)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
## Could a value of type `asked` ever be what the engine STORED as type `got`?
|
|
153
|
+
##
|
|
154
|
+
## 🔴 THIS IS THE LINE BETWEEN A COERCION AND A MISTAKE, and it is drawn at the type
|
|
155
|
+
## rather than at the value on purpose. A setter that clamps, snaps or normalises
|
|
156
|
+
## stores a DIFFERENT VALUE OF THE SAME TYPE and is doing its job — refusing that
|
|
157
|
+
## would break every `Range.value` in every project. A String that lands in a bool,
|
|
158
|
+
## or a Dictionary that lands in a Vector2, is a different kind of event: nothing the
|
|
159
|
+
## caller wrote survived, and the engine says nothing about it.
|
|
160
|
+
##
|
|
161
|
+
## The three promotions below are Godot's own and are never mistakes: int <-> float,
|
|
162
|
+
## String <-> StringName, and a plain Array into any of the packed array types (JSON
|
|
163
|
+
## has one array and the engine has ten).
|
|
164
|
+
static func types_compatible(asked: int, got: int) -> bool:
|
|
165
|
+
if asked == got:
|
|
166
|
+
return true
|
|
167
|
+
if _is_numeric(asked) and _is_numeric(got):
|
|
168
|
+
return true
|
|
169
|
+
if _is_stringy(asked) and _is_stringy(got):
|
|
170
|
+
return true
|
|
171
|
+
if asked == TYPE_ARRAY and _is_packed_array(got):
|
|
172
|
+
return true
|
|
173
|
+
return false
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
static func _is_numeric(t: int) -> bool:
|
|
177
|
+
return t == TYPE_INT or t == TYPE_FLOAT
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
static func _is_stringy(t: int) -> bool:
|
|
181
|
+
return t == TYPE_STRING or t == TYPE_STRING_NAME
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
static func _is_packed_array(t: int) -> bool:
|
|
185
|
+
return t in [
|
|
186
|
+
TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,
|
|
187
|
+
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,
|
|
188
|
+
TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY,
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## Did the engine store the value that was asked for? Numeric pairs are compared
|
|
193
|
+
## approximately, because an int asked of a float property stores a float and the two
|
|
194
|
+
## are the same answer.
|
|
195
|
+
static func values_equal(asked: Variant, got: Variant) -> bool:
|
|
196
|
+
if _is_numeric(typeof(asked)) and _is_numeric(typeof(got)):
|
|
197
|
+
return is_equal_approx(float(asked), float(got))
|
|
198
|
+
return asked == got
|
package/dist/dap.js
CHANGED
|
@@ -120,9 +120,15 @@ export function unorderedHandshakeWarning(waitedMs) {
|
|
|
120
120
|
export const DAP_RESTART_REMEDY = "Call `dbg_launch` or `dbg_attach` first — a restart reuses the parameters of a launch that already happened, and none has.";
|
|
121
121
|
/**
|
|
122
122
|
* Per-line breakpoint modifier fields, each gated by an adapter capability. Godot
|
|
123
|
-
* advertises
|
|
123
|
+
* advertises none of the three AND ignores them — a "conditional" breakpoint halts
|
|
124
124
|
* unconditionally (measured live on 4.3 and 4.7), so an undropped modifier is not a
|
|
125
125
|
* no-op, it is the opposite of what the caller asked for.
|
|
126
|
+
*
|
|
127
|
+
* 🔴 278 — THE KEYS ARE ABSENT, NOT `false`, AND THIS PARAGRAPH SAID `false` FOR AS LONG
|
|
128
|
+
* AS IT HAS EXISTED. Both read the same way through `!== true`, which is why nothing ever
|
|
129
|
+
* caught it; they are different facts about the adapter, and `docs/dap_capability_ledger.json`
|
|
130
|
+
* is where the difference is now recorded rather than described. The ledger carries the run
|
|
131
|
+
* the reading came from, so the next reader can check it instead of believing this comment.
|
|
126
132
|
*/
|
|
127
133
|
export const BREAKPOINT_MODIFIER_CAPS = {
|
|
128
134
|
condition: "supportsConditionalBreakpoints",
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { applyWireDefaults } from "./wire-defaults.js";
|
|
|
17
17
|
import { applyOutputSchemas } from "./schemas.js";
|
|
18
18
|
import { applyAnnotations } from "./annotations.js";
|
|
19
19
|
import { applyTimeoutCaveat } from "./timeout-caveat.js";
|
|
20
|
+
import { packageVersion } from "./version.js";
|
|
20
21
|
import { applyCapabilities, applyDroppedToolRefusal, droppedTools, registerCapabilitiesResource, selectPrivilegedGroups, } from "./capabilities.js";
|
|
21
22
|
import { taskStore, TASK_CAPABILITIES } from "./tasks.js";
|
|
22
23
|
import { RESOURCE_CAPABILITIES, registerResourceSubscriptions } from "./subscriptions.js";
|
|
@@ -52,7 +53,14 @@ async function main() {
|
|
|
52
53
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
53
54
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
54
55
|
// godot://… resources and receive notifications/resources/updated.
|
|
55
|
-
|
|
56
|
+
// 🔴 READ, NEVER WRITTEN HERE (#248, twenty-eight sessions). This is the version every
|
|
57
|
+
// connected client is told at `initialize`, and it was the ONE place in host/src where
|
|
58
|
+
// a version was a literal — while `--version`, `lsp.ts`'s clientInfo and `cslsp.ts`'s
|
|
59
|
+
// all call `packageVersion()`. `version.ts` says why in its own words: the best
|
|
60
|
+
// outcome is not a gated literal but no literal at all, and the incident it documents
|
|
61
|
+
// is this exact field — twenty-odd releases announcing 0.2.0 to every language server
|
|
62
|
+
// they met, because a literal nobody compares to anything cannot go stale loudly.
|
|
63
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: packageVersion() }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
56
64
|
// 208 §7.1 — delete from the wire what the protocol already says: the SDK's
|
|
57
65
|
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
58
66
|
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
package/dist/schemas.js
CHANGED
|
@@ -19,6 +19,32 @@ import { z } from "zod";
|
|
|
19
19
|
* scalar, array, or a {"__type__": ...}-tagged object — so it stays `any`.
|
|
20
20
|
*/
|
|
21
21
|
const encodedValue = z.any();
|
|
22
|
+
/**
|
|
23
|
+
* The same Variant, on the way IN — and REQUIRED, which `z.any()` cannot be.
|
|
24
|
+
*
|
|
25
|
+
* 🔴 ISSUE #327, AND THE HALF OF IT NOBODY HAD REPORTED. `z.any()` answers true to
|
|
26
|
+
* zod's `isOptional()`, so every input key spelled that way was published to clients
|
|
27
|
+
* OUTSIDE the JSON Schema's `required` list — including `value` on seven shipped
|
|
28
|
+
* tools whose own descriptions call it "New value". A client omitting it was doing
|
|
29
|
+
* exactly what our schema told it it could do, and the addon then read
|
|
30
|
+
* `params.get("value")` as null and wrote the property type's ZERO over whatever was
|
|
31
|
+
* there. Measured on Godot 4.7: `rotation` 1.25 -> 0.0, `position` (123, 456) ->
|
|
32
|
+
* (0, 0), reported as success both times.
|
|
33
|
+
*
|
|
34
|
+
* 🔴 THE UNION IS THE POINT, NOT A NARROWING. It admits everything JSON can carry,
|
|
35
|
+
* including an explicit `null` — which is a real request (`material_override = null`)
|
|
36
|
+
* and is NOT the same request as sending nothing. What it stops admitting is
|
|
37
|
+
* ABSENCE. The addon refuses a missing key on its own side as well: a schema is the
|
|
38
|
+
* client's contract, and a direct socket has never had one.
|
|
39
|
+
*/
|
|
40
|
+
export const requiredEncodedValue = z.union([
|
|
41
|
+
z.string(),
|
|
42
|
+
z.number(),
|
|
43
|
+
z.boolean(),
|
|
44
|
+
z.null(),
|
|
45
|
+
z.array(z.any()),
|
|
46
|
+
z.record(z.string(), z.any()),
|
|
47
|
+
]);
|
|
22
48
|
const capturedRaw = z.object({
|
|
23
49
|
code: z.number().nullable(),
|
|
24
50
|
stdout: z.string(),
|
|
@@ -184,7 +210,11 @@ export const outputSchemas = {
|
|
|
184
210
|
node_delete: { deleted: z.string() },
|
|
185
211
|
node_rename: { path: z.string(), name: z.string() },
|
|
186
212
|
node_reparent: { path: z.string() },
|
|
187
|
-
|
|
213
|
+
// `coerced`/`requested` appear only when the engine STORED something other than what
|
|
214
|
+
// was asked for and the type still matched — a clamp, a snap, a normalise (issue #327).
|
|
215
|
+
// Optional because the ordinary answer carries neither, and declared because a field the
|
|
216
|
+
// wire can carry and the schema cannot name is a field a strict client rejects.
|
|
217
|
+
node_set_property: { path: z.string(), property: z.string(), value: encodedValue, coerced: z.boolean().optional(), requested: encodedValue.optional() },
|
|
188
218
|
node_get_property: { path: z.string(), property: z.string(), value: encodedValue },
|
|
189
219
|
node_duplicate: { path: z.string(), name: z.string(), type: z.string() },
|
|
190
220
|
node_get_children: { path: z.string(), children: z.array(z.object({ name: z.string(), type: z.string(), path: z.string() })) },
|
|
@@ -540,7 +570,7 @@ export const outputSchemas = {
|
|
|
540
570
|
children: z.array(runtimeNode).optional(),
|
|
541
571
|
},
|
|
542
572
|
runtime_get_property: { ...engineLog, path: z.string(), property: z.string(), value: encodedValue },
|
|
543
|
-
runtime_set_property: { ...engineLog, path: z.string(), property: z.string(), value: encodedValue },
|
|
573
|
+
runtime_set_property: { ...engineLog, path: z.string(), property: z.string(), value: encodedValue, coerced: z.boolean().optional(), requested: encodedValue.optional() },
|
|
544
574
|
runtime_call_method: { ...engineLog, return: encodedValue },
|
|
545
575
|
runtime_emit_signal: { ...engineLog, emitted: z.boolean() },
|
|
546
576
|
runtime_inject_input: { ...engineLog, injected: z.boolean(), kind: z.string() },
|
package/dist/tools/dap.js
CHANGED
|
@@ -398,17 +398,18 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
398
398
|
"or a path resolving outside the Godot project root — Godot binds breakpoints only to scripts in the project it runs, so " +
|
|
399
399
|
"unlike cs_dbg_set_breakpoints an absolute path outside the root is refused here too. " +
|
|
400
400
|
"Feature-detected: the per-line conditions / hit_conditions / log_messages modifiers are only sent when the connected adapter " +
|
|
401
|
-
"advertises support (supportsConditionalBreakpoints / supportsHitConditionalBreakpoints / supportsLogPoints).
|
|
402
|
-
"
|
|
403
|
-
"
|
|
401
|
+
"advertises support (supportsConditionalBreakpoints / supportsHitConditionalBreakpoints / supportsLogPoints). Measured: no Godot " +
|
|
402
|
+
"build this project tests advertises it — 4.3-stable and 4.7-stable advertise none of the three (the keys are ABSENT rather than " +
|
|
403
|
+
"false) and IGNORE the modifiers, so such a breakpoint would halt unconditionally; see docs/dap_capability_ledger.json. All three " +
|
|
404
|
+
"are therefore dropped on every build measured, and the result includes `unsupported_modifiers` plus a `warning`. Detection happens when the breakpoints are " +
|
|
404
405
|
"applied, so it covers buffered ones too: setting modifiers before a session exists returns `modifier_detection: \"deferred\"` with a " +
|
|
405
406
|
"warning, and dbg_launch then reports the `unsupported_modifiers` actually dropped during the handshake.",
|
|
406
407
|
inputSchema: {
|
|
407
408
|
path: z.string().describe("Script path (res://..., absolute, or project-relative)"),
|
|
408
409
|
lines: z.array(z.number().int().positive()).describe("1-based line numbers"),
|
|
409
|
-
conditions: z.array(z.string().nullable()).optional().describe("Optional per-line condition expressions (aligned to lines, use null to skip a line); break only when the expression is true"),
|
|
410
|
-
hit_conditions: z.array(z.string().nullable()).optional().describe("Optional per-line hit expressions aligned to lines, e.g. '>3' or '%5' — break based on hit count (null to skip)"),
|
|
411
|
-
log_messages: z.array(z.string().nullable()).optional().describe("Optional per-line log messages aligned to lines; a message turns that breakpoint into a LOGPOINT (logs and continues, never halts). {expr} interpolates (null to skip)."),
|
|
410
|
+
conditions: z.array(z.string().nullable()).optional().describe("Optional per-line condition expressions (aligned to lines, use null to skip a line); break only when the expression is true. Gated on supportsConditionalBreakpoints — Measured: no Godot build this project tests advertises it, so this is dropped and reported on every build measured."),
|
|
411
|
+
hit_conditions: z.array(z.string().nullable()).optional().describe("Optional per-line hit expressions aligned to lines, e.g. '>3' or '%5' — break based on hit count (null to skip). Gated on supportsHitConditionalBreakpoints — Measured: no Godot build this project tests advertises it, so this is dropped and reported on every build measured."),
|
|
412
|
+
log_messages: z.array(z.string().nullable()).optional().describe("Optional per-line log messages aligned to lines; a message turns that breakpoint into a LOGPOINT (logs and continues, never halts). {expr} interpolates (null to skip). Gated on supportsLogPoints — Measured: no Godot build this project tests advertises it, so this is dropped and reported on every build measured."),
|
|
412
413
|
},
|
|
413
414
|
}, async ({ path, lines, conditions, hit_conditions, log_messages }) => {
|
|
414
415
|
try {
|
|
@@ -641,8 +642,11 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
641
642
|
"(DAP setExceptionBreakpoints). Pass the filter IDs to enable; call with no filters (or []) to clear them. The result echoes the " +
|
|
642
643
|
"active filters and lists `available_filters` — the exception filters the connected adapter actually advertises. " +
|
|
643
644
|
"Requires a running debug session. Not gated (it only configures the debugger). " +
|
|
644
|
-
"Feature-detected: on an adapter that advertises no exceptionBreakpointFilters (
|
|
645
|
-
"
|
|
645
|
+
"Feature-detected: on an adapter that advertises no exceptionBreakpointFilters (which also does not answer the request — it would " +
|
|
646
|
+
"otherwise time out) it returns a clear \"unsupported\" message WITHOUT sending anything. Measured: no Godot build this project " +
|
|
647
|
+
"tests advertises it — 4.3-stable AND 4.7-stable both advertise no filters, so on a shipped engine it is unsupported surface today " +
|
|
648
|
+
"rather than a feature you can reach; see docs/dap_capability_ledger.json. This sentence used to name 4.3 alone as the example, " +
|
|
649
|
+
"which read as though a newer build had filters. None does.",
|
|
646
650
|
inputSchema: {
|
|
647
651
|
filters: z.array(z.string()).optional().describe("Exception filter IDs to enable (default none = clear). Choose from available_filters in the result."),
|
|
648
652
|
},
|
|
@@ -782,6 +786,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
782
786
|
"list the valid goto targets on that line; when the line has exactly one target (or you pass `target_id`) it jumps there. " +
|
|
783
787
|
"DESTRUCTIVE: skips or repeats code by moving execution — confirm with the user and keep this gated. " +
|
|
784
788
|
"Feature-detected: on an adapter that does not advertise `supportsGotoTargetsRequest` it returns a clear \"unsupported\" message WITHOUT prompting. " +
|
|
789
|
+
"Measured: no Godot build this project tests advertises it — 4.3-stable and 4.7-stable both refuse this tool, so on a shipped engine it " +
|
|
790
|
+
"is unsupported surface today rather than a feature you can reach; see docs/dap_capability_ledger.json. It is kept, and kept guarded, " +
|
|
791
|
+
"because a capability is not a boundary and a build can start advertising it. " +
|
|
785
792
|
"Refuses a source that can never carry a target — a missing file, a directory, an empty path (which resolves to the project root), " +
|
|
786
793
|
"or a path resolving outside the Godot project root — the same guard dbg_set_breakpoints applies. " +
|
|
787
794
|
"Only meaningful while stopped at a breakpoint.",
|
|
@@ -854,7 +861,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
854
861
|
"resolvable id is armed in one setDataBreakpoints call. Call with no `watch` (or []) to clear all data breakpoints. The result reports the " +
|
|
855
862
|
"armed `breakpoints` (each with its resolved data_id and verified flag) and any `unresolved` variables the adapter cannot watch. " +
|
|
856
863
|
"Requires a running session; NOT gated (it only configures the debugger). Feature-detected: on an adapter that does not advertise " +
|
|
857
|
-
"`supportsDataBreakpoints` it returns a clear \"unsupported\" message without sending any request."
|
|
864
|
+
"`supportsDataBreakpoints` it returns a clear \"unsupported\" message without sending any request. Measured: no Godot build this " +
|
|
865
|
+
"project tests advertises it — 4.3-stable and 4.7-stable both refuse this tool, so on a shipped engine it is unsupported surface " +
|
|
866
|
+
"today rather than a feature you can reach; see docs/dap_capability_ledger.json.",
|
|
858
867
|
inputSchema: {
|
|
859
868
|
watch: z.array(z.object({
|
|
860
869
|
name: z.string().describe("Variable name to watch"),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { requiredEncodedValue } from "../../schemas.js";
|
|
2
3
|
import { gate } from "../../confirm.js";
|
|
3
4
|
/** AnimationPlayer / Animation authoring + AnimationTree state machines. */
|
|
4
5
|
export function registerAnimationTools(server, call) {
|
|
@@ -56,7 +57,7 @@ export function registerAnimationTools(server, call) {
|
|
|
56
57
|
name: z.string().describe("Animation name"),
|
|
57
58
|
track: z.number().int().describe("Track index"),
|
|
58
59
|
time: z.number().describe("Key time in seconds"),
|
|
59
|
-
value:
|
|
60
|
+
value: requiredEncodedValue.describe("Key value (JSON scalar, array, object, or a __type__-tagged Variant matching the track type)"),
|
|
60
61
|
transition: z.number().optional().describe("Transition curve exponent (default 1.0)"),
|
|
61
62
|
library: z.string().optional().describe("Animation library name (default \"\")"),
|
|
62
63
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { requiredEncodedValue } from "../../schemas.js";
|
|
2
3
|
import { gate } from "../../confirm.js";
|
|
3
4
|
/** Editor session + core project settings (editor_* / project get/set). */
|
|
4
5
|
export function registerCoreTools(server, call) {
|
|
@@ -35,7 +36,7 @@ export function registerCoreTools(server, call) {
|
|
|
35
36
|
description: "Write a ProjectSettings value. Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
|
|
36
37
|
inputSchema: {
|
|
37
38
|
name: z.string().describe("ProjectSettings key"),
|
|
38
|
-
value:
|
|
39
|
+
value: requiredEncodedValue.describe("New value; rich types use the {\"__type__\":...} tagging convention"),
|
|
39
40
|
save: z.boolean().optional().describe("Persist to disk (default false)"),
|
|
40
41
|
confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
|
|
41
42
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { requiredEncodedValue } from "../../schemas.js";
|
|
2
3
|
import { gate } from "../../confirm.js";
|
|
3
4
|
/** Scene-tree node authoring: add / delete / reparent / property / group ops. */
|
|
4
5
|
export function registerNodeTools(server, call, guard) {
|
|
@@ -48,7 +49,7 @@ export function registerNodeTools(server, call, guard) {
|
|
|
48
49
|
inputSchema: {
|
|
49
50
|
path: z.string().describe("Node path relative to the scene root"),
|
|
50
51
|
property: z.string().describe("Property name, e.g. position, modulate, text"),
|
|
51
|
-
value:
|
|
52
|
+
value: requiredEncodedValue.describe("New value (JSON scalar, array, object, or a __type__-tagged Variant)"),
|
|
52
53
|
},
|
|
53
54
|
}, async ({ path, property, value }) => call("node.set_property", { path, property, value }));
|
|
54
55
|
server.registerTool("node_get_property", {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { requiredEncodedValue } from "../../schemas.js";
|
|
2
3
|
import { gate } from "../../confirm.js";
|
|
3
4
|
/**
|
|
4
5
|
* Resource (.tres/.res) create / load / save / property / import ops.
|
|
@@ -100,7 +101,7 @@ export function registerResourceTools(server, call, guard) {
|
|
|
100
101
|
inputSchema: {
|
|
101
102
|
path: z.string().describe("Resource res:// path"),
|
|
102
103
|
property: z.string().describe("Property name"),
|
|
103
|
-
value:
|
|
104
|
+
value: requiredEncodedValue.describe("New value (JSON scalar or __type__-tagged Variant)"),
|
|
104
105
|
confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
|
|
105
106
|
},
|
|
106
107
|
}, async ({ path, property, value, confirm }) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { requiredEncodedValue } from "../../schemas.js";
|
|
2
3
|
import { gate } from "../../confirm.js";
|
|
3
4
|
/** Shader + ShaderMaterial authoring. */
|
|
4
5
|
export function registerShaderTools(server, call, guard) {
|
|
@@ -68,7 +69,7 @@ export function registerShaderTools(server, call, guard) {
|
|
|
68
69
|
inputSchema: {
|
|
69
70
|
path: z.string().describe("Node path relative to the scene root"),
|
|
70
71
|
param: z.string().describe("Shader uniform name"),
|
|
71
|
-
value:
|
|
72
|
+
value: requiredEncodedValue.describe("New value (JSON scalar or __type__-tagged Variant)"),
|
|
72
73
|
},
|
|
73
74
|
}, async ({ path, param, value }) => call("shadermaterial.set_param", { path, param, value }));
|
|
74
75
|
}
|
package/dist/tools/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { requiredEncodedValue } from "../schemas.js";
|
|
2
3
|
import { isTransportUnavailable, remedyClause } from "../bridge.js";
|
|
3
4
|
import { MAX_PEERS } from "../peers.js";
|
|
4
5
|
import { gate } from "../confirm.js";
|
|
@@ -134,7 +135,7 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
134
135
|
server.registerTool("runtime_set_property", {
|
|
135
136
|
title: "Runtime set property",
|
|
136
137
|
description: "Set a property on a live node. DESTRUCTIVE (mutates running game state) — gated by confirmation. Rich types use the {\"__type__\":...} convention.",
|
|
137
|
-
inputSchema: { path: z.string(), property: z.string(), value:
|
|
138
|
+
inputSchema: { path: z.string(), property: z.string(), value: requiredEncodedValue, ...confirmField, ...peerField },
|
|
138
139
|
}, async ({ path, property, value, confirm, peer }) => {
|
|
139
140
|
const blocked = await gate(server, confirm, `Set live property ${path}.${property} on ${target(peer)}`);
|
|
140
141
|
if (blocked)
|
|
@@ -371,7 +372,7 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
371
372
|
inputSchema: {
|
|
372
373
|
path: z.string().describe("Node path (relative to the current scene; '/root/...' absolute allowed)"),
|
|
373
374
|
property: z.string().describe("Property to read on each poll"),
|
|
374
|
-
value:
|
|
375
|
+
value: requiredEncodedValue.describe("Value to compare the property against (tagged-Variant form for complex types)"),
|
|
375
376
|
op: z.enum(["eq", "ne", "gt", "ge", "lt", "le"]).optional().describe("Comparison operator (default eq)"),
|
|
376
377
|
timeout_ms: z.number().int().positive().optional().describe("Maximum time to wait, in ms (default 5000)"),
|
|
377
378
|
poll_interval_ms: z.number().int().positive().optional().describe("Delay between polls, in ms (default 100)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.82.1",
|
|
4
4
|
"description": "MCP server exposing Godot to AI coding assistants over the Model Context Protocol — developed and tested with Claude — across four planes (CLI, live editor, LSP+DAP, runtime) with elicitation-gated destructive tools, long jobs on the formal MCP task model, captured console output, and MCP resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|