breakpoint-mcp 1.74.0 → 1.75.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/addon/breakpoint_mcp/bridge_server.gd +4 -1
- package/addon/breakpoint_mcp/error_remedies.gd +113 -0
- package/addon/breakpoint_mcp/operations.gd +10 -2
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/runtime_bridge.gd +20 -2
- package/dist/addon-version.js +113 -0
- package/dist/annotations.js +10 -1
- package/dist/bridge.js +63 -4
- package/dist/capabilities.js +91 -2
- package/dist/cli/args.js +32 -2
- package/dist/cli/doctor.js +240 -19
- package/dist/cli/init.js +87 -16
- package/dist/cli/tools.js +12 -15
- package/dist/cli/usage.js +106 -0
- package/dist/index.js +84 -43
- package/dist/peers.js +17 -18
- package/dist/readiness.js +108 -0
- package/dist/remedies.js +63 -0
- package/dist/result-cost.js +90 -0
- package/dist/schemas.js +23 -4
- package/dist/tools/backend.js +2 -1
- package/dist/tools/cli.js +43 -6
- package/dist/tools/cslsp.js +19 -3
- package/dist/tools/editor/common.js +2 -1
- package/dist/tools/lsp-common.js +23 -0
- package/dist/tools/lsp.js +18 -3
- package/dist/tools/netcode.js +2 -1
- package/dist/tools/processes.js +26 -2
- package/dist/tools/runtime.js +34 -10
- package/dist/tools/tabletop.js +2 -1
- package/dist/wire-defaults.js +80 -8
- package/package.json +2 -2
package/dist/schemas.js
CHANGED
|
@@ -84,12 +84,31 @@ export const outputSchemas = {
|
|
|
84
84
|
},
|
|
85
85
|
godot_version: { version: z.string(), raw: capturedRaw },
|
|
86
86
|
godot_launch_editor: { launched: z.boolean(), pid: z.number().nullable(), project: z.string() },
|
|
87
|
-
|
|
87
|
+
// 🔴 `running` ALONE WAS THE DEFECT (249, closed 257). It reported that spawn()
|
|
88
|
+
// returned a pid, and callers read it as "runtime_* is reachable now" — which it
|
|
89
|
+
// was not, for 566–3213 ms. `running` still means what it always meant; the
|
|
90
|
+
// three readiness fields are the question that was actually being asked.
|
|
91
|
+
godot_run_project: {
|
|
92
|
+
running: z.boolean(),
|
|
93
|
+
pid: z.number().nullable(),
|
|
94
|
+
scene: z.string().nullable(),
|
|
95
|
+
bridge_ready: z.boolean(),
|
|
96
|
+
bridge_wait_ms: z.number(),
|
|
97
|
+
bridge_note: z.string().nullable(),
|
|
98
|
+
},
|
|
88
99
|
godot_export: { preset: z.string(), output_path: z.string(), exit_code: z.number().nullable(), timed_out: z.boolean(), stdout: z.string(), stderr: z.string() },
|
|
89
100
|
godot_import: { exit_code: z.number().nullable(), timed_out: z.boolean(), stdout: z.string(), stderr: z.string() },
|
|
90
101
|
godot_run_headless_script: { script_path: z.string(), exit_code: z.number().nullable(), timed_out: z.boolean(), stdout: z.string(), stderr: z.string() },
|
|
91
102
|
// ---- Managed processes (tools/processes.ts) ----
|
|
92
|
-
godot_run_managed: {
|
|
103
|
+
godot_run_managed: {
|
|
104
|
+
id: z.string(),
|
|
105
|
+
pid: z.number().nullable(),
|
|
106
|
+
running: z.boolean(),
|
|
107
|
+
scene: z.string().nullable(),
|
|
108
|
+
bridge_ready: z.boolean(),
|
|
109
|
+
bridge_wait_ms: z.number(),
|
|
110
|
+
bridge_note: z.string().nullable(),
|
|
111
|
+
},
|
|
93
112
|
godot_output: {
|
|
94
113
|
id: z.string(),
|
|
95
114
|
exited: z.boolean(),
|
|
@@ -341,7 +360,7 @@ export const outputSchemas = {
|
|
|
341
360
|
test_detect: { framework: z.string(), path: z.string(), version: z.string() },
|
|
342
361
|
test_list: { dir: z.string(), count: z.number(), tests: z.array(z.string()) },
|
|
343
362
|
// ---- Plane D: semantic / LSP (tools/lsp.ts) ----
|
|
344
|
-
gd_completion: { items: z.array(z.object({ label: z.string(), kind: z.string(), detail: z.string(), insertText: z.string() })) },
|
|
363
|
+
gd_completion: { items: z.array(z.object({ label: z.string(), kind: z.string(), detail: z.string(), insertText: z.string() })), truncated: z.boolean() },
|
|
345
364
|
gd_hover: { contents: z.string() },
|
|
346
365
|
gd_definition: { locations: z.array(location) },
|
|
347
366
|
gd_references: { locations: z.array(location) },
|
|
@@ -398,7 +417,7 @@ export const outputSchemas = {
|
|
|
398
417
|
})),
|
|
399
418
|
},
|
|
400
419
|
// ---- Plane D: C# semantic / OmniSharp LSP (tools/cslsp.ts) ----
|
|
401
|
-
cs_completion: { items: z.array(z.object({ label: z.string(), kind: z.string(), detail: z.string(), insertText: z.string() })) },
|
|
420
|
+
cs_completion: { items: z.array(z.object({ label: z.string(), kind: z.string(), detail: z.string(), insertText: z.string() })), truncated: z.boolean() },
|
|
402
421
|
cs_hover: { contents: z.string() },
|
|
403
422
|
cs_definition: { locations: z.array(location) },
|
|
404
423
|
cs_references: { locations: z.array(location) },
|
package/dist/tools/backend.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { remedyClause } from "../bridge.js";
|
|
2
3
|
import { gate } from "../confirm.js";
|
|
3
4
|
import { resolveInsideProject } from "../paths.js";
|
|
4
5
|
/**
|
|
@@ -46,7 +47,7 @@ function fail(err) {
|
|
|
46
47
|
const message = be?.message ?? String(err);
|
|
47
48
|
return {
|
|
48
49
|
isError: true,
|
|
49
|
-
content: [{ type: "text", text: `Backend scaffold error [${code}]: ${message}` }],
|
|
50
|
+
content: [{ type: "text", text: `Backend scaffold error [${code}]: ${message}${remedyClause(err)}` }],
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
// ---- the known SDKs + their capability matrix ----------------------------
|
package/dist/tools/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ok, failPath } from "./lsp-common.js";
|
|
|
7
7
|
import { resolveInsideProject } from "../paths.js";
|
|
8
8
|
import { portFree, portConflictMessage } from "../ports.js";
|
|
9
9
|
import { runDoctorChecks } from "../cli/doctor.js";
|
|
10
|
+
import { waitForRuntimeBridge, notReadyRemedy } from "../readiness.js";
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
/** Run the Godot binary to completion, capturing stdout/stderr. */
|
|
12
13
|
async function runCaptured(cfg, args, timeoutMs, signal) {
|
|
@@ -66,6 +67,8 @@ export function registerCliTools(server, cfg) {
|
|
|
66
67
|
server.registerTool("godot_run_project", {
|
|
67
68
|
title: "Run project",
|
|
68
69
|
description: "Run the project (detached). Optionally start from a specific scene path (res://...). Returns the process id. " +
|
|
70
|
+
"WAITS until the game's runtime bridge answers ping and reports bridge_ready — the game takes roughly half a " +
|
|
71
|
+
"second to three seconds to bind it, and no runtime_* tool is reachable before it does. " +
|
|
69
72
|
"Refuses if the runtime bridge port is already bound — the new game could not host the bridge, and every " +
|
|
70
73
|
"runtime_* call would address the process already holding the port. Use runtime_spawn_peers to drive more " +
|
|
71
74
|
"than one game at once.",
|
|
@@ -77,13 +80,26 @@ export function registerCliTools(server, cfg) {
|
|
|
77
80
|
.describe("Start even though the runtime bridge port is already bound (default false). The new game's runtime " +
|
|
78
81
|
"bridge will NOT be reachable — use only when you want the process for its side effects and will not " +
|
|
79
82
|
"call any runtime_* tool against it."),
|
|
83
|
+
wait_timeout_ms: z
|
|
84
|
+
.number()
|
|
85
|
+
.int()
|
|
86
|
+
.min(0)
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("How long to wait for the runtime bridge to answer ping, in ms (default 15000, the runtime bridge's own " +
|
|
89
|
+
"deadline). 0 returns as soon as the process is spawned, reporting bridge_ready false and " +
|
|
90
|
+
"bridge_wait_ms 0 — waited-not-at-all, which a caller can tell apart from waited-and-lost."),
|
|
80
91
|
},
|
|
81
92
|
},
|
|
82
93
|
// 🔴 MEASURED by the LAUNCHED PROCESS'S OWN ARGV: `res://../example_evil/x.tscn`
|
|
83
94
|
// produced `godot --path …/example res://../example_evil/x.tscn` and the game
|
|
84
95
|
// ran it. The port check comes second — a scene that can never legally run
|
|
85
96
|
// should not first claim the runtime port.
|
|
86
|
-
|
|
97
|
+
//
|
|
98
|
+
// 🔴 AND THE WAIT COMES LAST, WHICH IS THE WHOLE POINT (257). `portFree` above
|
|
99
|
+
// asserts 9081 is FREE; this handler used to prove the bridge was not up and
|
|
100
|
+
// then answer `running: true`, 566–3213 ms before it came up. The field was
|
|
101
|
+
// true and the moment was false — see `readiness.ts`.
|
|
102
|
+
async ({ scene, allow_port_conflict, wait_timeout_ms }) => {
|
|
87
103
|
try {
|
|
88
104
|
if (scene !== undefined)
|
|
89
105
|
resolveInsideProject(scene, cfg.projectPath, "scene");
|
|
@@ -98,7 +114,15 @@ export function registerCliTools(server, cfg) {
|
|
|
98
114
|
if (scene)
|
|
99
115
|
args.push(scene);
|
|
100
116
|
const pid = launchDetached(cfg, args);
|
|
101
|
-
|
|
117
|
+
const readiness = await waitForRuntimeBridge(cfg, wait_timeout_ms ?? cfg.runtimeTimeoutMs);
|
|
118
|
+
return ok({
|
|
119
|
+
running: true,
|
|
120
|
+
pid,
|
|
121
|
+
scene: scene ?? null,
|
|
122
|
+
bridge_ready: readiness.ready,
|
|
123
|
+
bridge_wait_ms: readiness.waited_ms,
|
|
124
|
+
bridge_note: readiness.ready ? null : notReadyRemedy(cfg, readiness.waited_ms),
|
|
125
|
+
});
|
|
102
126
|
});
|
|
103
127
|
registerTaskTool(server, "godot_export", {
|
|
104
128
|
title: "Export project",
|
|
@@ -198,22 +222,35 @@ export function registerCliTools(server, cfg) {
|
|
|
198
222
|
require_live: z
|
|
199
223
|
.boolean()
|
|
200
224
|
.optional()
|
|
201
|
-
.describe("Treat an unreachable bridge as a failure rather than information (default false)"
|
|
225
|
+
.describe("Treat an unreachable bridge as a failure rather than information (default false). True means " +
|
|
226
|
+
"the EDITOR's three bridges — editor, GDScript LSP, GDScript DAP. The runtime bridge lives " +
|
|
227
|
+
"inside the running game and is deliberately not implied; use live_level for that."),
|
|
228
|
+
live_level: z
|
|
229
|
+
.enum(["none", "editor", "runtime", "all"])
|
|
230
|
+
.optional()
|
|
231
|
+
.describe("Which bridges this report insists on: none (default), editor (the three the editor brings " +
|
|
232
|
+
"up), runtime (the one the running game brings up), all. Takes precedence over require_live."),
|
|
202
233
|
include_csharp: z
|
|
203
234
|
.boolean()
|
|
204
235
|
.optional()
|
|
205
236
|
.describe("Also probe OmniSharp / netcoredbg on PATH (the C# planes)"),
|
|
206
237
|
timeout_ms: z.number().int().positive().optional().describe("Per-bridge connect timeout (default 1500)"),
|
|
207
238
|
},
|
|
208
|
-
}, async ({ require_live, include_csharp, timeout_ms }) => {
|
|
239
|
+
}, async ({ require_live, live_level, include_csharp, timeout_ms }) => {
|
|
209
240
|
const report = await runDoctorChecks(cfg, {
|
|
210
241
|
timeoutMs: timeout_ms ?? 1500,
|
|
211
|
-
|
|
242
|
+
liveLevel: live_level ?? (require_live ? "editor" : "none"),
|
|
212
243
|
includeCsharp: include_csharp ?? false,
|
|
213
244
|
});
|
|
245
|
+
// `failed` counted BOTH severities beside `ok: true` — the same sentence
|
|
246
|
+
// the CLI summary used to print, in a field an agent reads instead of a
|
|
247
|
+
// human. Split, so the number that decides the verdict is its own number.
|
|
248
|
+
const failed = report.checks.filter((c) => c.status === "fail");
|
|
214
249
|
return ok({
|
|
215
250
|
ok: report.ok,
|
|
216
|
-
failed:
|
|
251
|
+
failed: failed.length,
|
|
252
|
+
required_failed: failed.filter((c) => c.severity === "required").length,
|
|
253
|
+
informational_failed: failed.filter((c) => c.severity === "info").length,
|
|
217
254
|
checks: report.checks,
|
|
218
255
|
});
|
|
219
256
|
});
|
package/dist/tools/cslsp.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { toFileUri, toFsPath, readFileText, resolveSourceFile } from "../paths.js";
|
|
4
4
|
import { gate } from "../confirm.js";
|
|
5
|
-
import { COMPLETION_KIND, SYMBOL_KIND, ok, fail, markupToString, isMethodNotFound, normalizeLocations, applyTextEdits, normalizeWorkspaceEdit, } from "./lsp-common.js";
|
|
5
|
+
import { COMPLETION_KIND, SYMBOL_KIND, ok, fail, markupToString, isMethodNotFound, normalizeLocations, applyTextEdits, normalizeWorkspaceEdit, capList, COMPLETION_LIMIT } from "./lsp-common.js";
|
|
6
6
|
/**
|
|
7
7
|
* D4 C2 — the C#/.NET semantic plane. Read-only `cs_*` tools mirroring the proven
|
|
8
8
|
* read-only `gd_*` LSP surface, but driven by OmniSharp (spawned over stdio by the
|
|
@@ -127,7 +127,23 @@ export function registerCsLspTools(server, cslsp, cfg) {
|
|
|
127
127
|
line: z.number().int().min(0).describe("0-based line"),
|
|
128
128
|
character: z.number().int().min(0).describe("0-based character"),
|
|
129
129
|
};
|
|
130
|
-
server.registerTool("cs_completion", {
|
|
130
|
+
server.registerTool("cs_completion", {
|
|
131
|
+
title: "C# completion",
|
|
132
|
+
description: "Type-aware C# code completion at a position via OmniSharp. Returns at most max_results items " +
|
|
133
|
+
"(default 200) and sets truncated when the language server offered more — `gd_completion`'s twin, " +
|
|
134
|
+
"capped for the reason measured on that one.",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
...posSchema,
|
|
137
|
+
max_results: z
|
|
138
|
+
.number()
|
|
139
|
+
.int()
|
|
140
|
+
.positive()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe("Maximum completion items to return (default 200). Completion is the one verb whose result size " +
|
|
143
|
+
"is a function of project scope rather than of the cursor — one call was measured at 342,116 bytes, " +
|
|
144
|
+
"more than the entire tool catalogue — so the list is capped and `truncated` says when it was."),
|
|
145
|
+
},
|
|
146
|
+
}, async ({ path, line, character, max_results }) => {
|
|
131
147
|
try {
|
|
132
148
|
const uri = await openAndPos(path);
|
|
133
149
|
const result = await cslsp.request("textDocument/completion", { textDocument: { uri }, position: { line, character } });
|
|
@@ -136,7 +152,7 @@ export function registerCsLspTools(server, cslsp, cfg) {
|
|
|
136
152
|
const it = i;
|
|
137
153
|
return { label: it.label ?? "", kind: it.kind ? COMPLETION_KIND[it.kind] ?? String(it.kind) : "", detail: it.detail ?? "", insertText: it.insertText ?? it.label ?? "" };
|
|
138
154
|
});
|
|
139
|
-
return ok(
|
|
155
|
+
return ok(capList(items, max_results ?? COMPLETION_LIMIT));
|
|
140
156
|
}
|
|
141
157
|
catch (err) {
|
|
142
158
|
return fail(err);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ok, failPath } from "../lsp-common.js";
|
|
2
2
|
import { resolveInsideProject } from "../../paths.js";
|
|
3
|
+
import { remedyClause } from "../../bridge.js";
|
|
3
4
|
/**
|
|
4
5
|
* MCP error envelope for a failed editor-bridge call (never throws to the
|
|
5
6
|
* caller). Distinct from lsp-common's `fail` (which labels errors "LSP error");
|
|
@@ -11,7 +12,7 @@ export function fail(err) {
|
|
|
11
12
|
const message = be?.message ?? String(err);
|
|
12
13
|
return {
|
|
13
14
|
isError: true,
|
|
14
|
-
content: [{ type: "text", text: `Bridge error [${code}]: ${message}` }],
|
|
15
|
+
content: [{ type: "text", text: `Bridge error [${code}]: ${message}${remedyClause(err)}` }],
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
/**
|
package/dist/tools/lsp-common.js
CHANGED
|
@@ -18,6 +18,29 @@ export const SYMBOL_KIND = {
|
|
|
18
18
|
14: "constant", 15: "string", 16: "number", 17: "boolean", 18: "array", 19: "object",
|
|
19
19
|
20: "key", 21: "null", 22: "enumMember", 23: "struct", 24: "event", 25: "operator", 26: "typeParameter",
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* The default ceiling on a completion list, and the only reason there is one.
|
|
23
|
+
*
|
|
24
|
+
* 🔴 MEASURED: ONE `gd_completion` RETURNED 342,116 B (249) — 99.6% of the whole
|
|
25
|
+
* 279-tool `tools/list` surface, in a single result, and pretty-printed and shipped
|
|
26
|
+
* twice by `ok()` below. Completion is the one language-server verb whose result size
|
|
27
|
+
* is a function of PROJECT scope rather than of the cursor: at most positions it is
|
|
28
|
+
* every global class, every autoload and every in-scope built-in. `gd_hover`,
|
|
29
|
+
* `gd_definition` and their C# twins are bounded by the thing under the cursor and are
|
|
30
|
+
* not in this population.
|
|
31
|
+
*
|
|
32
|
+
* 200 is the knowledge family's shipped default — `knowledge.ts` caps grep, symbols and
|
|
33
|
+
* usages there and returns a `truncated` flag beside them. The same number, because a
|
|
34
|
+
* second convention would be a second thing to keep true.
|
|
35
|
+
*/
|
|
36
|
+
export const COMPLETION_LIMIT = 200;
|
|
37
|
+
/**
|
|
38
|
+
* Cap a list and say so. The flag is the whole point: a silently short list reads as a
|
|
39
|
+
* complete one, which is the failure `knowledge.ts` avoided the same way.
|
|
40
|
+
*/
|
|
41
|
+
export function capList(items, max) {
|
|
42
|
+
return items.length > max ? { items: items.slice(0, max), truncated: true } : { items, truncated: false };
|
|
43
|
+
}
|
|
21
44
|
/** MCP success envelope: human-readable JSON text plus the structured content. */
|
|
22
45
|
export function ok(obj) {
|
|
23
46
|
return {
|
package/dist/tools/lsp.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { toFileUri, toFsPath, readFileText, resolveSourceFile } from "../paths.js";
|
|
4
4
|
import { gate } from "../confirm.js";
|
|
5
|
-
import { COMPLETION_KIND, SYMBOL_KIND, ok, fail, markupToString, isMethodNotFound, normalizeLocations, applyTextEdits, } from "./lsp-common.js";
|
|
5
|
+
import { COMPLETION_KIND, SYMBOL_KIND, ok, fail, markupToString, isMethodNotFound, normalizeLocations, applyTextEdits, capList, COMPLETION_LIMIT } from "./lsp-common.js";
|
|
6
6
|
/**
|
|
7
7
|
* The wording this plane's refusals have shipped with, kept verbatim while the guard
|
|
8
8
|
* itself moved to `paths.ts` (161 §8 item 5). `lsp-plane.integration.mjs` pins every
|
|
@@ -238,7 +238,22 @@ export function registerLspTools(server, lsp, cfg) {
|
|
|
238
238
|
line: z.number().int().min(0).describe("0-based line"),
|
|
239
239
|
character: z.number().int().min(0).describe("0-based character"),
|
|
240
240
|
};
|
|
241
|
-
server.registerTool("gd_completion", {
|
|
241
|
+
server.registerTool("gd_completion", {
|
|
242
|
+
title: "GDScript completion",
|
|
243
|
+
description: "Type-aware code completion at a position via the Godot language server. Returns at most max_results " +
|
|
244
|
+
"items (default 200) and sets truncated when the language server offered more.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
...posSchema,
|
|
247
|
+
max_results: z
|
|
248
|
+
.number()
|
|
249
|
+
.int()
|
|
250
|
+
.positive()
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Maximum completion items to return (default 200). Completion is the one verb whose result size " +
|
|
253
|
+
"is a function of project scope rather than of the cursor — one call was measured at 342,116 bytes, " +
|
|
254
|
+
"more than the entire tool catalogue — so the list is capped and `truncated` says when it was."),
|
|
255
|
+
},
|
|
256
|
+
}, async ({ path, line, character, max_results }) => {
|
|
242
257
|
try {
|
|
243
258
|
const uri = await openAndPos(path);
|
|
244
259
|
const result = await lsp.request("textDocument/completion", { textDocument: { uri }, position: { line, character } });
|
|
@@ -247,7 +262,7 @@ export function registerLspTools(server, lsp, cfg) {
|
|
|
247
262
|
const it = i;
|
|
248
263
|
return { label: it.label ?? "", kind: it.kind ? COMPLETION_KIND[it.kind] ?? String(it.kind) : "", detail: it.detail ?? "", insertText: it.insertText ?? it.label ?? "" };
|
|
249
264
|
});
|
|
250
|
-
return ok(
|
|
265
|
+
return ok(capList(items, max_results ?? COMPLETION_LIMIT));
|
|
251
266
|
}
|
|
252
267
|
catch (err) {
|
|
253
268
|
return fail(err);
|
package/dist/tools/netcode.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { remedyClause } from "../bridge.js";
|
|
2
3
|
import { gate } from "../confirm.js";
|
|
3
4
|
import { readFileText, resolveExistingFile, resolveInsideProject } from "../paths.js";
|
|
4
5
|
/**
|
|
@@ -41,7 +42,7 @@ function fail(err) {
|
|
|
41
42
|
const message = be?.message ?? String(err);
|
|
42
43
|
return {
|
|
43
44
|
isError: true,
|
|
44
|
-
content: [{ type: "text", text: `Netcode scaffold error [${code}]: ${message}` }],
|
|
45
|
+
content: [{ type: "text", text: `Netcode scaffold error [${code}]: ${message}${remedyClause(err)}` }],
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
// ------------------------------------------------------------- codegen ----
|
package/dist/tools/processes.js
CHANGED
|
@@ -4,6 +4,7 @@ import { log } from "../logger.js";
|
|
|
4
4
|
import { ok, failPath } from "./lsp-common.js";
|
|
5
5
|
import { resolveInsideProject } from "../paths.js";
|
|
6
6
|
import { portFree, portConflictMessage } from "../ports.js";
|
|
7
|
+
import { waitForRuntimeBridge, notReadyRemedy } from "../readiness.js";
|
|
7
8
|
const LINE_CAP = 5000;
|
|
8
9
|
/**
|
|
9
10
|
* Runs Godot as a MANAGED child (piped stdio) so the host captures ALL stdout/
|
|
@@ -86,6 +87,8 @@ export function registerProcessTools(server, cfg) {
|
|
|
86
87
|
title: "Run project (managed, captured output)",
|
|
87
88
|
description: "Run the project as a managed child process with captured stdout/stderr, so godot_output can read ALL print()/error output. " +
|
|
88
89
|
"Returns a process id. Use this instead of godot_run_project when you want the game's console log. " +
|
|
90
|
+
"WAITS until the game's runtime bridge answers ping and reports bridge_ready, exactly as godot_run_project " +
|
|
91
|
+
"does — no runtime_* tool is reachable before it does. " +
|
|
89
92
|
"Refuses if the runtime bridge port is already bound — the new game could not host the bridge, and every " +
|
|
90
93
|
"runtime_* call would address the process already holding the port. Use runtime_spawn_peers to drive more " +
|
|
91
94
|
"than one game at once.",
|
|
@@ -97,12 +100,24 @@ export function registerProcessTools(server, cfg) {
|
|
|
97
100
|
.describe("Start even though the runtime bridge port is already bound (default false). The new game's runtime " +
|
|
98
101
|
"bridge will NOT be reachable — use only when you want the process for its console output or side " +
|
|
99
102
|
"effects and will not call any runtime_* tool against it."),
|
|
103
|
+
wait_timeout_ms: z
|
|
104
|
+
.number()
|
|
105
|
+
.int()
|
|
106
|
+
.min(0)
|
|
107
|
+
.optional()
|
|
108
|
+
.describe("How long to wait for the runtime bridge to answer ping, in ms (default 15000, the runtime bridge's own " +
|
|
109
|
+
"deadline). 0 returns as soon as the process is spawned, reporting bridge_ready false and " +
|
|
110
|
+
"bridge_wait_ms 0 — waited-not-at-all, which a caller can tell apart from waited-and-lost."),
|
|
100
111
|
},
|
|
101
112
|
},
|
|
102
113
|
// 🔴 `godot_run_project`'s TWIN, in a different file, with the same defect —
|
|
103
114
|
// measured the same way and launching the same escaping argv. 1.42.0's lesson
|
|
104
115
|
// ("the second call site is the interesting one") applied before the fact.
|
|
105
|
-
|
|
116
|
+
//
|
|
117
|
+
// 🔴 AND THE SAME LESSON AGAIN AT 257 FOR THE READINESS RACE. The queue row
|
|
118
|
+
// `run-project-returns-before-bridge` names only `godot_run_project`; this
|
|
119
|
+
// returned `running: true` at the same false moment and no row said so.
|
|
120
|
+
async ({ scene, allow_port_conflict, wait_timeout_ms }) => {
|
|
106
121
|
try {
|
|
107
122
|
if (scene !== undefined)
|
|
108
123
|
resolveInsideProject(scene, cfg.projectPath, "scene");
|
|
@@ -114,7 +129,16 @@ export function registerProcessTools(server, cfg) {
|
|
|
114
129
|
return { isError: true, content: [{ type: "text", text: portConflictMessage(cfg.runtimeHost, cfg.runtimePort) }] };
|
|
115
130
|
}
|
|
116
131
|
const m = registry.run(cfg, scene ? [scene] : []);
|
|
117
|
-
|
|
132
|
+
const readiness = await waitForRuntimeBridge(cfg, wait_timeout_ms ?? cfg.runtimeTimeoutMs);
|
|
133
|
+
return ok({
|
|
134
|
+
id: m.id,
|
|
135
|
+
pid: m.child.pid ?? null,
|
|
136
|
+
running: true,
|
|
137
|
+
scene: scene ?? null,
|
|
138
|
+
bridge_ready: readiness.ready,
|
|
139
|
+
bridge_wait_ms: readiness.waited_ms,
|
|
140
|
+
bridge_note: readiness.ready ? null : notReadyRemedy(cfg, readiness.waited_ms),
|
|
141
|
+
});
|
|
118
142
|
});
|
|
119
143
|
server.registerTool("godot_output", {
|
|
120
144
|
title: "Read managed process output",
|
package/dist/tools/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { isTransportUnavailable, remedyClause } from "../bridge.js";
|
|
2
3
|
import { MAX_PEERS } from "../peers.js";
|
|
3
4
|
import { gate } from "../confirm.js";
|
|
4
5
|
import { ok, failPath } from "./lsp-common.js";
|
|
@@ -26,7 +27,7 @@ function fail(err) {
|
|
|
26
27
|
const be = err;
|
|
27
28
|
return {
|
|
28
29
|
isError: true,
|
|
29
|
-
content: [{ type: "text", text: `Runtime error [${be?.code ?? "error"}]: ${be?.message ?? String(err)}` }],
|
|
30
|
+
content: [{ type: "text", text: `Runtime error [${be?.code ?? "error"}]: ${be?.message ?? String(err)}${remedyClause(err)}` }],
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
@@ -259,14 +260,21 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
259
260
|
"Capture the baseline earlier with runtime_get_monitors and pass it back inline. Pass direction is inferred " +
|
|
260
261
|
"(time/fps is higher-better; every other monitor is lower-better) unless overridden per key.",
|
|
261
262
|
inputSchema: {
|
|
262
|
-
// 🔴 226 §2 —
|
|
263
|
-
//
|
|
264
|
-
// this
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
263
|
+
// 🔴 226 §2 — `1e999` MUST NOT REACH `float(baseline[key])` IN runtime_bridge.gd,
|
|
264
|
+
// where it arrives as `inf` and comes back as `1e99999`: the one non-finite path in
|
|
265
|
+
// this tree that needs no engine edge case at all, only a well-formed call.
|
|
266
|
+
//
|
|
267
|
+
// 🆕 255 — AND THE `.finite()` THAT CLOSED IT IS GONE, BECAUSE THE BASE TYPE CLOSED
|
|
268
|
+
// IT. Under the major this tree left, `z.number()` accepted ±Infinity and the
|
|
269
|
+
// refinement was the whole guard; under the installed one `z.number()` refuses it
|
|
270
|
+
// and `.finite()` contributes NO check and NO bag entry — measured, not assumed:
|
|
271
|
+
// `wire_invisible_gate.mjs` found the roster row it had been pinned by standing over
|
|
272
|
+
// nothing. A call that adds no rule is a sentence wearing a rule's clothes, and this
|
|
273
|
+
// repository has spent three sessions deleting those. What holds the door now is
|
|
274
|
+
// asserted in `finiteness.test.ts` against the INSTALLED zod, so a future major that
|
|
275
|
+
// relaxes `z.number()` reddens there rather than silently re-opening this line.
|
|
268
276
|
baseline: z
|
|
269
|
-
.record(z.string(), z.number()
|
|
277
|
+
.record(z.string(), z.number())
|
|
270
278
|
.describe("Monitor key -> baseline value (capture earlier via runtime_get_monitors)"),
|
|
271
279
|
tolerance: z
|
|
272
280
|
.number()
|
|
@@ -357,7 +365,9 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
357
365
|
title: "Runtime await condition",
|
|
358
366
|
description: "Poll a property on a LIVE node until it satisfies a comparison, or a timeout elapses (read-only verification). " +
|
|
359
367
|
"op is eq | ne | gt | ge | lt | le (the ordered operators require numeric values). Use this to wait for the " +
|
|
360
|
-
"running game to reach a state before asserting on it — e.g. wait for hp le 0, then runtime_assert_screen_text."
|
|
368
|
+
"running game to reach a state before asserting on it — e.g. wait for hp le 0, then runtime_assert_screen_text. " +
|
|
369
|
+
"An unreachable runtime bridge is retried until the timeout rather than failing on the first poll, so this " +
|
|
370
|
+
"tool can also be used to wait for a game that is still booting.",
|
|
361
371
|
inputSchema: {
|
|
362
372
|
path: z.string().describe("Node path (relative to the current scene; '/root/...' absolute allowed)"),
|
|
363
373
|
property: z.string().describe("Property to read on each poll"),
|
|
@@ -388,7 +398,21 @@ export function registerRuntimeTools(server, runtime, peers, config) {
|
|
|
388
398
|
res = (await client.request("runtime.get_property", { path, property }));
|
|
389
399
|
}
|
|
390
400
|
catch (err) {
|
|
391
|
-
|
|
401
|
+
// 🔴 THE ONE WAITING TOOL DID NOT WAIT FOR THE ONE THING WORTH WAITING
|
|
402
|
+
// FOR (249, closed 257). Every error ended the loop here, so a caller
|
|
403
|
+
// that set timeout_ms 15000 against a game that was still booting got
|
|
404
|
+
// an answer in 2 ms with polls 1 — `deadline` and `sleep(interval)`
|
|
405
|
+
// are both downstream of this catch and were never reached.
|
|
406
|
+
//
|
|
407
|
+
// `bridge_unavailable` is the transport saying *not yet*; every other
|
|
408
|
+
// code is the game saying *no* — a missing node, an unknown property,
|
|
409
|
+
// a refused method — and retrying those would turn a precise failure
|
|
410
|
+
// into a slow one. So the retry is scoped to the one code, and it is
|
|
411
|
+
// still bounded by the caller's own deadline rather than by a new one.
|
|
412
|
+
if (!isTransportUnavailable(err) || Date.now() >= deadline)
|
|
413
|
+
return fail(err);
|
|
414
|
+
await sleep(interval);
|
|
415
|
+
continue;
|
|
392
416
|
}
|
|
393
417
|
last = res?.value ?? null;
|
|
394
418
|
if (compareValues(last, value, operator)) {
|
package/dist/tools/tabletop.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { remedyClause } from "../bridge.js";
|
|
3
4
|
import { gate } from "../confirm.js";
|
|
4
5
|
import { readFileText, resolveExistingFile, resolveInsideProject, resolveWriteTarget } from "../paths.js";
|
|
5
6
|
/**
|
|
@@ -36,7 +37,7 @@ function fail(err) {
|
|
|
36
37
|
const message = be?.message ?? String(err);
|
|
37
38
|
return {
|
|
38
39
|
isError: true,
|
|
39
|
-
content: [{ type: "text", text: `Tabletop compose error [${code}]: ${message}` }],
|
|
40
|
+
content: [{ type: "text", text: `Tabletop compose error [${code}]: ${message}${remedyClause(err)}` }],
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
/** A bad-input failure that reads like a bridge error but never reaches the bridge. */
|
package/dist/wire-defaults.js
CHANGED
|
@@ -6,10 +6,19 @@ import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
|
6
6
|
* conforming peer does not already assume. Both are removed on the way out.
|
|
7
7
|
*
|
|
8
8
|
* 1. 🔴 `"$schema": "http://json-schema.org/draft-07/schema#"`, inside EVERY input and
|
|
9
|
-
* output schema. Nobody in this repo writes it: the SDK converts our Zod
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* output schema. Nobody in this repo writes it: the SDK converts our Zod and asks for
|
|
10
|
+
* draft-07 while doing it.
|
|
11
|
+
*
|
|
12
|
+
* 🆕 255 — AND MOVING TO THE ZOD-v4 BRANCH DID NOT CHANGE THAT, WHICH THE PARAGRAPH
|
|
13
|
+
* THIS REPLACES IMPLIED IT WOULD. The old text read "the `target` option is read only
|
|
14
|
+
* by the Zod-v4 branch", which is true and useless: `server/mcp.js` calls
|
|
15
|
+
* `toJsonSchemaCompat` with no `target` at either version, and the v4 branch maps an
|
|
16
|
+
* absent target to `'draft-7'` before handing it to zod's own converter. So the
|
|
17
|
+
* declaration on the wire is the SDK's choice and not the library's, it survived a
|
|
18
|
+
* major bump unchanged, and this strip stays necessary — the same conclusion the
|
|
19
|
+
* migration spike reached from the outside, now measured from inside the tree.
|
|
20
|
+
*
|
|
21
|
+
* MCP fixes the DEFAULT dialect at JSON Schema 2020-12 and
|
|
13
22
|
* requires every implementation to support it; a `$schema` field is an EXPLICIT
|
|
14
23
|
* SWITCH to a dialect nobody is obliged to support and which a peer MUST reject
|
|
15
24
|
* gracefully if it does not. So this is not decoration and it is not a trim:
|
|
@@ -49,12 +58,65 @@ export const DIALECT_SENSITIVE = [
|
|
|
49
58
|
["exclusiveMinimum", (v) => typeof v === "boolean"], // draft-04 form
|
|
50
59
|
["exclusiveMaximum", (v) => typeof v === "boolean"], // draft-04 form
|
|
51
60
|
["dependencies", () => true], // split into dependentSchemas/dependentRequired
|
|
52
|
-
["definitions", () => true], // $defs in 2020-12
|
|
53
|
-
["$defs", () => true], // 2020-12 only — the label would be wrong already
|
|
54
61
|
["prefixItems", () => true], // 2020-12 only
|
|
55
62
|
["unevaluatedProperties", () => true], // 2020-12 only
|
|
56
63
|
["unevaluatedItems", () => true], // 2020-12 only
|
|
57
64
|
];
|
|
65
|
+
/**
|
|
66
|
+
* The definition CONTAINERS, which are the one entry above that could not be decided from
|
|
67
|
+
* its own value.
|
|
68
|
+
*
|
|
69
|
+
* 🔴 255 — AND THEY WERE ON THE LIST FOR A SPELLING DIFFERENCE THAT DOES NOT REACH A
|
|
70
|
+
* REFERENCE. `definitions` is draft-07's container and `$defs` is 2020-12's, so each is an
|
|
71
|
+
* unknown keyword under the other dialect — and an unknown keyword is one both specs
|
|
72
|
+
* require a validator to IGNORE. What actually resolves a reference is `$ref`, and
|
|
73
|
+
* `"#/definitions/X"` is a URI-reference resolved as a JSON Pointer into the same schema
|
|
74
|
+
* resource under BOTH dialects: the pointer walks the document by key, and it does not
|
|
75
|
+
* care whether the key it walks through is vocabulary or not.
|
|
76
|
+
*
|
|
77
|
+
* So the container is dialect-bound only when something inside it is REACHED BY A NAME
|
|
78
|
+
* rather than by a path — `$anchor` (2020-12 only) or a nested `$id` (a new base URI in
|
|
79
|
+
* both, resolved differently) — or when a `$ref` anywhere in the document is not a plain
|
|
80
|
+
* local pointer. That is the predicate below, and it is a property of the WHOLE document,
|
|
81
|
+
* which is why it could not live in the table above.
|
|
82
|
+
*
|
|
83
|
+
* 🔴 THIS WAS MEASURED BEFORE IT WAS BELIEVED, AND IT IS MEASURED AGAIN ON EVERY RUN.
|
|
84
|
+
* `wire_defaults.test.ts` compiles the STRIPPED schema under a strict 2020-12 validator
|
|
85
|
+
* and the declared one under draft-07, and compares their verdicts on probe instances —
|
|
86
|
+
* Ajv is a second opinion with no stake in this argument. The fail-safe is not weakened:
|
|
87
|
+
* a container that is reached by name, or a reference that leaves the document, still
|
|
88
|
+
* keeps its declaration.
|
|
89
|
+
*/
|
|
90
|
+
const CONTAINER_KEYS = ["definitions", "$defs"];
|
|
91
|
+
/** Reached by NAME rather than by path — the two ways a container stops being inert. */
|
|
92
|
+
const NAME_REACHED = new Set(["$id", "$anchor", "$dynamicAnchor", "$dynamicRef", "$recursiveAnchor", "$recursiveRef"]);
|
|
93
|
+
/**
|
|
94
|
+
* Can a definition container be renamed between dialects without changing meaning?
|
|
95
|
+
* A property of the whole document: every `$ref` in it must be a plain local pointer, and
|
|
96
|
+
* nothing anywhere may be reached by name.
|
|
97
|
+
*/
|
|
98
|
+
export function containersInert(doc) {
|
|
99
|
+
let inert = true;
|
|
100
|
+
const walk = (node) => {
|
|
101
|
+
if (!inert || node === null || typeof node !== "object")
|
|
102
|
+
return;
|
|
103
|
+
if (Array.isArray(node))
|
|
104
|
+
return node.forEach(walk);
|
|
105
|
+
for (const [k, v] of Object.entries(node)) {
|
|
106
|
+
if (NAME_REACHED.has(k)) {
|
|
107
|
+
inert = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (k === "$ref" && !(typeof v === "string" && v.startsWith("#/"))) {
|
|
111
|
+
inert = false;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
walk(v);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
walk(doc);
|
|
118
|
+
return inert;
|
|
119
|
+
}
|
|
58
120
|
/** Keys under these keywords are AUTHOR-CHOSEN NAMES, not schema vocabulary. */
|
|
59
121
|
const NAME_BEARING = new Set(["properties", "patternProperties", "definitions", "$defs"]);
|
|
60
122
|
/**
|
|
@@ -65,12 +127,20 @@ const NAME_BEARING = new Set(["properties", "patternProperties", "definitions",
|
|
|
65
127
|
* have kept the declaration on the whole surface; the hit was `scene_get_dependencies`'s
|
|
66
128
|
* output field, a property NAMED dependencies. A walker that cannot tell a keyword from a
|
|
67
129
|
* key reads an author's vocabulary as the protocol's.
|
|
130
|
+
*
|
|
131
|
+
* 🔴 THE CONTAINER ANSWER IS COMPUTED ONCE, AT THE DOCUMENT, AND CARRIED DOWN. Whether
|
|
132
|
+
* `definitions` is load-bearing is not readable from `definitions` — see `containersInert`
|
|
133
|
+
* — so the recursion threads the document's answer rather than re-deriving a different one
|
|
134
|
+
* at each level, which is how the same schema would get two verdicts from one walk.
|
|
68
135
|
*/
|
|
69
136
|
export function dialectSensitive(node, inNamePosition = false) {
|
|
137
|
+
return walkSensitive(node, inNamePosition, containersInert(node));
|
|
138
|
+
}
|
|
139
|
+
function walkSensitive(node, inNamePosition, inert) {
|
|
70
140
|
if (node === null || typeof node !== "object")
|
|
71
141
|
return false;
|
|
72
142
|
if (Array.isArray(node))
|
|
73
|
-
return node.some((v) =>
|
|
143
|
+
return node.some((v) => walkSensitive(v, false, inert));
|
|
74
144
|
const obj = node;
|
|
75
145
|
// draft-07 ignores keywords adjacent to $ref; 2020-12 applies them.
|
|
76
146
|
if (obj.$ref !== undefined && Object.keys(obj).length > 1)
|
|
@@ -80,8 +150,10 @@ export function dialectSensitive(node, inNamePosition = false) {
|
|
|
80
150
|
for (const [key, matches] of DIALECT_SENSITIVE)
|
|
81
151
|
if (k === key && matches(v))
|
|
82
152
|
return true;
|
|
153
|
+
if (!inert && CONTAINER_KEYS.includes(k))
|
|
154
|
+
return true;
|
|
83
155
|
}
|
|
84
|
-
if (
|
|
156
|
+
if (walkSensitive(v, NAME_BEARING.has(k), inert))
|
|
85
157
|
return true;
|
|
86
158
|
}
|
|
87
159
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.75.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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
|
-
"zod": "^
|
|
59
|
+
"zod": "^4.4.3"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^20.14.0",
|