breakpoint-mcp 1.78.3 → 1.80.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/bridge.js +89 -5
- package/dist/config.js +13 -0
- package/dist/csdap.js +65 -22
- package/dist/cslsp.js +13 -8
- package/dist/dap.js +180 -21
- package/dist/exit-cause.js +13 -7
- package/dist/index.js +1 -1
- package/dist/lsp.js +28 -9
- package/dist/readiness.js +26 -7
- package/dist/schemas.js +10 -2
- package/dist/timeout-cause.js +66 -0
- package/dist/timeout-caveat.js +15 -4
- package/dist/tools/csdap.js +31 -9
- package/dist/tools/dap.js +40 -6
- package/dist/tools/editor/common.js +7 -2
- package/dist/tools/lsp-common.js +6 -3
- package/dist/tools/processes.js +6 -1
- package/package.json +3 -2
package/dist/tools/csdap.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DapError } from "../dap.js";
|
|
2
|
+
import { DapError, DAP_TIMEOUT_CODE, unorderedHandshakeWarning } from "../dap.js";
|
|
3
|
+
import { remedyClause } from "../bridge.js";
|
|
3
4
|
import { resolveSourceFile } from "../paths.js";
|
|
4
5
|
import { gate } from "../confirm.js";
|
|
5
6
|
import { ok } from "./lsp-common.js";
|
|
@@ -32,15 +33,28 @@ function fail(err) {
|
|
|
32
33
|
content: [{
|
|
33
34
|
type: "text",
|
|
34
35
|
text: `C# DAP error [${command}]: the debug adapter reported a failure with no message. ` +
|
|
35
|
-
`The request was rejected; nothing was changed by it
|
|
36
|
+
`The request was rejected; nothing was changed by it.${remedyClause(err)}`,
|
|
36
37
|
}],
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
return {
|
|
40
41
|
isError: true,
|
|
41
|
-
content: [{ type: "text", text: `C# DAP error [${command}]: ${message}` }],
|
|
42
|
+
content: [{ type: "text", text: `C# DAP error [${command}]: ${message}${remedyClause(err)}` }],
|
|
42
43
|
};
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Record whether the adapter announced itself before its breakpoints were applied — the
|
|
47
|
+
* twin of `tools/dap.ts`'s, for the same measured reason (267). Kept per-plane rather than
|
|
48
|
+
* shared because the two planes' result objects are built by different code and a helper
|
|
49
|
+
* imported across them would be the only thing they share.
|
|
50
|
+
*/
|
|
51
|
+
function reportHandshakeOrder(result, seen, waitedMs) {
|
|
52
|
+
result.initialized_seen = seen;
|
|
53
|
+
if (seen)
|
|
54
|
+
return;
|
|
55
|
+
const note = unorderedHandshakeWarning(waitedMs);
|
|
56
|
+
result.warning = typeof result.warning === "string" && result.warning.length > 0 ? `${result.warning} ${note}` : note;
|
|
57
|
+
}
|
|
44
58
|
/** Throw a host refusal — rendered verbatim by `fail()`, never as an adapter error. */
|
|
45
59
|
function refuse(message, code) {
|
|
46
60
|
throw Object.assign(new Error(message), { refusal: true, code });
|
|
@@ -50,9 +64,13 @@ function refuse(message, code) {
|
|
|
50
64
|
* adapter-reported failure or a dropped connection). Used to turn a
|
|
51
65
|
* setVariable / evaluate non-response into a clear message rather than the
|
|
52
66
|
* generic timeout — the same F1 discipline the GDScript DAP plane uses.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 A CODE SINCE 268, A REGEX OVER ENGLISH BEFORE IT — see `tools/dap.ts`'s twin for
|
|
69
|
+
* the whole argument. Both planes carried the identical prose predicate, which is why
|
|
70
|
+
* the row named two files and not one.
|
|
53
71
|
*/
|
|
54
72
|
function isDapTimeout(err) {
|
|
55
|
-
return err instanceof DapError &&
|
|
73
|
+
return err instanceof DapError && err.code === DAP_TIMEOUT_CODE;
|
|
56
74
|
}
|
|
57
75
|
/**
|
|
58
76
|
* D4 C3 — the C#/.NET debugging plane. `cs_dbg_*` tools mirroring the read/inspect
|
|
@@ -239,14 +257,16 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
239
257
|
};
|
|
240
258
|
}
|
|
241
259
|
try {
|
|
242
|
-
await dap.start("launch", {
|
|
260
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("launch", {
|
|
243
261
|
program: resolvedProgram,
|
|
244
262
|
args: resolvedArgs,
|
|
245
263
|
cwd: cfg.csDapProjectPath,
|
|
246
264
|
stopAtEntry: stop_on_entry ?? false,
|
|
247
265
|
justMyCode: just_my_code ?? true,
|
|
248
|
-
});
|
|
249
|
-
|
|
266
|
+
}, cfg.dapRequireInitialized);
|
|
267
|
+
const result = { session_id: "csharp", state: dap.state };
|
|
268
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
269
|
+
return ok(result);
|
|
250
270
|
}
|
|
251
271
|
catch (err) {
|
|
252
272
|
return fail(err);
|
|
@@ -280,8 +300,10 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
280
300
|
`was previously reported as "running" against a process that does not exist.`, "no_such_process");
|
|
281
301
|
}
|
|
282
302
|
}
|
|
283
|
-
await dap.start("attach", { processId: process_id });
|
|
284
|
-
|
|
303
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("attach", { processId: process_id }, cfg.dapRequireInitialized);
|
|
304
|
+
const result = { session_id: "csharp", state: dap.state };
|
|
305
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
306
|
+
return ok(result);
|
|
285
307
|
}
|
|
286
308
|
catch (err) {
|
|
287
309
|
return fail(err);
|
package/dist/tools/dap.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DapError } from "../dap.js";
|
|
2
|
+
import { DapError, DAP_TIMEOUT_CODE, unorderedHandshakeWarning } from "../dap.js";
|
|
3
|
+
import { remedyClause } from "../bridge.js";
|
|
3
4
|
import { resolveSourceFile } from "../paths.js";
|
|
4
5
|
import { gate } from "../confirm.js";
|
|
5
6
|
import { ok } from "./lsp-common.js";
|
|
@@ -116,24 +117,38 @@ function fail(err) {
|
|
|
116
117
|
// asked. Same distinction lsp-common.fail() already draws for the LSP planes.
|
|
117
118
|
const e = err;
|
|
118
119
|
if (err instanceof DapRefusal || e?.refusal) {
|
|
119
|
-
return { isError: true, content: [{ type: "text", text: e.message ?? String(err) }] };
|
|
120
|
+
return { isError: true, content: [{ type: "text", text: `${e.message ?? String(err)}${remedyClause(err)}` }] };
|
|
120
121
|
}
|
|
121
122
|
// An adapter can answer a failure with an EMPTY message (netcoredbg did, on
|
|
122
123
|
// setVariable). `?? String(err)` only covers an ABSENT one, so the whole answer
|
|
123
124
|
// became a label and a colon.
|
|
124
125
|
const detail = e.message && e.message.trim() !== "" ? e.message : String(err);
|
|
126
|
+
// 🆕 267 — `remedyClause(err)` reads the `remedy` FIELD, which `DapError` gained this
|
|
127
|
+
// release. Before it, this plane's next actions were pasted inside the message at the
|
|
128
|
+
// raise site and this renderer had nothing to append; check 28 could not see them and a
|
|
129
|
+
// reword dropped one silently. Empty string when the site has no answer, so a rejection
|
|
130
|
+
// relaying the adapter's own words is byte-identical to what it was.
|
|
125
131
|
return {
|
|
126
132
|
isError: true,
|
|
127
|
-
content: [{ type: "text", text: `DAP error [${e.command ?? "error"}]: ${detail}` }],
|
|
133
|
+
content: [{ type: "text", text: `DAP error [${e.command ?? "error"}]: ${detail}${remedyClause(err)}` }],
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
/**
|
|
131
137
|
* True when `err` is a DAP request that hit its own request deadline (as opposed to an
|
|
132
138
|
* adapter-reported failure or a dropped connection). Used to turn the setVariable /
|
|
133
139
|
* evaluate hangs on advertised-but-unimplemented Godot builds into a clear message.
|
|
140
|
+
*
|
|
141
|
+
* 🔴 **THIS READ THE MESSAGE BODY UNTIL 268, AND TWO SHIPPED TOOLS BRANCHED ON IT.** The
|
|
142
|
+
* predicate was `/timed out after/.test(err.message)`, so `dbg_evaluate`'s and
|
|
143
|
+
* `dbg_set_variable`'s specific answers were selected by a regex over English — a copy
|
|
144
|
+
* edit at the raise site would have silently returned every caller to a bare timeout,
|
|
145
|
+
* with every test in the suite still green. 267 declined to reword those messages for
|
|
146
|
+
* exactly that reason and asserted the wording instead, which is a test defending a
|
|
147
|
+
* defect rather than a behaviour. The raise site now sets `DAP_TIMEOUT_CODE` and this
|
|
148
|
+
* reads it; check 32 refuses the regex coming back.
|
|
134
149
|
*/
|
|
135
150
|
function isDapTimeout(err) {
|
|
136
|
-
return err instanceof DapError &&
|
|
151
|
+
return err instanceof DapError && err.code === DAP_TIMEOUT_CODE;
|
|
137
152
|
}
|
|
138
153
|
/**
|
|
139
154
|
* 🔴 The modifier feature-detection lives in `dap.ts` (the client), NOT here, and that
|
|
@@ -144,6 +159,23 @@ function isDapTimeout(err) {
|
|
|
144
159
|
* it, the breakpoint halted every frame, and the result carried no warning at all.
|
|
145
160
|
* Detection now happens where the modifiers go on the wire, so every path gets it.
|
|
146
161
|
*/
|
|
162
|
+
/**
|
|
163
|
+
* Record whether the adapter announced itself before its breakpoints were applied.
|
|
164
|
+
*
|
|
165
|
+
* 🔴 THE KEY IS ALWAYS PRESENT, THE WARNING ONLY WHEN IT IS FALSE (267). A field that
|
|
166
|
+
* appears only on the bad path is a field a caller cannot branch on — it cannot tell
|
|
167
|
+
* *this adapter was conformant* from *this host is too old to say* — which is the shape
|
|
168
|
+
* `required-any-reachability` is open about one layer up. The warning is the human half
|
|
169
|
+
* and merges rather than clobbers, for the same reason `reportDroppedModifiers` does:
|
|
170
|
+
* an out-of-order handshake and an unhonoured stop-on-entry can both be true at once.
|
|
171
|
+
*/
|
|
172
|
+
function reportHandshakeOrder(result, seen, waitedMs) {
|
|
173
|
+
result.initialized_seen = seen;
|
|
174
|
+
if (seen)
|
|
175
|
+
return;
|
|
176
|
+
const note = unorderedHandshakeWarning(waitedMs);
|
|
177
|
+
result.warning = typeof result.warning === "string" && result.warning.length > 0 ? `${result.warning} ${note}` : note;
|
|
178
|
+
}
|
|
147
179
|
const warnDropped = (dropped) => `The connected Godot debug adapter does not support ${dropped.join(", ")} on breakpoints (it advertises ` +
|
|
148
180
|
`${dropped.length > 1 ? "them" : "it"} unsupported), so ${dropped.length > 1 ? "they were" : "it was"} dropped — ` +
|
|
149
181
|
`the affected breakpoint(s) will halt unconditionally.`;
|
|
@@ -312,8 +344,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
312
344
|
};
|
|
313
345
|
}
|
|
314
346
|
try {
|
|
315
|
-
const { entryStopSeen } = await dap.start("launch", { project: cfg.projectPath, scene: scene ?? "main", stopOnEntry: stop_on_entry ?? false }, stop_on_entry ? ENTRY_STOP_WAIT_MS : 0);
|
|
347
|
+
const { entryStopSeen, initializedSeen, initializedWaitMs } = await dap.start("launch", { project: cfg.projectPath, scene: scene ?? "main", stopOnEntry: stop_on_entry ?? false }, stop_on_entry ? ENTRY_STOP_WAIT_MS : 0, cfg.dapRequireInitialized);
|
|
316
348
|
const result = { session_id: "godot", state: dap.state, scene: scene ?? "main" };
|
|
349
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
317
350
|
if (stop_on_entry) {
|
|
318
351
|
// 🔴 Measured on Godot 4.7: stopOnEntry is ignored outright — the game ran to
|
|
319
352
|
// completion and printed its _ready() line while the tool answered a bare
|
|
@@ -348,8 +381,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
348
381
|
},
|
|
349
382
|
}, async ({ address, port }) => {
|
|
350
383
|
try {
|
|
351
|
-
await dap.start("attach", { address: address ?? "127.0.0.1", port });
|
|
384
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("attach", { address: address ?? "127.0.0.1", port }, 0, cfg.dapRequireInitialized);
|
|
352
385
|
const result = { session_id: "godot", state: dap.state };
|
|
386
|
+
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
353
387
|
reportDroppedModifiers(result);
|
|
354
388
|
return ok(result);
|
|
355
389
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { ok, failPath } from "../lsp-common.js";
|
|
2
2
|
import { resolveInsideProject } from "../../paths.js";
|
|
3
|
-
import { remedyClause } from "../../bridge.js";
|
|
3
|
+
import { remedyClause, bridgeErrorLabel } from "../../bridge.js";
|
|
4
4
|
/**
|
|
5
5
|
* MCP error envelope for a failed editor-bridge call (never throws to the
|
|
6
6
|
* caller). Distinct from lsp-common's `fail` (which labels errors "LSP error");
|
|
7
7
|
* this one labels them "Bridge error".
|
|
8
|
+
*
|
|
9
|
+
* 🔴 THE LABEL IS BUILT BY `bridgeErrorLabel` AND NOT WRITTEN HERE (268), because
|
|
10
|
+
* `timeout-caveat.ts` matches against it to decide whether a mutating tool warns that
|
|
11
|
+
* a timed-out change may already have landed. Inlining the template again would put the
|
|
12
|
+
* two spellings back in two files with nothing joining them.
|
|
8
13
|
*/
|
|
9
14
|
export function fail(err) {
|
|
10
15
|
const be = err;
|
|
@@ -12,7 +17,7 @@ export function fail(err) {
|
|
|
12
17
|
const message = be?.message ?? String(err);
|
|
13
18
|
return {
|
|
14
19
|
isError: true,
|
|
15
|
-
content: [{ type: "text", text:
|
|
20
|
+
content: [{ type: "text", text: `${bridgeErrorLabel(code)}: ${message}${remedyClause(err)}` }],
|
|
16
21
|
};
|
|
17
22
|
}
|
|
18
23
|
/**
|
package/dist/tools/lsp-common.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// MarkupContent), so these live once here rather than being duplicated per plane.
|
|
5
5
|
// The generic MCP success-envelope helper `ok()` is the single shared envelope
|
|
6
6
|
// shape and is imported by the other tool modules too, not only the LSP planes.
|
|
7
|
+
import { remedyClause } from "../bridge.js";
|
|
7
8
|
// LSP CompletionItemKind numeric -> readable name.
|
|
8
9
|
export const COMPLETION_KIND = {
|
|
9
10
|
1: "text", 2: "method", 3: "function", 4: "constructor", 5: "field", 6: "variable",
|
|
@@ -57,12 +58,14 @@ export function fail(err) {
|
|
|
57
58
|
if (e?.refusal) {
|
|
58
59
|
return {
|
|
59
60
|
isError: true,
|
|
60
|
-
content: [{ type: "text", text: e.message ?? String(err) }],
|
|
61
|
+
content: [{ type: "text", text: `${e.message ?? String(err)}${remedyClause(err)}` }],
|
|
61
62
|
};
|
|
62
63
|
}
|
|
64
|
+
// 🆕 267 — see `tools/dap.ts`. `LspError` gained a `remedy` field this release; this is
|
|
65
|
+
// the one place both LSP planes render, so both get the clause from one edit.
|
|
63
66
|
return {
|
|
64
67
|
isError: true,
|
|
65
|
-
content: [{ type: "text", text: `LSP error [${e.code ?? "error"}]: ${e.message ?? String(err)}` }],
|
|
68
|
+
content: [{ type: "text", text: `LSP error [${e.code ?? "error"}]: ${e.message ?? String(err)}${remedyClause(err)}` }],
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
@@ -82,7 +85,7 @@ export function failPath(err) {
|
|
|
82
85
|
const be = err;
|
|
83
86
|
return {
|
|
84
87
|
isError: true,
|
|
85
|
-
content: [{ type: "text", text: `Path error [${be?.code ?? "error"}]: ${be?.message ?? String(err)}` }],
|
|
88
|
+
content: [{ type: "text", text: `Path error [${be?.code ?? "error"}]: ${be?.message ?? String(err)}${remedyClause(err)}` }],
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
/**
|
package/dist/tools/processes.js
CHANGED
|
@@ -157,7 +157,12 @@ export function registerProcessTools(server, cfg) {
|
|
|
157
157
|
const since = since_seq ?? 0;
|
|
158
158
|
const want = stream ?? "both";
|
|
159
159
|
const lines = m.lines.filter((l) => l.seq > since && (want === "both" || l.stream === want));
|
|
160
|
-
|
|
160
|
+
// 🆕 267 — `signal` on the wire. `exit_code` alone could not distinguish a child
|
|
161
|
+
// that chose to exit 0 from one the OS killed: both answered `exited: true` with a
|
|
162
|
+
// null-or-number code, and a SIGKILLed child answered `exit_code: null`, which reads
|
|
163
|
+
// exactly like a process that has not finished. The registry has captured the signal
|
|
164
|
+
// since 266; this is the key that lets a caller read it.
|
|
165
|
+
return ok({ id, exited: m.exited, exit_code: m.exitCode, signal: m.exitSignal, latest_seq: m.seq, lines });
|
|
161
166
|
});
|
|
162
167
|
server.registerTool("godot_stop", {
|
|
163
168
|
title: "Stop managed process",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.80.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",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"start": "node dist/index.js",
|
|
44
44
|
"dev": "tsc && node dist/index.js",
|
|
45
45
|
"stage-addon": "node scripts/stage-addon.mjs",
|
|
46
|
-
"prepublishOnly": "npm run build && npm run stage-addon",
|
|
46
|
+
"prepublishOnly": "npm run build && npm run stage-addon && node scripts/publish_guard.mjs",
|
|
47
|
+
"publish-guard": "node scripts/publish_guard.mjs --selftest && node scripts/publish_guard.mjs",
|
|
47
48
|
"path-cohort": "node scripts/path-cohort.mjs",
|
|
48
49
|
"tautology-gate": "node scripts/tautology_gate.selftest.mjs && node scripts/tautology_gate.mjs",
|
|
49
50
|
"scope-gate": "python3 ../scripts/scope_gate.py",
|