breakpoint-mcp 1.73.3 → 1.74.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/dist/annotations.js +3 -3
- package/dist/bridge.js +24 -2
- package/dist/capabilities.js +11 -0
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/init.js +3 -3
- package/dist/cli/tools.js +2 -2
- package/dist/finiteness.js +175 -0
- package/dist/index.js +6 -4
- package/dist/peers.js +1 -1
- package/dist/recipes.js +1 -1
- package/dist/schemas.js +33 -5
- package/dist/tasks.js +15 -2
- package/dist/tools/cli.js +44 -3
- package/dist/tools/editor/resource.js +2 -2
- package/dist/tools/runtime.js +9 -3
- package/dist/tools/tabletop.js +5 -5
- package/package.json +3 -3
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. **292 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.9.
|
|
10
|
+
const ADDON_VERSION := "1.9.9"
|
|
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.9.
|
|
6
|
+
version="1.9.9"
|
|
7
7
|
script="plugin.gd"
|
package/dist/annotations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Tools that mutate nothing. */
|
|
2
2
|
const READ_ONLY = [
|
|
3
|
-
"anim_get_track_keys", "anim_list", "backend_detect", "class_reference", "classdb_get_class", "cs_code_action",
|
|
3
|
+
"anim_get_track_keys", "anim_list", "backend_detect", "breakpoint_doctor", "class_reference", "classdb_get_class", "cs_code_action",
|
|
4
4
|
"cs_completion", "cs_dbg_scopes", "cs_dbg_stack_trace", "cs_dbg_variables", "cs_definition", "cs_diagnostics",
|
|
5
5
|
"cs_document_symbols", "cs_hover", "cs_references", "cs_signature_help", "cs_workspace_symbols", "dbg_scopes",
|
|
6
6
|
"dbg_stack_trace", "dbg_variables", "docs_search", "editor_get_state", "editor_ping", "example_snippet",
|
|
@@ -47,7 +47,7 @@ const IDEMPOTENT = [
|
|
|
47
47
|
"area_set_gravity", "area_set_monitoring", "asset_gen_configure", "asset_gen_placeholder",
|
|
48
48
|
"audio_bus_set_volume", "audio_set_bus_layout", "audio_set_stream", "auth_scaffold", "backend_configure",
|
|
49
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",
|
|
50
|
+
"body_set_collision_layer", "body_set_collision_mask", "body_set_physics_material", "breakpoint_doctor", "card_template_create",
|
|
51
51
|
"class_reference", "classdb_get_class", "cloudsave_scaffold", "control_set_anchors", "control_set_layout_preset",
|
|
52
52
|
"control_set_size_flags", "control_set_theme", "cs_code_action", "cs_completion", "cs_dbg_attach",
|
|
53
53
|
"cs_dbg_scopes", "cs_dbg_set_breakpoints", "cs_dbg_set_exception_breakpoints", "cs_dbg_set_variable",
|
|
@@ -111,7 +111,7 @@ export const ALL_ANNOTATED = [
|
|
|
111
111
|
"audio_bus_set_volume", "audio_player_create", "audio_set_bus_layout", "audio_set_stream", "auth_scaffold",
|
|
112
112
|
"backend_configure", "backend_detect", "board_create", "board_place", "board_tile_create", "board_tile_place",
|
|
113
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",
|
|
114
|
+
"breakpoint_doctor", "camera_create", "card_deck_from_table", "card_hand_layout", "card_instance", "card_set_face",
|
|
115
115
|
"card_template_create", "class_reference", "classdb_get_class", "cloudsave_scaffold", "collisionpolygon_add",
|
|
116
116
|
"collisionshape_add", "container_add_child", "control_create", "control_set_anchors",
|
|
117
117
|
"control_set_layout_preset", "control_set_size_flags", "control_set_theme", "cs_code_action", "cs_completion",
|
package/dist/bridge.js
CHANGED
|
@@ -2,6 +2,7 @@ import net from "node:net";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import { OverdueLedger } from "./late-reply.js";
|
|
5
|
+
import { findNonFinite, describeNonFinite, tolerate, TOLERANT_METHODS } from "./finiteness.js";
|
|
5
6
|
export class BridgeError extends Error {
|
|
6
7
|
code;
|
|
7
8
|
constructor(code, message) {
|
|
@@ -201,7 +202,28 @@ export class BridgeClient {
|
|
|
201
202
|
this.pending.delete(id);
|
|
202
203
|
clearTimeout(p.timer);
|
|
203
204
|
if (msg.ok) {
|
|
204
|
-
|
|
205
|
+
// 🔴 THE ONE PLACE A NON-FINITE ENGINE FLOAT CAN ENTER THE HOST. Godot stringifies
|
|
206
|
+
// INF as `1e99999`, which is valid JSON, so `JSON.parse` above hands back a real
|
|
207
|
+
// `Infinity` — accepted by zod 3, refused by zod 4, and turned into `null` by our
|
|
208
|
+
// own re-serialisation to the client either way.
|
|
209
|
+
//
|
|
210
|
+
// 🔴 226 §2: THE POLICY IS PER-METHOD AND IT IS NOT A NULL. Two methods report a
|
|
211
|
+
// partial reading and prune; every other method REFUSES, naming the path and the
|
|
212
|
+
// value. Writing `null` into a `z.number()` slot — which is what the first
|
|
213
|
+
// containment did on 91 tools that never declared it — fails the schema with a
|
|
214
|
+
// message about the shape, and takes the roster that would have explained it down
|
|
215
|
+
// with the parse. See finiteness.ts for the population and the measurement.
|
|
216
|
+
const result = msg.result ?? {};
|
|
217
|
+
if (TOLERANT_METHODS.has(p.method)) {
|
|
218
|
+
p.resolve(tolerate(result));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const hits = findNonFinite(result);
|
|
222
|
+
if (hits.length)
|
|
223
|
+
p.reject(new BridgeError("non_finite", describeNonFinite(hits)));
|
|
224
|
+
else
|
|
225
|
+
p.resolve(result);
|
|
226
|
+
}
|
|
205
227
|
}
|
|
206
228
|
else {
|
|
207
229
|
const e = msg.error ?? { code: "unknown", message: "Unknown bridge error" };
|
|
@@ -246,7 +268,7 @@ export class BridgeClient {
|
|
|
246
268
|
this.ledger.note(id, method, timeoutMs);
|
|
247
269
|
reject(new BridgeError("timeout", `Bridge request '${method}' timed out after ${timeoutMs}ms`));
|
|
248
270
|
}, timeoutMs);
|
|
249
|
-
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
271
|
+
this.pending.set(id, { resolve: resolve, reject, timer, method });
|
|
250
272
|
socket.write(payload, (err) => {
|
|
251
273
|
if (err) {
|
|
252
274
|
clearTimeout(timer);
|
package/dist/capabilities.js
CHANGED
|
@@ -37,6 +37,17 @@ export const TOOL_CAPABILITIES = {
|
|
|
37
37
|
// leaderboard_scaffold / cloudsave_scaffold / auth_scaffold — that write the
|
|
38
38
|
// same kind of generated GDScript and were never privileged.
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* How many tools the secure default drops, DERIVED rather than restated.
|
|
42
|
+
*
|
|
43
|
+
* 🔴 `breakpoint-mcp init` printed "drops the 14 code-execution + network tools"
|
|
44
|
+
* for as long as this group has had thirteen members — a wrong number in the first
|
|
45
|
+
* thing a new user reads, on the subject the secure default exists to explain.
|
|
46
|
+
* Check 25 reads three-digit numerals and this one is two digits; check 11 claims
|
|
47
|
+
* tool TOTALS and this is a subset of one. NO READER OWNED IT, which is 222 §2's
|
|
48
|
+
* finding arriving one instrument over, in a string we print to a terminal.
|
|
49
|
+
*/
|
|
50
|
+
export const PRIVILEGED_TOOL_COUNT = Object.keys(TOOL_CAPABILITIES).length;
|
|
40
51
|
/**
|
|
41
52
|
* Parse the raw BREAKPOINT_PRIVILEGED_GROUPS env into a normalized token list
|
|
42
53
|
* (or null for "unset" → no groups → the safe-default surface). Comma/whitespace
|
package/dist/cli/doctor.js
CHANGED
|
@@ -195,7 +195,7 @@ export function checkCapabilities(config) {
|
|
|
195
195
|
status: "ok",
|
|
196
196
|
severity: "info",
|
|
197
197
|
detail: dropped.length === 0
|
|
198
|
-
? `${state} — full
|
|
198
|
+
? `${state} — full 292-tool surface`
|
|
199
199
|
: `${state} (secure default) — ${dropped.length} higher-trust tool(s) dropped from the surface`,
|
|
200
200
|
hint: dropped.length === 0
|
|
201
201
|
? undefined
|
package/dist/cli/init.js
CHANGED
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { parseArgs } from "./args.js";
|
|
16
16
|
import { CLIENT_IDS, clientInfo, mergeClientConfig, serverEntry, snippet } from "./clients.js";
|
|
17
17
|
import { DEFAULT_REPO, fetchAddonFromGitHub } from "./github.js";
|
|
18
|
-
import { CAPABILITY_GROUPS, parsePrivilegedGroups, selectPrivilegedGroups } from "../capabilities.js";
|
|
18
|
+
import { CAPABILITY_GROUPS, PRIVILEGED_TOOL_COUNT, parsePrivilegedGroups, selectPrivilegedGroups, } from "../capabilities.js";
|
|
19
19
|
/**
|
|
20
20
|
* Resolve the guided trust preset into a validated BREAKPOINT_PRIVILEGED_GROUPS
|
|
21
21
|
* value ("" = the safe default — `code-execution` + `network` off). Accepts either
|
|
@@ -279,11 +279,11 @@ 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 292-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");
|
|
286
|
-
say(
|
|
286
|
+
say(`everyday authoring/debug surface and drops the ${PRIVILEGED_TOOL_COUNT} code-execution + network tools (e.g.`);
|
|
287
287
|
say("godot_run_headless_script, the *_call_method / dbg_evaluate tools, asset-gen, backend_*).");
|
|
288
288
|
say("Enable them by re-running with `--trust full` (or `--privileged-groups code-execution,network`),");
|
|
289
289
|
say("or set BREAKPOINT_PRIVILEGED_GROUPS in the server env. The `godot://capabilities` resource and");
|
package/dist/cli/tools.js
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* container, and on a machine with no Godot installed.
|
|
18
18
|
*
|
|
19
19
|
* Two surfaces are reported, because the difference is the security story:
|
|
20
|
-
* • `full` — all
|
|
21
|
-
* • `secure-default` —
|
|
20
|
+
* • `full` — all 292 tools, what loads with BREAKPOINT_PRIVILEGED_GROUPS=all
|
|
21
|
+
* • `secure-default` — 279, what an untouched install actually advertises
|
|
22
22
|
* `--surface` picks which one populates `tools[]`; both counts are always in the
|
|
23
23
|
* header so a consumer cannot report one as the other by accident.
|
|
24
24
|
*/
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-finite engine floats, contained at the one place they enter the host.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 224 §7.3 ASKED WHETHER GODOT'S PERFORMANCE MONITORS CAN EMIT `INF` THROUGH
|
|
5
|
+
* `runtime_get_monitors`, and called it "the only item that can break a working tool
|
|
6
|
+
* silently in production rather than loudly in CI". Measured on Godot 4.7 headless in
|
|
7
|
+
* session 225, every hop:
|
|
8
|
+
*
|
|
9
|
+
* Godot `JSON.stringify(INF)` -> `1e99999` — VALID JSON, so `JSON.parse` gives Infinity
|
|
10
|
+
* Godot `JSON.stringify(NAN)` -> `null` — plus a one-shot engine warning
|
|
11
|
+
* zod 3 `z.number()` -> ACCEPTS ±Infinity, REJECTS NaN
|
|
12
|
+
* zod 4 `z.number()` -> REJECTS both
|
|
13
|
+
* host -> MCP client -> `JSON.stringify(Infinity)` is `null`
|
|
14
|
+
*
|
|
15
|
+
* The answer is yes, and it needs no engine edge case at all: a client sending
|
|
16
|
+
* `"baseline": 1e999` to `runtime_assert_perf` round-trips through the addon as `inf`.
|
|
17
|
+
*
|
|
18
|
+
* 🔴 226 §2 — AND THE FIRST CONTAINMENT WAS SCOPED TO A ROSTER OF TWO. Session 225 put
|
|
19
|
+
* this walk at the single `JSON.parse` in `bridge.ts`, which is the right place, and then
|
|
20
|
+
* widened exactly the two schemas it had been thinking about. Measured off the shipped
|
|
21
|
+
* wire rather than the roster:
|
|
22
|
+
*
|
|
23
|
+
* bridge-routed tools carrying >=1 non-nullable number field 93
|
|
24
|
+
* their non-nullable number field paths 243
|
|
25
|
+
* of those, declaring `non_finite` 2
|
|
26
|
+
*
|
|
27
|
+
* Replacing a non-finite reading with `null` on the other 91 turns a silent wrong value
|
|
28
|
+
* into a HARD SCHEMA REFUSAL whose message blames the shape — `current: Expected number,
|
|
29
|
+
* received null` — and the `non_finite` roster that would have explained it is destroyed
|
|
30
|
+
* by the very parse that fails, because those 91 schemas do not declare the key. That is
|
|
31
|
+
* 225 §5's own finding one layer out: a coherent, specific message about the wrong cause.
|
|
32
|
+
*
|
|
33
|
+
* SO THE POLICY IS SPLIT, AND BOTH HALVES ARE DERIVED RATHER THAN CHOSEN:
|
|
34
|
+
*
|
|
35
|
+
* - DEFAULT — REFUSE, NAMING THE PATH AND THE VALUE. A non-finite number anywhere in a
|
|
36
|
+
* bridge reply rejects the call with code `non_finite` and a message that says which
|
|
37
|
+
* dotted path carried it and what it was. Loud, accurate, and — because no schema
|
|
38
|
+
* moves — invisible to `tools/list`. This is a strictly better failure than either
|
|
39
|
+
* 1.73.4 (silent `null` on the wire) or the first containment (a shape complaint).
|
|
40
|
+
*
|
|
41
|
+
* - TOLERANT — PRUNE AND ROSTER, for the tools whose whole job is a partial reading.
|
|
42
|
+
* `NON_FINITE_TOLERANT` below is the only place that set is written down, and
|
|
43
|
+
* `finiteness.test.ts` asserts it equals the set of shipped schemas declaring
|
|
44
|
+
* `non_finite`, in BOTH directions. A tool cannot join one without joining the other.
|
|
45
|
+
*
|
|
46
|
+
* WHAT THIS DELIBERATELY DOES NOT DO: widen a number to `number | null`. 225's note said
|
|
47
|
+
* "the numeric contract is not redefined … older clients are unaffected"; `wire_diff.mjs`
|
|
48
|
+
* classified that same change MAJOR at four field paths. The prose and the classifier
|
|
49
|
+
* disagreed and the classifier was right. Nothing here moves a declared type — every
|
|
50
|
+
* shipped `outputSchema` is byte-identical to v1.73.4 except for the OPTIONAL
|
|
51
|
+
* `non_finite` roster added to the two tolerant tools, which is what took the cut's
|
|
52
|
+
* classification from MAJOR back to MINOR.
|
|
53
|
+
*/
|
|
54
|
+
/** The key a tolerant reply carries when something was pruned. Optional in the schemas. */
|
|
55
|
+
export const NON_FINITE_KEY = "non_finite";
|
|
56
|
+
/**
|
|
57
|
+
* The tools that report a PARTIAL reading rather than refusing, keyed to the addon method
|
|
58
|
+
* `bridge.ts` matches on.
|
|
59
|
+
*
|
|
60
|
+
* 🔴 THIS IS THE ONLY WRITTEN-DOWN COPY OF THAT SET. `finiteness.test.ts` asserts, both
|
|
61
|
+
* ways, that its keys are exactly the shipped schemas declaring `non_finite` and that
|
|
62
|
+
* every method value is actually called by `tools/runtime.ts` — so the set cannot drift
|
|
63
|
+
* from the wire, and a rename cannot orphan an entry. 225 §9's carried finding is that
|
|
64
|
+
* every scope is prose until something derives it; this one is asserted against two
|
|
65
|
+
* independent readers.
|
|
66
|
+
*/
|
|
67
|
+
export const NON_FINITE_TOLERANT = new Map([
|
|
68
|
+
["runtime_get_monitors", "runtime.get_monitors"],
|
|
69
|
+
["runtime_assert_perf", "runtime.assert_perf"],
|
|
70
|
+
]);
|
|
71
|
+
/** The addon methods whose replies are pruned instead of refused. */
|
|
72
|
+
export const TOLERANT_METHODS = new Set(NON_FINITE_TOLERANT.values());
|
|
73
|
+
/** How deep the walk goes. Bridge replies are shallow; the cap is a runaway guard, and it
|
|
74
|
+
* is asserted rather than assumed — see `finiteness.test.ts`'s depth case. */
|
|
75
|
+
const MAX_DEPTH = 64;
|
|
76
|
+
/** `Infinity` / `-Infinity` / `NaN` as a word. `String(NaN)` is already "NaN". */
|
|
77
|
+
function word(n) {
|
|
78
|
+
return Number.isNaN(n) ? "NaN" : n > 0 ? "Infinity" : "-Infinity";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Every non-finite number in `value`, in encounter order. Pure: allocates one array and
|
|
82
|
+
* never copies the input, so the normal path — which is every real reply — costs one walk
|
|
83
|
+
* and returns the empty list.
|
|
84
|
+
*/
|
|
85
|
+
export function findNonFinite(value) {
|
|
86
|
+
const hits = [];
|
|
87
|
+
const walk = (v, path, depth) => {
|
|
88
|
+
if (typeof v === "number") {
|
|
89
|
+
if (!Number.isFinite(v))
|
|
90
|
+
hits.push({ path: path || "(root)", value: word(v) });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (v === null || typeof v !== "object" || depth >= MAX_DEPTH)
|
|
94
|
+
return;
|
|
95
|
+
if (Array.isArray(v)) {
|
|
96
|
+
v.forEach((item, i) => walk(item, `${path}[${i}]`, depth + 1));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const [k, item] of Object.entries(v)) {
|
|
100
|
+
walk(item, path ? `${path}.${k}` : k, depth + 1);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
walk(value, "", 0);
|
|
104
|
+
return hits;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The refusal message. It names the paths and the values, because the whole point of
|
|
108
|
+
* refusing here rather than at the schema is that the schema cannot say either.
|
|
109
|
+
* Capped so a pathological reply cannot produce an unbounded error string; the cap is
|
|
110
|
+
* asserted in `finiteness.test.ts` rather than trusted.
|
|
111
|
+
*/
|
|
112
|
+
export const MESSAGE_CAP = 8;
|
|
113
|
+
export function describeNonFinite(hits) {
|
|
114
|
+
const shown = hits.slice(0, MESSAGE_CAP).map((h) => `${h.path}=${h.value}`).join(", ");
|
|
115
|
+
const rest = hits.length > MESSAGE_CAP ? ` (+${hits.length - MESSAGE_CAP} more)` : "";
|
|
116
|
+
return (`The engine returned ${hits.length} non-finite number(s) this reply: ${shown}${rest}. ` +
|
|
117
|
+
`JSON cannot carry them — Godot stringifies INF as 1e99999 and NAN as null — so the ` +
|
|
118
|
+
`call is refused rather than reported as a success with the value silently missing.`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drop every non-finite value from a flat monitor record, returning what survived and the
|
|
122
|
+
* keys that did not. The declared type of the record does not move: a reading that is not
|
|
123
|
+
* a number is ABSENT, not `null`, and `non_finite` is where it says so.
|
|
124
|
+
*/
|
|
125
|
+
export function pruneRecord(rec) {
|
|
126
|
+
const kept = {};
|
|
127
|
+
const dropped = [];
|
|
128
|
+
if (rec === null || typeof rec !== "object" || Array.isArray(rec))
|
|
129
|
+
return { kept, dropped };
|
|
130
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
131
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
132
|
+
kept[k] = v;
|
|
133
|
+
else
|
|
134
|
+
dropped.push(k);
|
|
135
|
+
}
|
|
136
|
+
return { kept, dropped };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The tolerant policy for the two partial-reading tools: prune `monitors`, drop any
|
|
140
|
+
* comparison row that cannot be compared, and roster every key that left.
|
|
141
|
+
*
|
|
142
|
+
* 🔴 A DROPPED COMPARISON IS NOT A PASSED ONE. `ok` and `checked` are left exactly as the
|
|
143
|
+
* addon computed them — this host does not re-decide the assertion — but a key that could
|
|
144
|
+
* not be read appears in `non_finite`, so a caller reading `ok: true` alongside a
|
|
145
|
+
* non-empty roster can see that the two statements are about different key sets.
|
|
146
|
+
*/
|
|
147
|
+
export function tolerate(result) {
|
|
148
|
+
if (result === null || typeof result !== "object" || Array.isArray(result))
|
|
149
|
+
return result;
|
|
150
|
+
const src = result;
|
|
151
|
+
const roster = new Set();
|
|
152
|
+
const out = { ...src };
|
|
153
|
+
if ("monitors" in src) {
|
|
154
|
+
const { kept, dropped } = pruneRecord(src.monitors);
|
|
155
|
+
if (dropped.length) {
|
|
156
|
+
out.monitors = kept;
|
|
157
|
+
dropped.forEach((k) => roster.add(k));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(src.regressions)) {
|
|
161
|
+
const rows = src.regressions;
|
|
162
|
+
const survivors = rows.filter((row) => {
|
|
163
|
+
const bad = ["baseline", "current"].some((f) => typeof row?.[f] === "number" && !Number.isFinite(row[f]));
|
|
164
|
+
if (bad)
|
|
165
|
+
roster.add(typeof row?.key === "string" ? row.key : "(unnamed)");
|
|
166
|
+
return !bad;
|
|
167
|
+
});
|
|
168
|
+
if (survivors.length !== rows.length)
|
|
169
|
+
out.regressions = survivors;
|
|
170
|
+
}
|
|
171
|
+
if (roster.size === 0)
|
|
172
|
+
return result;
|
|
173
|
+
return { ...out, [NON_FINITE_KEY]: [...roster] };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=finiteness.js.map
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ async function main() {
|
|
|
42
42
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
43
43
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
44
44
|
// godot://… resources and receive notifications/resources/updated.
|
|
45
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
45
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.74.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
46
46
|
// 208 §7.1 — delete from the wire what the protocol already says: the SDK's
|
|
47
47
|
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
48
48
|
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
|
@@ -73,14 +73,14 @@ async function main() {
|
|
|
73
73
|
// Capability groups — a risk-based axis over the toolsets. `code-execution` is
|
|
74
74
|
// OFF by default; a disabled group's tools are DROPPED at
|
|
75
75
|
// registration (omitted from tools/list), so the secure-default surface is
|
|
76
|
-
//
|
|
76
|
+
// 292 − 13 = 279 tools. Enable via BREAKPOINT_PRIVILEGED_GROUPS. Wraps
|
|
77
77
|
// server.registerTool AFTER applyOutputSchemas (schema wrapper stays innermost).
|
|
78
78
|
const privilegedGroups = selectPrivilegedGroups(config.privilegedGroups, (unknown) => log(`ignoring unknown BREAKPOINT_PRIVILEGED_GROUPS token(s): ${unknown.join(", ")}`));
|
|
79
79
|
applyCapabilities(server, privilegedGroups);
|
|
80
80
|
// The A/B/C/D planes ARE the grouping. Build the ordered toolset registry
|
|
81
81
|
// (the single source of truth, shared with the registration tests) and
|
|
82
82
|
// register only the selected groups. Default (BREAKPOINT_TOOLSETS unset) =
|
|
83
|
-
// every group → the full
|
|
83
|
+
// every group → the full 292-tool surface, byte-identical to before. A filter
|
|
84
84
|
// lets a client that can't defer tools, or a user who wants a smaller default
|
|
85
85
|
// menu, load only the planes a project needs (GitHub-MCP `--toolsets` style).
|
|
86
86
|
let processes;
|
|
@@ -120,7 +120,7 @@ async function main() {
|
|
|
120
120
|
log(`toolsets enabled: ${[...enabled].sort().join(", ")} (${enabled.size}/${toolsets.length} groups)`);
|
|
121
121
|
}
|
|
122
122
|
// Recipes: a free, curated task-recipe layer exposed as MCP prompts (discoverable
|
|
123
|
-
// via prompts/list). Adds NO tools — the
|
|
123
|
+
// via prompts/list). Adds NO tools — the 292-tool count is unchanged — and drives
|
|
124
124
|
// the enforced tools above, so it's a skill-pack layer over typed/undoable tools.
|
|
125
125
|
registerRecipes(server);
|
|
126
126
|
// Always-on capability affordance — never behind a toolset or a privileged
|
|
@@ -177,6 +177,8 @@ function printUsage() {
|
|
|
177
177
|
" --dry-run Print what would change without writing anything.",
|
|
178
178
|
" --from-github [ref] Fetch the editor addon from GitHub at [ref] (default: this package's version tag) instead of the bundled copy.",
|
|
179
179
|
" --repo <owner/repo> With --from-github, the source repo (default: jlivingston-Cipher/godot-breakpoint-mcp).",
|
|
180
|
+
" --trust <level> secure | full. `full` enables every higher-trust group (default: secure).",
|
|
181
|
+
" --privileged-groups <a,b> Enable named higher-trust groups instead of all of them.",
|
|
180
182
|
"",
|
|
181
183
|
"doctor options:",
|
|
182
184
|
" --project <dir> Project to check (default: $GODOT_PROJECT or the current directory).",
|
package/dist/peers.js
CHANGED
|
@@ -6,7 +6,7 @@ import { portFree } from "./ports.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* Hard ceiling on simultaneously live peers.
|
|
8
8
|
*
|
|
9
|
-
* Four, not the eight
|
|
9
|
+
* Four, not the eight an alternative offers. Four headless Godot instances is already a
|
|
10
10
|
* heavy CI runner, the convergence cases that matter are covered at four, and
|
|
11
11
|
* every extra instance multiplies the flake surface of the one feature whose
|
|
12
12
|
* entire selling point is that it does NOT flake. Raise it on evidence, never
|
package/dist/recipes.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* it's **free** (MIT, shipped in the server), and it sits **over typed,
|
|
8
8
|
* schema-validated, undoable tools** — so the contract is executed by the
|
|
9
9
|
* server, not merely described in prose the model might misapply. Recipes are
|
|
10
|
-
* discoverable via MCP `prompts/list`; they add **no tools** (the
|
|
10
|
+
* discoverable via MCP `prompts/list`; they add **no tools** (the 292-tool
|
|
11
11
|
* count is unchanged) and cost nothing until a client pulls one.
|
|
12
12
|
*/
|
|
13
13
|
import { z } from "zod";
|
package/dist/schemas.js
CHANGED
|
@@ -71,6 +71,17 @@ const engineLog = {
|
|
|
71
71
|
/** name -> ZodRawShape describing that tool's structuredContent. */
|
|
72
72
|
export const outputSchemas = {
|
|
73
73
|
// ---- Plane B: headless CLI (tools/cli.ts) ----
|
|
74
|
+
breakpoint_doctor: {
|
|
75
|
+
ok: z.boolean(),
|
|
76
|
+
failed: z.number(),
|
|
77
|
+
checks: z.array(z.object({
|
|
78
|
+
name: z.string(),
|
|
79
|
+
status: z.string(),
|
|
80
|
+
severity: z.string(),
|
|
81
|
+
detail: z.string(),
|
|
82
|
+
hint: z.string().optional(),
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
74
85
|
godot_version: { version: z.string(), raw: capturedRaw },
|
|
75
86
|
godot_launch_editor: { launched: z.boolean(), pid: z.number().nullable(), project: z.string() },
|
|
76
87
|
godot_run_project: { running: z.boolean(), pid: z.number().nullable(), scene: z.string().nullable() },
|
|
@@ -203,7 +214,7 @@ export const outputSchemas = {
|
|
|
203
214
|
resource_duplicate: { duplicated: z.string(), from: z.string(), deep: z.boolean() },
|
|
204
215
|
resource_get_property: { path: z.string(), property: z.string(), value: encodedValue },
|
|
205
216
|
resource_set_property: { path: z.string(), property: z.string(), value: encodedValue },
|
|
206
|
-
resource_get_import_settings: { path: z.string(), imported: z.boolean(), importer: z.string(), settings: z.record(encodedValue) },
|
|
217
|
+
resource_get_import_settings: { path: z.string(), imported: z.boolean(), importer: z.string(), settings: z.record(z.string(), encodedValue) },
|
|
207
218
|
// `changed` (1.46.0) is narrower than `settings`: `settings` lists every key the call
|
|
208
219
|
// SET, `changed` only those whose stored value actually moved. Setting a key to the
|
|
209
220
|
// value it already held reported `reimported: true` with the sidecar's bytes unmoved
|
|
@@ -506,7 +517,15 @@ export const outputSchemas = {
|
|
|
506
517
|
runtime_call_method: { ...engineLog, return: encodedValue },
|
|
507
518
|
runtime_emit_signal: { ...engineLog, emitted: z.boolean() },
|
|
508
519
|
runtime_inject_input: { ...engineLog, injected: z.boolean(), kind: z.string() },
|
|
509
|
-
|
|
520
|
+
// `monitors` values are `Performance.get_monitor()` readings, so they are engine floats
|
|
521
|
+
// and can be non-finite. 🔴 226 §2: a reading that is not a number is ABSENT from
|
|
522
|
+
// `monitors` and named in `non_finite` — it is NOT `null`. The value type does not move,
|
|
523
|
+
// so this schema is byte-identical to v1.73.4 apart from the optional roster, and
|
|
524
|
+
// arithmetic over `Object.values(monitors)` keeps working on every key it can see.
|
|
525
|
+
runtime_get_monitors: { ...engineLog,
|
|
526
|
+
monitors: z.record(z.string(), z.number()),
|
|
527
|
+
non_finite: z.array(z.string()).optional(),
|
|
528
|
+
},
|
|
510
529
|
runtime_get_log: { ...engineLog,
|
|
511
530
|
entries: z.array(z.object({ seq: z.number(), level: z.string(), message: z.string() })),
|
|
512
531
|
latest_seq: z.number(),
|
|
@@ -530,6 +549,14 @@ export const outputSchemas = {
|
|
|
530
549
|
actual: encodedValue.optional(),
|
|
531
550
|
})),
|
|
532
551
|
},
|
|
552
|
+
// Same source as runtime_get_monitors for `current`/`monitors`. 🔴 `baseline` was the
|
|
553
|
+
// CLIENT's own number echoed back through the addon (`float(baseline[key])` in
|
|
554
|
+
// runtime_bridge.gd), and `1e999` in a request parses to `inf` in GDScript — the field
|
|
555
|
+
// reachable from a well-formed call with no engine edge case at all. 226 §2 closes that
|
|
556
|
+
// at the DOOR instead: the input schema now takes `z.number().finite()`, which refuses
|
|
557
|
+
// `1e999` on the way in and emits an identical JSON Schema, so neither side of this
|
|
558
|
+
// tool's wire moves. A row that cannot be compared is DROPPED and its key named in
|
|
559
|
+
// `non_finite`; `ok` and `checked` stay exactly as the addon computed them.
|
|
533
560
|
runtime_assert_perf: { ...engineLog,
|
|
534
561
|
ok: z.boolean(),
|
|
535
562
|
checked: z.number(),
|
|
@@ -539,7 +566,8 @@ export const outputSchemas = {
|
|
|
539
566
|
current: z.number(),
|
|
540
567
|
direction: z.string(),
|
|
541
568
|
})),
|
|
542
|
-
monitors: z.record(z.number()),
|
|
569
|
+
monitors: z.record(z.string(), z.number()),
|
|
570
|
+
non_finite: z.array(z.string()).optional(),
|
|
543
571
|
},
|
|
544
572
|
runtime_assert_screen_text: { ...engineLog,
|
|
545
573
|
ok: z.boolean(),
|
|
@@ -574,7 +602,7 @@ export const outputSchemas = {
|
|
|
574
602
|
// F4 additions: deterministic playtesting (time control / frame stepping / state digest / RNG seed).
|
|
575
603
|
runtime_time_scale: { ...engineLog, previous: z.number(), current: z.number() },
|
|
576
604
|
runtime_step_frames: { ...engineLog, frames_advanced: z.number(), frame_index: z.number() },
|
|
577
|
-
runtime_state_digest: { ...engineLog, digest: z.record(z.record(encodedValue)), node_count: z.number() },
|
|
605
|
+
runtime_state_digest: { ...engineLog, digest: z.record(z.string(), z.record(z.string(), encodedValue)), node_count: z.number() },
|
|
578
606
|
runtime_seed_rng: { ...engineLog, seed: z.number() },
|
|
579
607
|
// F6 additions: multi-peer deterministic playtesting (spawn / stop / converge).
|
|
580
608
|
runtime_spawn_peers: {
|
|
@@ -589,7 +617,7 @@ export const outputSchemas = {
|
|
|
589
617
|
},
|
|
590
618
|
runtime_peer_stop: { stopped: z.array(z.string()) },
|
|
591
619
|
runtime_peers_digest: {
|
|
592
|
-
digests: z.array(z.object({ id: z.string(), digest: z.record(z.record(encodedValue)), node_count: z.number() })),
|
|
620
|
+
digests: z.array(z.object({ id: z.string(), digest: z.record(z.string(), z.record(z.string(), encodedValue)), node_count: z.number() })),
|
|
593
621
|
converged: z.boolean(),
|
|
594
622
|
diverged_at: z.array(z.string()).nullable(),
|
|
595
623
|
},
|
package/dist/tasks.js
CHANGED
|
@@ -8,7 +8,7 @@ import { log } from "./logger.js";
|
|
|
8
8
|
* Long, run-to-completion tools (headless export / import / script runs) used to
|
|
9
9
|
* emit ad-hoc `notifications/progress`. They now register under the spec's task
|
|
10
10
|
* model: a call creates a task, returns a handle immediately, and the client
|
|
11
|
-
* drives it with tasks/get (poll)
|
|
11
|
+
* drives it with tasks/get (poll) and tasks/cancel
|
|
12
12
|
* (stop). Plain, non-task clients are unaffected — the SDK auto-creates a task,
|
|
13
13
|
* polls it to completion, and returns the result synchronously.
|
|
14
14
|
*/
|
|
@@ -20,6 +20,18 @@ export const TASK_POLL_INTERVAL_MS = 500;
|
|
|
20
20
|
* Server capabilities advertising the task-execution model for tools/call.
|
|
21
21
|
* Passed to the McpServer constructor so the SDK installs the tasks/get,
|
|
22
22
|
* tasks/result, tasks/list and tasks/cancel request handlers.
|
|
23
|
+
*
|
|
24
|
+
* Revision 2026-07-28 removes all four from CORE: tasks/result and tasks/list have
|
|
25
|
+
* no successor anywhere, while tasks/get and tasks/cancel survive only inside the
|
|
26
|
+
* io.modelcontextprotocol/tasks extension. The handlers stay while we negotiate
|
|
27
|
+
* 2025-11-25 — they are the SDK's, and this comment is not shipped to a client.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 THIS COMMENT USED TO ASSERT "no shipped tool description advertises them".
|
|
30
|
+
* Nothing checked that, and it was ALREADY FALSE for tasks/get and tasks/cancel on
|
|
31
|
+
* the day it was written — three descriptions in tools/cli.ts named both. An
|
|
32
|
+
* exemption a gate cannot verify is a promise. scripts/spec_conformance.py now
|
|
33
|
+
* measures the claim instead of restating it, and refuses on a shipped string that
|
|
34
|
+
* names a method the current revision removed.
|
|
23
35
|
*/
|
|
24
36
|
export const TASK_CAPABILITIES = {
|
|
25
37
|
tasks: { requests: { tools: { call: {} } } },
|
|
@@ -70,7 +82,8 @@ function errorResult(message) {
|
|
|
70
82
|
* ToolTaskHandler. createTask registers the task, launches the work in the
|
|
71
83
|
* background, and returns the handle immediately; the background settle stores
|
|
72
84
|
* the result — which the SDK turns into a notifications/tasks/status update and
|
|
73
|
-
* uses to satisfy any pending tasks/result long-poll.
|
|
85
|
+
* uses to satisfy any pending tasks/result long-poll. Both names are 2025-11-25's;
|
|
86
|
+
* see TASK_CAPABILITIES above for what 2026-07-28 does to them.
|
|
74
87
|
*/
|
|
75
88
|
function makeTaskHandler(name, worker) {
|
|
76
89
|
const shape = outputSchemas[name];
|
package/dist/tools/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { registerTaskTool } from "../tasks.js";
|
|
|
6
6
|
import { ok, failPath } from "./lsp-common.js";
|
|
7
7
|
import { resolveInsideProject } from "../paths.js";
|
|
8
8
|
import { portFree, portConflictMessage } from "../ports.js";
|
|
9
|
+
import { runDoctorChecks } from "../cli/doctor.js";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
/** Run the Godot binary to completion, capturing stdout/stderr. */
|
|
11
12
|
async function runCaptured(cfg, args, timeoutMs, signal) {
|
|
@@ -102,7 +103,7 @@ export function registerCliTools(server, cfg) {
|
|
|
102
103
|
registerTaskTool(server, "godot_export", {
|
|
103
104
|
title: "Export project",
|
|
104
105
|
description: "Headless export using an export preset. Runs to completion and returns exit code + logs. Can be slow — " +
|
|
105
|
-
"exposed as an MCP task, so task-aware
|
|
106
|
+
"exposed as an MCP task, so a task-aware client can poll it or cancel it while it runs.",
|
|
106
107
|
inputSchema: {
|
|
107
108
|
preset: z.string().describe("Export preset name as defined in export_presets.cfg"),
|
|
108
109
|
output_path: z.string().describe("Output file path for the exported build"),
|
|
@@ -137,7 +138,7 @@ export function registerCliTools(server, cfg) {
|
|
|
137
138
|
registerTaskTool(server, "godot_import", {
|
|
138
139
|
title: "Import assets",
|
|
139
140
|
description: "Headless (re)import of project assets. Runs to completion and returns exit code + logs. " +
|
|
140
|
-
"Exposed as an MCP task
|
|
141
|
+
"Exposed as an MCP task, so a task-aware client can poll it or cancel it while it runs.",
|
|
141
142
|
inputSchema: {
|
|
142
143
|
timeout_ms: z.number().int().positive().optional().describe("Max run time (default 600000)"),
|
|
143
144
|
},
|
|
@@ -149,7 +150,7 @@ export function registerCliTools(server, cfg) {
|
|
|
149
150
|
title: "Run headless script",
|
|
150
151
|
description: "Run a GDScript in headless mode (godot --headless -s <script>). Use this to invoke test runners " +
|
|
151
152
|
"(GdUnit4 / GUT) or any batch tool. Returns exit code + logs. Exposed as an MCP task, so a long test " +
|
|
152
|
-
"run can be polled
|
|
153
|
+
"run can be polled or cancelled while it is in flight.",
|
|
153
154
|
inputSchema: {
|
|
154
155
|
script_path: z.string().describe("Script to execute, e.g. res://addons/gdUnit4/bin/GdUnitCmdTool.gd"),
|
|
155
156
|
args: z.array(z.string()).optional().describe("Extra CLI args passed after the script"),
|
|
@@ -176,5 +177,45 @@ export function registerCliTools(server, cfg) {
|
|
|
176
177
|
stderr: tail(r.stderr),
|
|
177
178
|
});
|
|
178
179
|
});
|
|
180
|
+
// 🔴 D4, CARRIED SINCE 205 — THE DIAGNOSTIC THE PERSON WHO NEEDS IT CANNOT REACH.
|
|
181
|
+
// `breakpoint-mcp doctor` has shipped for many versions and answers exactly this
|
|
182
|
+
// question. It is a TERMINAL command, and a user whose bridge is not running is
|
|
183
|
+
// not in a terminal — they are in a conversation, watching an editor_* tool fail
|
|
184
|
+
// with a connect error and no idea which of the addon, the editor, the port or
|
|
185
|
+
// the secret is the one that is wrong. The assistant could not look. Now it can.
|
|
186
|
+
//
|
|
187
|
+
// Same `runDoctorChecks` the CLI drives, so the two can never disagree. It takes
|
|
188
|
+
// the ALREADY-LOADED server Config rather than calling loadConfig() — `runDoctor`
|
|
189
|
+
// mutates process.env.GODOT_PROJECT before loading, and doing that inside a tool
|
|
190
|
+
// handler would reconfigure the live server out from under every other tool.
|
|
191
|
+
server.registerTool("breakpoint_doctor", {
|
|
192
|
+
title: "Diagnose the setup",
|
|
193
|
+
description: "Check this Breakpoint MCP setup end to end: the Godot binary, the editor addon's install and " +
|
|
194
|
+
"enable state, the capability groups in force, and the editor/runtime/LSP/DAP bridges. Returns a " +
|
|
195
|
+
"per-check status with a hint for anything wrong. Read-only — run it first when an editor_*, " +
|
|
196
|
+
"runtime_*, gd_* or dbg_* tool fails to connect.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
require_live: z
|
|
199
|
+
.boolean()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Treat an unreachable bridge as a failure rather than information (default false)"),
|
|
202
|
+
include_csharp: z
|
|
203
|
+
.boolean()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Also probe OmniSharp / netcoredbg on PATH (the C# planes)"),
|
|
206
|
+
timeout_ms: z.number().int().positive().optional().describe("Per-bridge connect timeout (default 1500)"),
|
|
207
|
+
},
|
|
208
|
+
}, async ({ require_live, include_csharp, timeout_ms }) => {
|
|
209
|
+
const report = await runDoctorChecks(cfg, {
|
|
210
|
+
timeoutMs: timeout_ms ?? 1500,
|
|
211
|
+
requireLive: require_live ?? false,
|
|
212
|
+
includeCsharp: include_csharp ?? false,
|
|
213
|
+
});
|
|
214
|
+
return ok({
|
|
215
|
+
ok: report.ok,
|
|
216
|
+
failed: report.checks.filter((c) => c.status === "fail").length,
|
|
217
|
+
checks: report.checks,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
179
220
|
}
|
|
180
221
|
//# sourceMappingURL=cli.js.map
|
|
@@ -14,7 +14,7 @@ export function registerResourceTools(server, call, guard) {
|
|
|
14
14
|
inputSchema: {
|
|
15
15
|
class_name: z.string().describe("Resource subclass to instantiate, e.g. StyleBoxFlat, Theme, GDScript"),
|
|
16
16
|
to_path: z.string().describe("Destination res:// path, e.g. res://styles/panel.tres"),
|
|
17
|
-
properties: z.record(z.any()).optional().describe("Initial property values (JSON scalars or __type__-tagged Variants)"),
|
|
17
|
+
properties: z.record(z.string(), z.any()).optional().describe("Initial property values (JSON scalars or __type__-tagged Variants)"),
|
|
18
18
|
confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
|
|
19
19
|
},
|
|
20
20
|
}, async ({ class_name, to_path, properties, confirm }) => {
|
|
@@ -130,7 +130,7 @@ export function registerResourceTools(server, call, guard) {
|
|
|
130
130
|
description: "Update import parameters in an asset's .import metadata and trigger a reimport. DESTRUCTIVE (rewrites metadata + reimports) — gated by confirmation. Errors not_found when the path is not a file, not_imported when the file has no .import sidecar. `settings` echoes every key set; `changed` lists only those whose stored value actually moved, so a no-op is distinguishable from a real edit.",
|
|
131
131
|
inputSchema: {
|
|
132
132
|
path: z.string().describe("Asset res:// path"),
|
|
133
|
-
settings: z.record(z.any()).describe("Import params to set (name -> JSON scalar or __type__-tagged Variant)"),
|
|
133
|
+
settings: z.record(z.string(), z.any()).describe("Import params to set (name -> JSON scalar or __type__-tagged Variant)"),
|
|
134
134
|
reimport: z.boolean().optional().describe("Reimport after writing (default true)"),
|
|
135
135
|
confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
|
|
136
136
|
},
|
package/dist/tools/runtime.js
CHANGED
|
@@ -228,7 +228,7 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
228
228
|
inputSchema: {
|
|
229
229
|
path: z.string().describe("Node path (relative to the current scene; '/root/...' absolute allowed)"),
|
|
230
230
|
expect: z
|
|
231
|
-
.record(z.any())
|
|
231
|
+
.record(z.string(), z.any())
|
|
232
232
|
.describe("Map of property name -> expected value (JSON; use the tagged-Variant form for complex types like Vector2/Color)"),
|
|
233
233
|
tolerance: z
|
|
234
234
|
.number()
|
|
@@ -259,8 +259,14 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
259
259
|
"Capture the baseline earlier with runtime_get_monitors and pass it back inline. Pass direction is inferred " +
|
|
260
260
|
"(time/fps is higher-better; every other monitor is lower-better) unless overridden per key.",
|
|
261
261
|
inputSchema: {
|
|
262
|
+
// 🔴 226 §2 — `.finite()` REFUSES `1e999` AT THE DOOR AND MOVES NO BYTE ON THE
|
|
263
|
+
// WIRE. `zodToJsonSchema` emits `{"type":"number"}` for both forms, measured, so
|
|
264
|
+
// this narrows what the host accepts without changing what it advertises. Before
|
|
265
|
+
// it, a well-formed call carrying `1e999` reached `float(baseline[key])` in
|
|
266
|
+
// runtime_bridge.gd as `inf` and came back as `1e99999` — the one non-finite path
|
|
267
|
+
// in this tree that needed no engine edge case at all.
|
|
262
268
|
baseline: z
|
|
263
|
-
.record(z.number())
|
|
269
|
+
.record(z.string(), z.number().finite())
|
|
264
270
|
.describe("Monitor key -> baseline value (capture earlier via runtime_get_monitors)"),
|
|
265
271
|
tolerance: z
|
|
266
272
|
.number()
|
|
@@ -268,7 +274,7 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
268
274
|
.optional()
|
|
269
275
|
.describe("Fractional tolerance applied to each comparison (default 0 = exact)"),
|
|
270
276
|
direction: z
|
|
271
|
-
.record(z.enum(["higher_better", "lower_better"]))
|
|
277
|
+
.record(z.string(), z.enum(["higher_better", "lower_better"]))
|
|
272
278
|
.optional()
|
|
273
279
|
.describe("Per-key override of the pass direction (defaults: time/fps higher_better, else lower_better)"),
|
|
274
280
|
...peerField,
|
package/dist/tools/tabletop.js
CHANGED
|
@@ -1526,7 +1526,7 @@ export function registerTabletopTools(server, bridge, config) {
|
|
|
1526
1526
|
inputSchema: {
|
|
1527
1527
|
template_path: z.string().describe("Card template scene, e.g. res://ui/cards/Card.tscn"),
|
|
1528
1528
|
parent: z.string().describe("Node path to parent the instance under (in the open scene); \".\" for the root"),
|
|
1529
|
-
data: z.record(z.union([z.string(), z.number(), z.boolean()])).describe("Slot name → value; a texture slot takes a res:// path"),
|
|
1529
|
+
data: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).describe("Slot name → value; a texture slot takes a res:// path"),
|
|
1530
1530
|
position: z.object({ x: z.number(), y: z.number() }).optional().describe("Local position of the instance"),
|
|
1531
1531
|
face_up: z.boolean().optional().describe("Show the face (default true); false shows the back on two-sided cards"),
|
|
1532
1532
|
name: z.string().optional().describe("Optional node name for the instance"),
|
|
@@ -1556,7 +1556,7 @@ export function registerTabletopTools(server, bridge, config) {
|
|
|
1556
1556
|
template_path: z.string().describe("Card template scene, e.g. res://ui/cards/Card.tscn"),
|
|
1557
1557
|
parent: z.string().describe("Container node path the cards are instanced under; \".\" for the root"),
|
|
1558
1558
|
cards: z.array(z.object({
|
|
1559
|
-
data: z.record(z.union([z.string(), z.number(), z.boolean()])).describe("Slot name → value for this card"),
|
|
1559
|
+
data: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).describe("Slot name → value for this card"),
|
|
1560
1560
|
face_up: z.boolean().optional().describe("Show the face (default true)"),
|
|
1561
1561
|
})).min(1).describe("One entry per card to instance"),
|
|
1562
1562
|
mode: z.enum(["row", "fan", "stack", "grid"]).describe("Arrangement mode"),
|
|
@@ -1589,7 +1589,7 @@ export function registerTabletopTools(server, bridge, config) {
|
|
|
1589
1589
|
parent: z.string().describe("Container node path the cards are instanced under; \".\" for the root"),
|
|
1590
1590
|
table_path: z.string().describe("CSV or JSON table on disk (res:// or absolute); format auto-detected by extension unless `format` set"),
|
|
1591
1591
|
format: z.enum(["csv", "json"]).optional().describe("Override the table format"),
|
|
1592
|
-
column_map: z.record(z.string()).describe("Slot name → column expression (a bare {column} or a composed \"{a} · {b}\")"),
|
|
1592
|
+
column_map: z.record(z.string(), z.string()).describe("Slot name → column expression (a bare {column} or a composed \"{a} · {b}\")"),
|
|
1593
1593
|
filter: z.object({
|
|
1594
1594
|
column: z.string(),
|
|
1595
1595
|
equals: z.union([z.string(), z.number(), z.boolean()]),
|
|
@@ -1869,7 +1869,7 @@ export function registerTabletopTools(server, bridge, config) {
|
|
|
1869
1869
|
inputSchema: {
|
|
1870
1870
|
template_path: z.string().describe("Piece template scene, e.g. res://ui/pieces/Piece.tscn"),
|
|
1871
1871
|
parent: z.string().describe("Node path to parent the instance under (in the open scene); \".\" for the root"),
|
|
1872
|
-
data: z.record(z.union([z.string(), z.number(), z.boolean()])).describe("Slot name → value (art takes a res:// path; color a #RRGGBB; label a string)"),
|
|
1872
|
+
data: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).describe("Slot name → value (art takes a res:// path; color a #RRGGBB; label a string)"),
|
|
1873
1873
|
position: z.object({ x: z.number(), y: z.number() }).optional().describe("Local position of the instance (ignored when place_on snaps it to a cell)"),
|
|
1874
1874
|
face_up: z.boolean().optional().describe("Show the face (default true); false shows the back on two-sided pieces"),
|
|
1875
1875
|
name: z.string().optional().describe("Optional node name for the instance"),
|
|
@@ -1933,7 +1933,7 @@ export function registerTabletopTools(server, bridge, config) {
|
|
|
1933
1933
|
node: z.string().describe("Node path (in the open scene) to make draggable; \".\" for the root"),
|
|
1934
1934
|
script_path: z.string().describe("Where to save the generated drag script, e.g. res://ui/interact/Draggable.gd"),
|
|
1935
1935
|
mode: z.enum(["control", "node2d"]).describe("control→Control _get_drag_data DnD; node2d→Area2D hit region + pointer handler"),
|
|
1936
|
-
payload: z.record(z.union([z.string(), z.number(), z.boolean()])).optional().describe("Neutral data the drag carries (bound into the script as a Dictionary); default empty"),
|
|
1936
|
+
payload: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("Neutral data the drag carries (bound into the script as a Dictionary); default empty"),
|
|
1937
1937
|
preview: z.boolean().optional().describe("control mode: show a translucent drag preview (default false)"),
|
|
1938
1938
|
button: z.number().int().nonnegative().optional().describe("node2d mode: mouse button index that begins the drag (default 1 = left)"),
|
|
1939
1939
|
action: z.string().optional().describe("node2d mode: input action name registered for the drag button (default \"drag\")"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.0",
|
|
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",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"node": ">=18"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
59
|
-
"zod": "^3.
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
|
+
"zod": "^3.25.76"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^20.14.0",
|