breakpoint-mcp 1.14.0 → 1.19.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/dist/pause.js ADDED
@@ -0,0 +1,105 @@
1
+ import { log } from "./logger.js";
2
+ export class PauseLatch {
3
+ _paused;
4
+ waiters = new Set();
5
+ activity = [];
6
+ seq = 0;
7
+ cap;
8
+ waitTimeoutMs;
9
+ constructor(opts) {
10
+ this._paused = opts?.startPaused ?? false;
11
+ this.waitTimeoutMs = opts?.waitTimeoutMs ?? 120000;
12
+ this.cap = Math.max(1, opts?.activityCap ?? 50);
13
+ }
14
+ isPaused() {
15
+ return this._paused;
16
+ }
17
+ pause(reason = "signal") {
18
+ if (this._paused)
19
+ return;
20
+ this._paused = true;
21
+ log(`[pause] agent PAUSED (${reason}) — new mutating tool calls will wait for resume`);
22
+ }
23
+ resume(reason = "signal") {
24
+ if (!this._paused)
25
+ return;
26
+ this._paused = false;
27
+ const n = this.waiters.size;
28
+ for (const release of [...this.waiters])
29
+ release();
30
+ this.waiters.clear();
31
+ log(`[pause] agent RESUMED (${reason})${n ? ` — released ${n} waiting call(s)` : ""}`);
32
+ }
33
+ toggle(reason = "signal") {
34
+ if (this._paused)
35
+ this.resume(reason);
36
+ else
37
+ this.pause(reason);
38
+ }
39
+ /** Record a mutating action reaching the gate (the activity signal source). */
40
+ record(action) {
41
+ this.seq += 1;
42
+ this.activity.push({ seq: this.seq, action, at: Date.now() });
43
+ while (this.activity.length > this.cap)
44
+ this.activity.shift();
45
+ }
46
+ /** Last `n` recorded actions (most recent last). */
47
+ recent(n = 10) {
48
+ return this.activity.slice(-Math.max(0, n));
49
+ }
50
+ latestSeq() {
51
+ return this.seq;
52
+ }
53
+ /**
54
+ * Resolve `true` immediately when not paused. While paused, wait until resumed
55
+ * (→ `true`) or `timeoutMs` elapses (→ `false`). Only entry to a NEW action
56
+ * waits; nothing already running is affected.
57
+ */
58
+ async awaitResumed(timeoutMs = this.waitTimeoutMs) {
59
+ if (!this._paused)
60
+ return true;
61
+ return new Promise((resolve) => {
62
+ const release = () => {
63
+ cleanup();
64
+ resolve(true);
65
+ };
66
+ const timer = setTimeout(() => {
67
+ cleanup();
68
+ resolve(false);
69
+ }, timeoutMs);
70
+ const cleanup = () => {
71
+ clearTimeout(timer);
72
+ this.waiters.delete(release);
73
+ };
74
+ this.waiters.add(release);
75
+ });
76
+ }
77
+ }
78
+ function envFlag(name) {
79
+ return /^(1|true|on|yes)$/i.test(process.env[name] ?? "");
80
+ }
81
+ function envInt(name, dflt) {
82
+ const raw = process.env[name];
83
+ const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
84
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
85
+ }
86
+ /**
87
+ * Process-wide latch, configured from the environment at load:
88
+ * BREAKPOINT_START_PAUSED start held (resume with SIGUSR2). Default off.
89
+ * BREAKPOINT_PAUSE_TIMEOUT_MS how long a held call waits before it blocks.
90
+ * `gate()` consults this singleton; `main()` wires the signal handlers.
91
+ */
92
+ export const pauseLatch = new PauseLatch({
93
+ startPaused: envFlag("BREAKPOINT_START_PAUSED"),
94
+ waitTimeoutMs: envInt("BREAKPOINT_PAUSE_TIMEOUT_MS", 120000),
95
+ });
96
+ /**
97
+ * Wire OS-signal control onto a running host (the prototype's control surface):
98
+ * SIGUSR1 → pause, SIGUSR2 → resume. Call once from `main()`; never at import,
99
+ * so importing this module in tests registers no process-level handlers.
100
+ */
101
+ export function installPauseSignalHandlers(latch = pauseLatch) {
102
+ process.on("SIGUSR1", () => latch.pause("SIGUSR1"));
103
+ process.on("SIGUSR2", () => latch.resume("SIGUSR2"));
104
+ }
105
+ //# sourceMappingURL=pause.js.map
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Recipes — a free, curated task-recipe layer exposed as MCP **prompts**.
3
+ *
4
+ * A recipe is a short, opinionated workflow that drives Breakpoint's own
5
+ * enforced tools to accomplish a common Godot task and then *verifies* it. It
6
+ * is the same idea as a paid "skill pack", with two differences that matter:
7
+ * it's **free** (MIT, shipped in the server), and it sits **over typed,
8
+ * schema-validated, undoable tools** — so the contract is executed by the
9
+ * server, not merely described in prose the model might misapply. Recipes are
10
+ * discoverable via MCP `prompts/list`; they add **no tools** (the 276-tool
11
+ * count is unchanged) and cost nothing until a client pulls one.
12
+ */
13
+ import { z } from "zod";
14
+ const str = (a, k, dflt) => typeof a?.[k] === "string" && a[k].length ? a[k] : dflt;
15
+ /** Wrap recipe body text as a single-message GetPromptResult. */
16
+ function recipe(description, text) {
17
+ return { description, messages: [{ role: "user", content: { type: "text", text } }] };
18
+ }
19
+ /** The shared closing note every recipe carries — the safety/story hook. */
20
+ const SAFETY = "Throughout: every edit goes through the editor's undo stack (editor_undo / Ctrl-Z " +
21
+ "reverts anything), destructive tools are confirmation-gated, and the runtime_assert_* " +
22
+ "verification family is free and read-only — a failed assert is one dbg_* step from *why*.";
23
+ export const RECIPE_NAMES = [
24
+ "recipe_2d_player_controller",
25
+ "recipe_wire_signal_and_assert",
26
+ "recipe_debug_inspect_variable",
27
+ "recipe_screenshot_regression",
28
+ "recipe_type_safe_edit",
29
+ "recipe_csharp_fix_and_debug",
30
+ ];
31
+ export function registerRecipes(server) {
32
+ // 1 — 2D player controller, then verify it moves.
33
+ server.registerPrompt("recipe_2d_player_controller", {
34
+ title: "2D player: CharacterBody2D + input + camera, then verify",
35
+ description: "Build a movable 2D player (input actions, body, collision, sprite, camera), type-check the script, then run and assert it actually moved.",
36
+ argsSchema: {
37
+ scene_path: z.string().optional().describe("Scene to edit (default res://main.tscn)"),
38
+ player_name: z.string().optional().describe("Node name for the player (default Player)"),
39
+ speed: z.string().optional().describe("Move speed in px/s (default 220)"),
40
+ },
41
+ }, (a) => {
42
+ const scene = str(a, "scene_path", "res://main.tscn");
43
+ const player = str(a, "player_name", "Player");
44
+ const speed = str(a, "speed", "220");
45
+ return recipe("2D player controller with a runtime movement check.", [
46
+ `Goal: add a keyboard-movable "${player}" to ${scene} and prove it moves.`,
47
+ "",
48
+ "1. Input: inputmap_add_action for move_left/right/up/down (skip any inputmap_list already reports), then inputmap_add_event to bind arrow keys / WASD.",
49
+ `2. Scene: scene_open ${scene} (or scene_new). node_add a CharacterBody2D named "${player}"; collisionshape_add a rectangle to it; node_add a Sprite2D (set its texture via node_set_property, or asset_gen_sprite if no art yet); camera_create as a child so the view follows.`,
50
+ `3. Script: author a _physics_process that reads Input.get_vector("move_left","move_right","move_up","move_down") * ${speed} into velocity and calls move_and_slide(); attach it to "${player}" (node_set_property script). Run gd_diagnostics on the script and fix until it reports zero errors BEFORE running.`,
51
+ `4. Verify: godot_run_project. runtime_assert_scene_structure that "${player}" (+ Camera2D, CollisionShape2D) exists. Capture the start position (runtime_get_property position), runtime_inject_input a right/up press for a few frames, then runtime_assert_node_state that position changed in the expected direction. godot_stop.`,
52
+ "",
53
+ SAFETY,
54
+ ].join("\n"));
55
+ });
56
+ // 2 — Wire a signal and assert at runtime that it fired.
57
+ server.registerPrompt("recipe_wire_signal_and_assert", {
58
+ title: "Wire a signal and assert at runtime that it fired",
59
+ description: "Connect a signal to a handler, then run the game, trigger it, and confirm the handler actually ran — the contract enforced, not assumed.",
60
+ argsSchema: {
61
+ signal_name: z.string().optional().describe("Signal to wire (e.g. body_entered, pressed, timeout)"),
62
+ },
63
+ }, (a) => {
64
+ const sig = str(a, "signal_name", "pressed");
65
+ return recipe("Signal wiring with a runtime firing assertion.", [
66
+ `Goal: wire the "${sig}" signal to a handler and prove the handler runs.`,
67
+ "",
68
+ `1. Inspect: signal_list on the emitter to confirm "${sig}" exists (or signal_add_user_signal to declare a custom one).`,
69
+ `2. Connect: signal_connect the emitter's "${sig}" to a method on the receiver (signal_list_connections to confirm the edge).`,
70
+ "3. Handler: author the handler so it leaves an observable trace — set a Label's text, flip a property, or print() (captured by the runtime log). Run gd_diagnostics until clean.",
71
+ "4. Run + trigger: godot_run_project, then cause the signal — runtime_inject_input for input signals, runtime_call_method / runtime_emit_signal for logic signals.",
72
+ "5. Assert: confirm the trace — runtime_assert_screen_text for the label, runtime_assert_node_state for the flipped property, or scan runtime_get_log for the print. A green assert means the wire is real, not hoped-for. godot_stop.",
73
+ "",
74
+ SAFETY,
75
+ ].join("\n"));
76
+ });
77
+ // 3 — Debugger loop: breakpoint, launch, inspect a real variable.
78
+ server.registerPrompt("recipe_debug_inspect_variable", {
79
+ title: "Set a breakpoint, launch under the debugger, read a real variable",
80
+ description: "The core differentiator: stop a running GDScript (or C#) game at a line and read actual call-stack values over Godot's Debug Adapter — not a guess from the outside.",
81
+ argsSchema: {
82
+ script_path: z.string().optional().describe("Script to break in (e.g. res://player.gd)"),
83
+ line: z.string().optional().describe("1-based line to break on"),
84
+ },
85
+ }, (a) => {
86
+ const path = str(a, "script_path", "res://player.gd");
87
+ const line = str(a, "line", "24");
88
+ return recipe("Live step-debugging with real variable inspection.", [
89
+ `Goal: pause the running game inside ${path}:${line} and read live state.`,
90
+ "",
91
+ `1. Breakpoint: dbg_set_breakpoints on ${path} at line ${line} (find the right line first with gd_document_symbols / gd_definition if unsure).`,
92
+ "2. Launch: dbg_launch to start the game under the Debug Adapter and run to the breakpoint.",
93
+ "3. Inspect (paused): dbg_stack_trace for the frames, dbg_scopes + dbg_variables for the real locals/members in the top frame, and dbg_evaluate to compute an expression in that frame. dbg_watch to track a value across steps.",
94
+ "4. Step: dbg_step over/into to watch state evolve; dbg_continue to resume (or dbg_set_exception_breakpoints to stop on the next error). dbg_restart to re-run.",
95
+ "",
96
+ "This is the capability a screenshot-and-scene-tree server can't offer: it observes from outside; Breakpoint stops execution and looks inside. (C#? the same flow with cs_dbg_* via netcoredbg.)",
97
+ "",
98
+ SAFETY,
99
+ ].join("\n"));
100
+ });
101
+ // 4 — Screenshot regression: reference frame, then diff.
102
+ server.registerPrompt("recipe_screenshot_regression", {
103
+ title: "Capture a reference frame and assert a scene hasn't visually regressed",
104
+ description: "Golden-image testing for a Godot scene using the free, read-only runtime_screenshot_diff — and the debugger on hand when it fails.",
105
+ argsSchema: {
106
+ reference_path: z.string().optional().describe("Project path for the golden PNG (default res://tests/ref.png)"),
107
+ tolerance: z.string().optional().describe("Max fraction of differing pixels (default 0.02)"),
108
+ },
109
+ }, (a) => {
110
+ const ref = str(a, "reference_path", "res://tests/ref.png");
111
+ const tol = str(a, "tolerance", "0.02");
112
+ return recipe("Golden-image regression check for a scene.", [
113
+ `Goal: lock a scene's look to a golden image at ${ref} and catch visual drift.`,
114
+ "",
115
+ `1. Establish the golden (first run only): godot_run_project, drive the game to the state under test (runtime_inject_input / runtime_call_method), then runtime_screenshot and save the frame to ${ref}. Commit it (vcs_add) so it's the reference.`,
116
+ `2. Regression run (every time after): godot_run_project, reproduce the same state, then runtime_screenshot_diff against ${ref} with tolerance ${tol} (pass a region to ignore volatile HUD areas). ok:true means no meaningful drift.`,
117
+ "3. On failure: the diff returns diff_ratio + differing_pixels — don't just eyeball it. runtime_get_tree / runtime_assert_node_state to find the node that changed, or dbg_* to break where the offending value is set. Update the golden deliberately (re-do step 1) only once you've confirmed the change is intended.",
118
+ "",
119
+ "The whole diff runs engine-side (Image), so there's no OCR/image dependency and it stays read-only and ungated.",
120
+ "",
121
+ SAFETY,
122
+ ].join("\n"));
123
+ });
124
+ // 5 — Type-safe edit with the language server before running.
125
+ server.registerPrompt("recipe_type_safe_edit", {
126
+ title: "Make a type-checked edit with the language server before running",
127
+ description: "Edit GDScript the way an IDE does — symbols, references, hover, and diagnostics from Godot's real LSP — so errors are caught before the game runs.",
128
+ argsSchema: {
129
+ script_path: z.string().optional().describe("Script to edit (e.g. res://player.gd)"),
130
+ },
131
+ }, (a) => {
132
+ const path = str(a, "script_path", "res://player.gd");
133
+ return recipe("Type-aware editing gated by the language server.", [
134
+ `Goal: change ${path} with real symbol knowledge and land it error-free.`,
135
+ "",
136
+ `1. Understand: gd_document_symbols for the file's shape; gd_definition / gd_references before touching a symbol so you know every call site; find_usages across the project for wider blast radius.`,
137
+ "2. Edit with help: while writing, use gd_completion and gd_hover to get exact API names and signatures (this is where a generic 'call any method' tool hallucinates — the LSP gives you the truth). gd_rename to rename safely across the project.",
138
+ "3. Gate: run gd_diagnostics and do not proceed until it's clean. gd_code_action for quick-fixes it suggests.",
139
+ "4. Prove behavior: godot_run_project and a runtime_assert_* that the change did what you intended. If it didn't, dbg_* into it.",
140
+ "",
141
+ "Every mutation is on the undo stack — editor_undo reverts a bad edit instantly.",
142
+ "",
143
+ SAFETY,
144
+ ].join("\n"));
145
+ });
146
+ // 6 — C#: inspect, fix with diagnostics, and debug.
147
+ server.registerPrompt("recipe_csharp_fix_and_debug", {
148
+ title: "C#: inspect, fix with diagnostics, and debug via netcoredbg",
149
+ description: "Full C# parity — OmniSharp for symbols and diagnostics, netcoredbg for real step-debugging — the same enforced loop GDScript gets.",
150
+ argsSchema: {
151
+ script_path: z.string().optional().describe("C# file to work in (e.g. res://Player.cs)"),
152
+ },
153
+ }, (a) => {
154
+ const path = str(a, "script_path", "res://Player.cs");
155
+ return recipe("C# fix-and-debug loop over OmniSharp + netcoredbg.", [
156
+ `Goal: diagnose and fix ${path}, then step through it to confirm the fix.`,
157
+ "",
158
+ `1. Locate: cs_document_symbols / cs_workspace_symbols to navigate; cs_diagnostics to list the actual compiler errors/warnings in ${path}.`,
159
+ "2. Fix: cs_hover / cs_definition / cs_references for correct types and call sites; cs_code_action for offered fixes; cs_rename to rename safely. Re-run cs_diagnostics until clean.",
160
+ `3. Debug: cs_dbg_set_breakpoints in ${path}, cs_dbg_launch, then at the stop cs_dbg_stack_trace + cs_dbg_variables + cs_dbg_evaluate to inspect real values; cs_dbg_step / cs_dbg_continue to walk the fix. (netcoredbg is spawned lazily, so this costs nothing until used.)`,
161
+ "",
162
+ "Same schema-validated, undoable discipline as the GDScript planes — C# is a first-class citizen here, not an afterthought.",
163
+ "",
164
+ SAFETY,
165
+ ].join("\n"));
166
+ });
167
+ }
168
+ //# sourceMappingURL=recipes.js.map
package/dist/schemas.js CHANGED
@@ -451,6 +451,49 @@ export const outputSchemas = {
451
451
  // capture, no managed parent). Optional so older addons still validate.
452
452
  capture: z.boolean().optional(),
453
453
  },
454
+ runtime_assert_node_state: {
455
+ path: z.string(),
456
+ ok: z.boolean(),
457
+ checked: z.number(),
458
+ mismatches: z.array(z.object({ property: z.string(), expected: encodedValue, actual: encodedValue })),
459
+ },
460
+ runtime_assert_scene_structure: {
461
+ ok: z.boolean(),
462
+ checked: z.number(),
463
+ failures: z.array(z.object({
464
+ path: z.string(),
465
+ reason: z.string(),
466
+ expected: encodedValue.optional(),
467
+ actual: encodedValue.optional(),
468
+ })),
469
+ },
470
+ runtime_assert_perf: {
471
+ ok: z.boolean(),
472
+ checked: z.number(),
473
+ regressions: z.array(z.object({
474
+ key: z.string(),
475
+ baseline: z.number(),
476
+ current: z.number(),
477
+ direction: z.string(),
478
+ })),
479
+ monitors: z.record(z.number()),
480
+ },
481
+ runtime_assert_screen_text: {
482
+ ok: z.boolean(),
483
+ matches: z.number(),
484
+ present: z.boolean(),
485
+ samples: z.array(z.object({ path: z.string(), text: z.string() })),
486
+ },
487
+ runtime_screenshot_diff: {
488
+ ok: z.boolean(),
489
+ diff_ratio: z.number(),
490
+ differing_pixels: z.number(),
491
+ total_pixels: z.number(),
492
+ width: z.number(),
493
+ height: z.number(),
494
+ reference: z.string(),
495
+ reason: z.string().optional(),
496
+ },
454
497
  // ---- Group K: knowledge & search ----
455
498
  // Host-side project index (tools/knowledge.ts) — no bridge/LSP; read the project files directly.
456
499
  project_search: {
package/dist/secret.js ADDED
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Loopback-bridge shared secret (host side).
5
+ *
6
+ * The editor addon / runtime autoload mint a per-project secret to
7
+ * <projectPath>/.godot/breakpoint_mcp.secret (see addons/breakpoint_mcp/
8
+ * bridge_secret.gd). Reading the same file lets the host authenticate to the
9
+ * loopback bridges with ZERO configuration. An env override wins for advanced /
10
+ * host-launched-child cases. When no material is available (an insecure or
11
+ * not-yet-provisioned bridge) the resolver returns null and the client connects
12
+ * without an auth line — backward-compatible with a bridge that isn't enforcing.
13
+ */
14
+ /** Read the minted project secret, or null if absent/empty/unreadable. */
15
+ export function readProjectSecret(projectPath) {
16
+ try {
17
+ const p = path.join(projectPath, ".godot", "breakpoint_mcp.secret");
18
+ const s = fs.readFileSync(p, "utf8").trim();
19
+ return s.length ? s : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * Resolve the bridge secret: the first non-empty env var in `envNames` wins,
27
+ * else the minted project-secret file. Read lazily per connect so a secret that
28
+ * appears after startup (the editor launched later) is picked up on reconnect.
29
+ */
30
+ export function resolveBridgeSecret(projectPath, envNames) {
31
+ for (const name of envNames) {
32
+ const v = process.env[name];
33
+ if (v && v.trim())
34
+ return v.trim();
35
+ }
36
+ return readProjectSecret(projectPath);
37
+ }
38
+ //# sourceMappingURL=secret.js.map
@@ -121,5 +121,117 @@ export function registerRuntimeTools(server, runtime) {
121
121
  levels: z.array(z.string()).optional().describe("Filter to these levels, e.g. [\"error\",\"warning\"]"),
122
122
  },
123
123
  }, async ({ since_seq, levels }) => call("runtime.get_log", { since_seq: since_seq ?? 0, levels: levels ?? [] }));
124
+ server.registerTool("runtime_assert_node_state", {
125
+ title: "Runtime assert node state",
126
+ description: "Assert that properties of a LIVE node equal expected values (read-only verification). " +
127
+ "Reports per-property mismatches; supports an optional absolute numeric tolerance.",
128
+ inputSchema: {
129
+ path: z.string().describe("Node path (relative to the current scene; '/root/...' absolute allowed)"),
130
+ expect: z
131
+ .record(z.any())
132
+ .describe("Map of property name -> expected value (JSON; use the tagged-Variant form for complex types like Vector2/Color)"),
133
+ tolerance: z
134
+ .number()
135
+ .nonnegative()
136
+ .optional()
137
+ .describe("Absolute tolerance for numeric comparisons (default 0 = exact match)"),
138
+ },
139
+ }, async ({ path, expect, tolerance }) => call("runtime.assert_node_state", tolerance !== undefined ? { path, expect, tolerance } : { path, expect }));
140
+ server.registerTool("runtime_assert_scene_structure", {
141
+ title: "Runtime assert scene structure",
142
+ description: "Assert the LIVE SceneTree matches structural expectations (read-only). Each entry asserts a node " +
143
+ "exists at a path (and, if given, is of a class via is_class); set absent:true to assert it is NOT present.",
144
+ inputSchema: {
145
+ expect: z
146
+ .array(z.object({
147
+ path: z.string(),
148
+ type: z.string().optional(),
149
+ absent: z.boolean().optional(),
150
+ }))
151
+ .describe("List of node expectations: {path, type?, absent?}."),
152
+ },
153
+ }, async ({ expect }) => call("runtime.assert_scene_structure", { expect }));
154
+ server.registerTool("runtime_assert_perf", {
155
+ title: "Runtime assert perf",
156
+ description: "Assert that live Performance monitors meet a caller-supplied baseline within tolerance (read-only). " +
157
+ "Capture the baseline earlier with runtime_get_monitors and pass it back inline. Pass direction is inferred " +
158
+ "(time/fps is higher-better; every other monitor is lower-better) unless overridden per key.",
159
+ inputSchema: {
160
+ baseline: z
161
+ .record(z.number())
162
+ .describe("Monitor key -> baseline value (capture earlier via runtime_get_monitors)"),
163
+ tolerance: z
164
+ .number()
165
+ .nonnegative()
166
+ .optional()
167
+ .describe("Fractional tolerance applied to each comparison (default 0 = exact)"),
168
+ direction: z
169
+ .record(z.enum(["higher_better", "lower_better"]))
170
+ .optional()
171
+ .describe("Per-key override of the pass direction (defaults: time/fps higher_better, else lower_better)"),
172
+ },
173
+ }, async ({ baseline, tolerance, direction }) => call("runtime.assert_perf", {
174
+ baseline,
175
+ ...(tolerance !== undefined ? { tolerance } : {}),
176
+ ...(direction !== undefined ? { direction } : {}),
177
+ }));
178
+ server.registerTool("runtime_assert_screen_text", {
179
+ title: "Runtime assert screen text",
180
+ description: "Assert that on-screen text is present (or absent) by scanning visible Control text in the LIVE scene tree " +
181
+ "(read-only; no OCR). Sees text on Label / RichTextLabel / Button / LineEdit / TextEdit / CheckBox / LinkButton " +
182
+ "and similar; does NOT see text drawn directly to the canvas or baked into textures.",
183
+ inputSchema: {
184
+ text: z.string().describe("Text (or regular expression) to look for"),
185
+ present: z
186
+ .boolean()
187
+ .optional()
188
+ .describe("Assert the text IS present (default true); set false to assert it is absent"),
189
+ regex: z.boolean().optional().describe("Treat `text` as a regular expression (default false = substring)"),
190
+ case_sensitive: z.boolean().optional().describe("Case-sensitive match (default false)"),
191
+ min_count: z
192
+ .number()
193
+ .int()
194
+ .positive()
195
+ .optional()
196
+ .describe("Require at least this many matches (implies present)"),
197
+ },
198
+ }, async ({ text, present, regex, case_sensitive, min_count }) => call("runtime.assert_screen_text", {
199
+ text,
200
+ ...(present !== undefined ? { present } : {}),
201
+ ...(regex !== undefined ? { regex } : {}),
202
+ ...(case_sensitive !== undefined ? { case_sensitive } : {}),
203
+ ...(min_count !== undefined ? { min_count } : {}),
204
+ }));
205
+ server.registerTool("runtime_screenshot_diff", {
206
+ title: "Runtime screenshot diff",
207
+ description: "Capture the current frame and compare it to a reference PNG at a project path, returning diff stats and a " +
208
+ "pass/fail vs tolerance (read-only; the diff is computed engine-side so the host stays dependency-free). " +
209
+ "Establish the reference first by saving a runtime_screenshot as a project asset.",
210
+ inputSchema: {
211
+ reference: z.string().describe("res:// or user:// path to the reference PNG"),
212
+ tolerance: z
213
+ .number()
214
+ .min(0)
215
+ .max(1)
216
+ .optional()
217
+ .describe("Max fraction of differing pixels that still passes (default 0)"),
218
+ per_channel_threshold: z
219
+ .number()
220
+ .int()
221
+ .min(0)
222
+ .max(255)
223
+ .optional()
224
+ .describe("Per-channel delta (0-255) for a pixel to count as different (default 0)"),
225
+ region: z
226
+ .object({ x: z.number().int(), y: z.number().int(), w: z.number().int(), h: z.number().int() })
227
+ .optional()
228
+ .describe("Optional sub-region (applied to both frame and reference) to compare"),
229
+ },
230
+ }, async ({ reference, tolerance, per_channel_threshold, region }) => call("runtime.screenshot_diff", {
231
+ reference,
232
+ ...(tolerance !== undefined ? { tolerance } : {}),
233
+ ...(per_channel_threshold !== undefined ? { per_channel_threshold } : {}),
234
+ ...(region !== undefined ? { region } : {}),
235
+ }));
124
236
  }
125
237
  //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,44 @@
1
+ import { registerCliTools } from "./tools/cli.js";
2
+ import { registerEditorTools } from "./tools/editor.js";
3
+ import { registerLspTools } from "./tools/lsp.js";
4
+ import { registerCsLspTools } from "./tools/cslsp.js";
5
+ import { registerDapTools } from "./tools/dap.js";
6
+ import { registerCsDapTools } from "./tools/csdap.js";
7
+ import { registerRuntimeTools } from "./tools/runtime.js";
8
+ import { registerProcessTools } from "./tools/processes.js";
9
+ import { registerKnowledgeTools } from "./tools/knowledge.js";
10
+ import { registerVcsTools } from "./tools/vcs.js";
11
+ import { registerAssetGenTools } from "./tools/assetgen.js";
12
+ import { registerNetcodeTools } from "./tools/netcode.js";
13
+ import { registerBackendTools } from "./tools/backend.js";
14
+ import { registerTabletopTools } from "./tools/tabletop.js";
15
+ import { registerResources } from "./tools/resources.js";
16
+ /**
17
+ * Build the ordered toolset list. Order matches `index.ts` exactly so the live
18
+ * tool list is byte-identical whether or not filtering is applied. `resources`
19
+ * here registers only the MCP resources; resource *subscriptions* are wired by
20
+ * the caller after selection (they add no tools, only notification plumbing).
21
+ */
22
+ export function buildToolsets(d) {
23
+ const s = d.server;
24
+ const c = d.config;
25
+ const stub = (v) => v;
26
+ return [
27
+ { id: "cli", describe: "Plane B — headless CLI (no editor required)", run: () => registerCliTools(s, c) },
28
+ { id: "editor", describe: "Plane A — live editor authoring (addon bridge)", run: () => registerEditorTools(s, stub(d.bridge)) },
29
+ { id: "lsp", describe: "Plane D — GDScript language server (LSP)", run: () => registerLspTools(s, stub(d.lsp), c) },
30
+ { id: "cslsp", describe: "Plane D — C# language server (OmniSharp)", run: () => registerCsLspTools(s, stub(d.csLsp), c) },
31
+ { id: "dap", describe: "Plane D — GDScript debug adapter (DAP)", run: () => registerDapTools(s, stub(d.dap), c) },
32
+ { id: "csdap", describe: "Plane D — C# debug adapter (netcoredbg)", run: () => registerCsDapTools(s, stub(d.csDap), c) },
33
+ { id: "runtime", describe: "Plane C — running-game runtime + verification family", run: () => registerRuntimeTools(s, stub(d.runtime)) },
34
+ { id: "processes", describe: "Managed run + captured console output", run: () => { const h = registerProcessTools(s, c); d.onProcesses?.(h); } },
35
+ { id: "knowledge", describe: "Group K — host-side project grep / symbol & idiom lookup", run: () => registerKnowledgeTools(s, c) },
36
+ { id: "vcs", describe: "Group L — read-only version control (git) over the project", run: () => registerVcsTools(s, c) },
37
+ { id: "assetgen", describe: "Group J — AI asset generation (degrades to a spec when off)", run: () => registerAssetGenTools(s, stub(d.bridge), c) },
38
+ { id: "netcode", describe: "Group M — multiplayer scaffolding (mp_*)", run: () => registerNetcodeTools(s, stub(d.bridge), c) },
39
+ { id: "backend", describe: "Group M — backend-SDK integration scaffolding", run: () => registerBackendTools(s, stub(d.bridge), c) },
40
+ { id: "tabletop", describe: "Group N — card/board/piece authoring composites", run: () => registerTabletopTools(s, stub(d.bridge), c) },
41
+ { id: "resources", describe: "MCP resources (scene tree, editor/runtime state, ClassDB)", run: () => registerResources(s, stub(d.bridge), stub(d.runtime)) },
42
+ ];
43
+ }
44
+ //# sourceMappingURL=toolsets.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakpoint-mcp",
3
- "version": "1.14.0",
3
+ "version": "1.19.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",