breakpoint-mcp 1.79.0 โ 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 +21 -8
- package/dist/dap.js +73 -13
- package/dist/index.js +1 -1
- package/dist/timeout-cause.js +18 -0
- package/dist/timeout-caveat.js +15 -4
- package/dist/tools/csdap.js +8 -4
- package/dist/tools/dap.js +13 -4
- package/dist/tools/editor/common.js +7 -2
- package/package.json +3 -2
package/dist/bridge.js
CHANGED
|
@@ -65,6 +65,68 @@ export function remedyClause(err) {
|
|
|
65
65
|
const r = err?.remedy;
|
|
66
66
|
return typeof r === "string" && r !== "" ? ` โ ${r}` : "";
|
|
67
67
|
}
|
|
68
|
+
/** The code this class raises when a bridge request outlives its own deadline. */
|
|
69
|
+
export const BRIDGE_TIMEOUT_CODE = "timeout";
|
|
70
|
+
/**
|
|
71
|
+
* The label `tools/editor/common.ts`'s `fail()` puts in front of a bridge failure.
|
|
72
|
+
*
|
|
73
|
+
* ๐ด **ONE BUILDER, TWO READERS, AND THE SECOND ONE WAS A STRING LITERAL UNTIL 268.**
|
|
74
|
+
* `timeout-caveat.ts` decides whether a mutating tool gets its retry caveat by
|
|
75
|
+
* looking for `Bridge error [timeout]` inside the rendered envelope โ a substring it
|
|
76
|
+
* spelled out itself, one file away from the template that produces it. That is
|
|
77
|
+
* `dap-timeout-predicate-reads-prose` again with a far wider blast radius: a reword of
|
|
78
|
+
* `fail()` would silently stop every non-idempotent tool from warning that a timed-out
|
|
79
|
+
* mutation may already have landed, and nothing compares the two spellings. Derived here
|
|
80
|
+
* so the two move together, and check 32 refuses a fresh literal.
|
|
81
|
+
*/
|
|
82
|
+
export function bridgeErrorLabel(code) {
|
|
83
|
+
return `Bridge error [${code}]`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The code raised when the request could not be handed to the socket at all.
|
|
87
|
+
*
|
|
88
|
+
* ๐ด **ONE WORD, TWO PRODUCERS, AND ONLY ONE OF THEM HAS A REMEDY THAT FITS (268).** The
|
|
89
|
+
* ADDON raises `write_failed` too, from `operations.gd`, for a FILE it could not open for
|
|
90
|
+
* writing โ and `error_remedies.gd` answers it with *"Check the target path is inside the
|
|
91
|
+
* project and not read-only; the number is a Godot Error code."* That sentence is correct
|
|
92
|
+
* for the addon and nonsense for this site, which has no path, no file and no Godot error
|
|
93
|
+
* code: it is a TCP socket that went away. The collision was invisible while this branch
|
|
94
|
+
* was believed unreachable; 268 measured it reachable 200/200 and it became live.
|
|
95
|
+
*
|
|
96
|
+
* The CODE is kept and the message and remedy are made to fit โ 262's disposition for an
|
|
97
|
+
* overloaded shipped code, for the same reason: fix what the reader acts on, and leave a
|
|
98
|
+
* wire vocabulary that callers may already branch on alone. Whether the two producers
|
|
99
|
+
* should share a word at all is a wire question, and it is enqueued as one rather than
|
|
100
|
+
* decided here.
|
|
101
|
+
*/
|
|
102
|
+
export const BRIDGE_WRITE_FAILED = "write_failed";
|
|
103
|
+
/**
|
|
104
|
+
* The next action for a request that never reached the socket.
|
|
105
|
+
*
|
|
106
|
+
* ๐ด IT IS THE ONE FAILURE IN THIS CLASS THAT CAN SAY *RETRY* WITHOUT QUALIFICATION, and
|
|
107
|
+
* that is the whole value of separating it. `timeout-caveat.ts` exists because a bridge
|
|
108
|
+
* TIMEOUT may already have been applied by the editor; `closeRemedy` cannot promise
|
|
109
|
+
* otherwise either, since a request in flight when the socket dropped may have landed.
|
|
110
|
+
* This one is different in a way the host actually knows: the frame was never handed to
|
|
111
|
+
* the kernel, so nothing can have been applied, so a retry is a first attempt and not a
|
|
112
|
+
* second. 264's rule โ measure N families, speak for N โ read in the direction that
|
|
113
|
+
* licenses a stronger sentence rather than a weaker one.
|
|
114
|
+
*/
|
|
115
|
+
export const WRITE_FAILED_REMEDY = "Retry the call โ the request was never sent, so nothing was applied and this is a " +
|
|
116
|
+
"first attempt rather than a second. A recurrence means the host is tearing the " +
|
|
117
|
+
"connection down mid-request.";
|
|
118
|
+
/**
|
|
119
|
+
* The sentence for a request that never reached the socket.
|
|
120
|
+
*
|
|
121
|
+
* `cause` is node's own error, kept in a parenthetical rather than used as the whole
|
|
122
|
+
* message: `Cannot call write after a stream was destroyed` is a true statement about a
|
|
123
|
+
* Node stream and tells a caller of `node_add` nothing about their call, which is what
|
|
124
|
+
* this site shipped until 268.
|
|
125
|
+
*/
|
|
126
|
+
export function writeFailedMessage(method, cause) {
|
|
127
|
+
return (`Bridge request '${method}' was never sent โ the connection was torn down between ` +
|
|
128
|
+
`preparing the request and writing it, so the editor did not receive it (${cause.message}).`);
|
|
129
|
+
}
|
|
68
130
|
/**
|
|
69
131
|
* TCP client for the in-editor Breakpoint MCP addon. Speaks newline-delimited
|
|
70
132
|
* JSON. Requests are correlated to responses by `id`. Connects lazily and
|
|
@@ -396,9 +458,15 @@ export class BridgeClient {
|
|
|
396
458
|
const timer = setTimeout(() => {
|
|
397
459
|
this.pending.delete(id);
|
|
398
460
|
// Remember the id BEFORE rejecting, so a reply already in flight is
|
|
399
|
-
// reconciled rather than dropped as anonymous.
|
|
400
|
-
//
|
|
401
|
-
//
|
|
461
|
+
// reconciled rather than dropped as anonymous.
|
|
462
|
+
//
|
|
463
|
+
// ๐ด THIS COMMENT USED TO SAY THE `timed out after <n>ms` PHRASING WAS
|
|
464
|
+
// LOAD-BEARING, NAMING `tools/dap.ts:29` AND `tools/csdap.ts:31`. Both line
|
|
465
|
+
// numbers had drifted by more than a hundred lines, and as of 268 the claim is
|
|
466
|
+
// false as well: those predicates read `DapError.code`, and this class's own
|
|
467
|
+
// caveat reader derives its marker from `bridgeErrorLabel`. A comment naming a
|
|
468
|
+
// line number is a claim that goes stale in silence โ which is the whole reason
|
|
469
|
+
// the join is structural now.
|
|
402
470
|
this.ledger.note(id, method, timeoutMs);
|
|
403
471
|
// ๐ด 262 ยง2 CALLED THIS "THE ONE HOST-RAISED FAILURE THAT HAS A KNOWABLE CAUSE",
|
|
404
472
|
// and said everything else this class raises โ "a closed socket, a peer that never
|
|
@@ -408,14 +476,30 @@ export class BridgeClient {
|
|
|
408
476
|
// line โ a peer that never answered really does leave nothing behind โ which is
|
|
409
477
|
// why the one cause the host can supply comes from outside the socket: its own
|
|
410
478
|
// debugger holding the game. See `setHoldProbe`, and 264's census for the rest.
|
|
411
|
-
reject(new BridgeError(
|
|
479
|
+
reject(new BridgeError(BRIDGE_TIMEOUT_CODE, `Bridge request '${method}' timed out after ${timeoutMs}ms`, this.holdProbe?.() ?? undefined));
|
|
412
480
|
}, timeoutMs);
|
|
413
481
|
this.pending.set(id, { resolve: resolve, reject, timer, method });
|
|
414
482
|
socket.write(payload, (err) => {
|
|
415
483
|
if (err) {
|
|
416
484
|
clearTimeout(timer);
|
|
417
485
|
this.pending.delete(id);
|
|
418
|
-
|
|
486
|
+
// ๐ด REACHABLE, AND 265 CONCLUDED THE OPPOSITE FROM THE SAME TECHNIQUE (268).
|
|
487
|
+
// 265 broke a live socket four ways and read `bridge_closed` every time, and
|
|
488
|
+
// reasoned that `onClose()` always rejects the pending map first. Measured here
|
|
489
|
+
// 300/300 at the node level, that ordering is BACKWARDS: on a destroyed socket
|
|
490
|
+
// the write callback fires with ERR_STREAM_DESTROYED **before** the `close`
|
|
491
|
+
// event, so this line wins whenever the socket dies inside the await gap
|
|
492
|
+
// `request()` leaves between `connect()` and here. Driven through the real
|
|
493
|
+
// client โ `request()` not awaited, `close()` on the next statement โ the caller
|
|
494
|
+
// received THIS rejection 200 times out of 200. 265's drives all broke the
|
|
495
|
+
// socket and then WAITED, which is the one window in which they could not win.
|
|
496
|
+
//
|
|
497
|
+
// The sentence is worth having because this case knows something the other two
|
|
498
|
+
// do not: the payload never left. A `timeout` may have been applied by the
|
|
499
|
+
// editor and `bridge_closed` may have been too, so neither can tell a caller to
|
|
500
|
+
// retry; this one can, and 267's rule is that a next action rides in the field
|
|
501
|
+
// rather than inside the message.
|
|
502
|
+
reject(new BridgeError(BRIDGE_WRITE_FAILED, writeFailedMessage(method, err), WRITE_FAILED_REMEDY));
|
|
419
503
|
}
|
|
420
504
|
});
|
|
421
505
|
});
|
package/dist/config.js
CHANGED
|
@@ -111,6 +111,18 @@ function positiveInt(raw, fallback) {
|
|
|
111
111
|
const n = Number(t);
|
|
112
112
|
return Number.isSafeInteger(n) && n > 0 && n <= 2_147_483_647 ? n : fallback;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* An opt-OUT flag: true unless the operator spelled a recognised falsehood.
|
|
116
|
+
*
|
|
117
|
+
* ๐ด THE UNRECOGNISED VALUE KEEPS THE SAFE DEFAULT, AND THAT IS THE OPPOSITE OF
|
|
118
|
+
* `positiveInt`'s reasoning ON PURPOSE. A malformed deadline falls back because both
|
|
119
|
+
* outcomes are ordinary; a malformed *guard* setting must not disable the guard, or a
|
|
120
|
+
* typo โ `GODOT_DAP_REQUIRE_INITIALIZED=no thanks` โ turns a refusal off silently. Only
|
|
121
|
+
* `0`, `false`, `off` and `no` are read as intent, case-insensitively.
|
|
122
|
+
*/
|
|
123
|
+
function optOut(raw) {
|
|
124
|
+
return !/^(0|false|off|no)$/i.test((raw ?? "").trim());
|
|
125
|
+
}
|
|
114
126
|
export function loadConfig() {
|
|
115
127
|
const projectPath = process.env.GODOT_PROJECT ?? process.cwd();
|
|
116
128
|
// The C# project defaults to the main project, but is usually pointed at a
|
|
@@ -136,6 +148,7 @@ export function loadConfig() {
|
|
|
136
148
|
dapTimeoutMs: positiveInt(process.env.GODOT_DAP_TIMEOUT_MS, 20000),
|
|
137
149
|
dapSetVarTimeoutMs: positiveInt(process.env.GODOT_DAP_SETVAR_TIMEOUT_MS, 8000),
|
|
138
150
|
dapEvaluateTimeoutMs: positiveInt(process.env.GODOT_DAP_EVALUATE_TIMEOUT_MS, 8000),
|
|
151
|
+
dapRequireInitialized: optOut(process.env.GODOT_DAP_REQUIRE_INITIALIZED),
|
|
139
152
|
csDapCmd: process.env.GODOT_CSDAP_CMD ?? "netcoredbg",
|
|
140
153
|
csDapArgs: (process.env.GODOT_CSDAP_ARGS ?? "--interpreter=vscode").split(/\s+/).filter(Boolean),
|
|
141
154
|
// The default program cs_dbg_launch launches is the Mono/.NET Godot binary. GODOT_CSHARP_BIN
|
package/dist/csdap.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { DapError, INITIALIZED_WAIT_CEILING_MS } from "./dap.js";
|
|
2
|
+
import { DapError, DAP_TIMEOUT_CODE, DAP_UNANNOUNCED_CODE, INITIALIZED_WAIT_CEILING_MS, unannouncedMessage, } from "./dap.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
-
import { timeoutRemedy } from "./timeout-cause.js";
|
|
5
|
+
import { timeoutRemedy, unannouncedRemedy } from "./timeout-cause.js";
|
|
6
6
|
/**
|
|
7
7
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
8
8
|
*
|
|
@@ -230,9 +230,9 @@ export class CsDapClient extends EventEmitter {
|
|
|
230
230
|
// Remember the seq BEFORE rejecting, so a response already in flight is
|
|
231
231
|
// reconciled rather than dropped as anonymous.
|
|
232
232
|
this.ledger.note(seq, command, timeoutMs);
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB)));
|
|
233
|
+
// ๐ด THE WORDING IS NO LONGER LOAD-BEARING (268) โ see `dap.ts`'s twin. The code
|
|
234
|
+
// is what `tools/csdap.ts`'s `isDapTimeout` reads; the sentence is free.
|
|
235
|
+
reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB), DAP_TIMEOUT_CODE));
|
|
236
236
|
}, timeoutMs);
|
|
237
237
|
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
238
238
|
this.channel.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
@@ -350,12 +350,17 @@ export class CsDapClient extends EventEmitter {
|
|
|
350
350
|
* afterwards failed with a bare hex code against a phantom session. The same held
|
|
351
351
|
* for `program: ""` and for `cs_dbg_attach` on a process id that does not exist.
|
|
352
352
|
*/
|
|
353
|
-
async start(mode, args) {
|
|
353
|
+
async start(mode, args, requireInitialized = true) {
|
|
354
354
|
this.lastStartMode = mode;
|
|
355
355
|
this.lastStartArgs = args;
|
|
356
356
|
this.sessionStarted = false;
|
|
357
|
-
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
358
|
-
|
|
357
|
+
// Listen for `initialized` before we ask, so we cannot miss it. Two windows since
|
|
358
|
+
// 268 โ see `dap.ts`'s twin for the argument. Refusing is the default, so the wait
|
|
359
|
+
// runs to the caller's own deadline; the five-second ceiling survives on the opt-out
|
|
360
|
+
// path alone, which exists to reproduce 267's behaviour including its numbers.
|
|
361
|
+
const initializedWaitMs = requireInitialized
|
|
362
|
+
? this.timeoutMs
|
|
363
|
+
: Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
|
|
359
364
|
const onInit = this.waitEvent("initialized", initializedWaitMs);
|
|
360
365
|
this.capabilities = await this.request("initialize", {
|
|
361
366
|
clientID: "breakpoint-mcp",
|
|
@@ -390,6 +395,14 @@ export class CsDapClient extends EventEmitter {
|
|
|
390
395
|
});
|
|
391
396
|
// ๐ด CAPTURED, NOT DISCARDED (267) โ see `dap.ts`'s twin.
|
|
392
397
|
const initializedSeen = await onInit;
|
|
398
|
+
// ๐ด REFUSED, NOT REPORTED (268) โ and BEFORE `applyAllBreakpoints()`, because that
|
|
399
|
+
// call is the defect itself. See `dap.ts`'s twin.
|
|
400
|
+
if (!initializedSeen && requireInitialized) {
|
|
401
|
+
this.state = "terminated";
|
|
402
|
+
this.sessionStarted = false;
|
|
403
|
+
this.configured = false;
|
|
404
|
+
throw new DapError(mode, unannouncedMessage("The C# debug adapter", mode, initializedWaitMs), unannouncedRemedy(CSDAP_PEER, CSDAP_TIMEOUT_KNOB), DAP_UNANNOUNCED_CODE);
|
|
405
|
+
}
|
|
393
406
|
await this.applyAllBreakpoints();
|
|
394
407
|
// Arm the entry-stop wait BEFORE configurationDone is sent: netcoredbg can emit
|
|
395
408
|
// `stopped` before its configurationDone response lands, and a listener attached
|
package/dist/dap.js
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { FramedConnection } from "./framing.js";
|
|
3
3
|
import { OverdueLedger } from "./late-reply.js";
|
|
4
4
|
import { closeDetail, closeRemedy } from "./close-cause.js";
|
|
5
|
-
import { timeoutRemedy } from "./timeout-cause.js";
|
|
5
|
+
import { timeoutRemedy, unannouncedRemedy } from "./timeout-cause.js";
|
|
6
6
|
/**
|
|
7
7
|
* Who answers this plane, in the words its late-reply ledger already uses.
|
|
8
8
|
*
|
|
@@ -22,6 +22,13 @@ const DAP_PEER = "the debug adapter";
|
|
|
22
22
|
* nothing reads, which is worse than saying nothing.
|
|
23
23
|
*/
|
|
24
24
|
const DAP_TIMEOUT_KNOB = "GODOT_DAP_TIMEOUT_MS";
|
|
25
|
+
/** This request hit its own deadline. The only code `isDapTimeout` answers to. */
|
|
26
|
+
export const DAP_TIMEOUT_CODE = "timeout";
|
|
27
|
+
/**
|
|
28
|
+
* The adapter answered `initialize` and never emitted `initialized`, so the handshake
|
|
29
|
+
* was refused rather than continued out of order. See `start()`.
|
|
30
|
+
*/
|
|
31
|
+
export const DAP_UNANNOUNCED_CODE = "unannounced";
|
|
25
32
|
export class DapError extends Error {
|
|
26
33
|
command;
|
|
27
34
|
/**
|
|
@@ -38,12 +45,27 @@ export class DapError extends Error {
|
|
|
38
45
|
* here means nobody has answered that site yet, not that it is unanswerable.
|
|
39
46
|
*/
|
|
40
47
|
remedy;
|
|
41
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Why the host raised this, for callers that branch โ `undefined` on the six sites
|
|
50
|
+
* that relay the adapter's own failure.
|
|
51
|
+
*
|
|
52
|
+
* ๐ด **`LspError` HAS CARRIED ONE SINCE IT WAS WRITTEN AND THIS CLASS DID NOT, FOR NO
|
|
53
|
+
* REASON ANYBODY DECIDED.** `LspError`'s first constructor argument happens to be a
|
|
54
|
+
* code, so its two timeout sites pass `"timeout"` and every LSP caller branches on a
|
|
55
|
+
* field. This class's first argument is `command`, so its two timeout sites had
|
|
56
|
+
* nowhere to put one โ and the tool layer read the message body instead. The shape of
|
|
57
|
+
* a constructor decided whether a shipped branch was structural or textual, and
|
|
58
|
+
* nothing ever asked.
|
|
59
|
+
*/
|
|
60
|
+
code;
|
|
61
|
+
constructor(command, message, remedy, code) {
|
|
42
62
|
super(message);
|
|
43
63
|
this.command = command;
|
|
44
64
|
this.name = "DapError";
|
|
45
65
|
if (remedy)
|
|
46
66
|
this.remedy = remedy;
|
|
67
|
+
if (code)
|
|
68
|
+
this.code = code;
|
|
47
69
|
}
|
|
48
70
|
}
|
|
49
71
|
/**
|
|
@@ -55,6 +77,24 @@ export class DapError extends Error {
|
|
|
55
77
|
* was actually waited.
|
|
56
78
|
*/
|
|
57
79
|
export const INITIALIZED_WAIT_CEILING_MS = 5000;
|
|
80
|
+
/**
|
|
81
|
+
* The message both planes raise when the adapter never announces itself.
|
|
82
|
+
*
|
|
83
|
+
* ๐ด ONE BUILDER, TWO PLANES โ the same discipline `timeoutRemedy` and `closeRemedy`
|
|
84
|
+
* already follow, and the reason is 267's finding one file over: the SAME question was
|
|
85
|
+
* pasted into `peers.ts` and `readiness.ts`, 266 fixed one, and the other survived the
|
|
86
|
+
* session that fixed it because nothing joined the two sites. A sentence shipped twice
|
|
87
|
+
* is a sentence that will be corrected once.
|
|
88
|
+
*
|
|
89
|
+
* `waitedMs` is the window ACTUALLY waited, not the ceiling โ a refusal naming a wait
|
|
90
|
+
* that did not happen sends an operator to the wrong knob, which is exactly why 267
|
|
91
|
+
* captured the window instead of recomputing it at the report site.
|
|
92
|
+
*/
|
|
93
|
+
export function unannouncedMessage(peer, mode, waitedMs) {
|
|
94
|
+
return (`${peer} answered \`initialize\` for this ${mode} and never emitted \`initialized\` ` +
|
|
95
|
+
`within ${waitedMs}ms, so the session was refused rather than configured out of order. ` +
|
|
96
|
+
`No breakpoints were applied and no program was left running by this host.`);
|
|
97
|
+
}
|
|
58
98
|
/**
|
|
59
99
|
* What a launch/attach result says when the adapter never announced itself.
|
|
60
100
|
*
|
|
@@ -297,10 +337,12 @@ export class DapClient extends EventEmitter {
|
|
|
297
337
|
// Remember the seq BEFORE rejecting, so a response already in flight is
|
|
298
338
|
// reconciled rather than dropped as anonymous.
|
|
299
339
|
this.ledger.note(seq, command, timeoutMs);
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
340
|
+
// ๐ด THE WORDING IS NO LONGER LOAD-BEARING, AND THAT IS 268's CHANGE. It used to
|
|
341
|
+
// be: `tools/dap.ts`'s `isDapTimeout` regexed `/timed out after/` against this
|
|
342
|
+
// string, so a copy edit here silently changed which answer `dbg_evaluate` and
|
|
343
|
+
// `dbg_set_variable` gave, with every test green. The code below is what those
|
|
344
|
+
// branches read now; the sentence is free to be improved.
|
|
345
|
+
reject(new DapError(command, `DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(DAP_PEER, DAP_TIMEOUT_KNOB), DAP_TIMEOUT_CODE));
|
|
304
346
|
}, timeoutMs);
|
|
305
347
|
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
306
348
|
this.conn.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
@@ -489,7 +531,7 @@ export class DapClient extends EventEmitter {
|
|
|
489
531
|
* not refused: every adapter that works today keeps working, and the four launch tools
|
|
490
532
|
* say which of the two happened โ the shape `stop_on_entry_honored` already ships in.
|
|
491
533
|
*/
|
|
492
|
-
async start(mode, args, entryStopWaitMs = 0) {
|
|
534
|
+
async start(mode, args, entryStopWaitMs = 0, requireInitialized = true) {
|
|
493
535
|
// Remember how we started so restart() can reuse (or override) these params.
|
|
494
536
|
this.lastStartMode = mode;
|
|
495
537
|
this.lastStartArgs = args;
|
|
@@ -497,12 +539,20 @@ export class DapClient extends EventEmitter {
|
|
|
497
539
|
// A fresh session re-detects against whatever adapter answers this time โ a
|
|
498
540
|
// restart may reach a different build than the one that dropped last time.
|
|
499
541
|
this.droppedModifiers.clear();
|
|
500
|
-
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
|
|
542
|
+
// Listen for `initialized` before we ask, so we cannot miss it. The window is
|
|
543
|
+
// captured rather than recomputed at the report site, so a warning or refusal names
|
|
544
|
+
// the wait that actually happened rather than the one the code would compute later.
|
|
545
|
+
//
|
|
546
|
+
// ๐ด TWO WINDOWS, AND WHICH ONE APPLIES IS THE WHOLE OF 268's DECISION. Refusing is
|
|
547
|
+
// the default now, so the ceiling that made a longer wait pointless has gone with it:
|
|
548
|
+
// when nothing is going to proceed regardless, the honest bound is the deadline the
|
|
549
|
+
// CALLER declared. A conformant adapter that takes six seconds used to be configured
|
|
550
|
+
// out of order at five and told nobody; it now simply succeeds. The five-second
|
|
551
|
+
// ceiling survives only on the opt-out path, where it must, because that path exists
|
|
552
|
+
// to reproduce 267's behaviour exactly โ including the number its warning prints.
|
|
553
|
+
const initializedWaitMs = requireInitialized
|
|
554
|
+
? this.timeoutMs
|
|
555
|
+
: Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
|
|
506
556
|
const onInit = this.waitEvent("initialized", initializedWaitMs);
|
|
507
557
|
this.capabilities = await this.request("initialize", {
|
|
508
558
|
clientID: "breakpoint-mcp",
|
|
@@ -536,6 +586,16 @@ export class DapClient extends EventEmitter {
|
|
|
536
586
|
// that licenses it was invisible: the failing case and the succeeding case returned
|
|
537
587
|
// the same `undefined`.
|
|
538
588
|
const initializedSeen = await onInit;
|
|
589
|
+
// ๐ด THE REFUSAL GOES HERE AND NOWHERE LATER (268), because the next line is the
|
|
590
|
+
// defect. `applyAllBreakpoints()` is `setBreakpoints` going out ahead of the event
|
|
591
|
+
// the DAP specification says must precede it โ the thing 266 measured against a stub
|
|
592
|
+
// adapter and 267 made observable without stopping. Refusing after it would leave the
|
|
593
|
+
// adapter holding breakpoints from a session the caller was told did not start.
|
|
594
|
+
if (!initializedSeen && requireInitialized) {
|
|
595
|
+
this.state = "terminated";
|
|
596
|
+
this.sessionStarted = false;
|
|
597
|
+
throw new DapError(mode, unannouncedMessage("The debug adapter", mode, initializedWaitMs), unannouncedRemedy(DAP_PEER, DAP_TIMEOUT_KNOB), DAP_UNANNOUNCED_CODE);
|
|
598
|
+
}
|
|
539
599
|
await this.applyAllBreakpoints();
|
|
540
600
|
await this.request("configurationDone", {}).catch(() => undefined);
|
|
541
601
|
// ๐ด By the time we get here an already-arrived rejection has ALREADY run its
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
53
53
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
54
54
|
// godot://โฆ resources and receive notifications/resources/updated.
|
|
55
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
55
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.80.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
56
56
|
// 208 ยง7.1 โ delete from the wire what the protocol already says: the SDK's
|
|
57
57
|
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
58
58
|
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
package/dist/timeout-cause.js
CHANGED
|
@@ -45,4 +45,22 @@ export function timeoutRemedy(peer, knob) {
|
|
|
45
45
|
return (`Raise \`${knob}\` if ${peer} is merely slow โ the request reached it and this ` +
|
|
46
46
|
`host's deadline fired first, so it may still answer and a retry sends a second request.`);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The next action when an adapter answered `initialize` and never announced itself.
|
|
50
|
+
*
|
|
51
|
+
* ๐ด IT NAMES BOTH WAYS OUT, BECAUSE THE HOST CANNOT TELL THEM APART FROM HERE. Either
|
|
52
|
+
* the adapter is slower than the caller's own declared deadline โ raise it โ or it does
|
|
53
|
+
* not implement the `initialized` event at all, in which case a session can still be had
|
|
54
|
+
* by opting out and accepting that breakpoints are applied ahead of the event that
|
|
55
|
+
* licenses them, which is what every session did silently before 268. Naming only the
|
|
56
|
+
* knob would send an operator to lengthen a wait that is never going to end.
|
|
57
|
+
*
|
|
58
|
+
* Written to check 28's grammar: an imperative at the head, a full stop at the end, and
|
|
59
|
+
* nothing named that does not resolve.
|
|
60
|
+
*/
|
|
61
|
+
export function unannouncedRemedy(peer, knob) {
|
|
62
|
+
return (`Raise \`${knob}\` if ${peer} is merely slow to announce itself, or set ` +
|
|
63
|
+
`\`GODOT_DAP_REQUIRE_INITIALIZED=0\` to start anyway against an adapter that never ` +
|
|
64
|
+
`sends \`initialized\`.`);
|
|
65
|
+
}
|
|
48
66
|
//# sourceMappingURL=timeout-cause.js.map
|
package/dist/timeout-caveat.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { annotationsFor } from "./annotations.js";
|
|
2
|
+
import { BRIDGE_TIMEOUT_CODE, bridgeErrorLabel } from "./bridge.js";
|
|
2
3
|
/**
|
|
3
4
|
* The marker identifying a bridge timeout envelope, built by
|
|
4
5
|
* `tools/editor/common.ts`'s `fail()`. Deliberately narrow: LSP and DAP timeouts
|
|
5
|
-
* are a different transport with a different failure mode
|
|
6
|
-
* mutation is implied
|
|
7
|
-
*
|
|
6
|
+
* are a different transport with a different failure mode โ no editor-side
|
|
7
|
+
* mutation is implied โ and the two DAP planes classify their own by
|
|
8
|
+
* `DapError.code`, which this neither reads nor disturbs.
|
|
9
|
+
*
|
|
10
|
+
* ๐ด **DERIVED SINCE 268; A HAND-COPIED LITERAL BEFORE IT.** This constant used to
|
|
11
|
+
* spell `Bridge error [timeout]` out, here, while the template producing it lives in
|
|
12
|
+
* `tools/editor/common.ts` and the code it embeds is raised in `bridge.ts`. Three
|
|
13
|
+
* files, one sentence, nothing joining them โ so a reword of `fail()`'s label would
|
|
14
|
+
* have left this matching nothing, and the caveat that tells a caller their timed-out
|
|
15
|
+
* `node_add` may ALREADY have landed would have gone quiet on every mutating tool
|
|
16
|
+
* with the whole suite still green. `dap-timeout-predicate-reads-prose` with a hundred
|
|
17
|
+
* times the blast radius, found only by asking what ELSE in the tree decides behaviour
|
|
18
|
+
* by matching English.
|
|
8
19
|
*/
|
|
9
|
-
const BRIDGE_TIMEOUT_MARK =
|
|
20
|
+
const BRIDGE_TIMEOUT_MARK = bridgeErrorLabel(BRIDGE_TIMEOUT_CODE);
|
|
10
21
|
/** Mutating, but repeating it lands in the same state โ retrying is safe. */
|
|
11
22
|
export const CAVEAT_IDEMPOTENT = "The request WAS delivered to the editor and may already have been applied. " +
|
|
12
23
|
"This tool is idempotent, so retrying is safe.";
|
package/dist/tools/csdap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DapError, unorderedHandshakeWarning } from "../dap.js";
|
|
2
|
+
import { DapError, DAP_TIMEOUT_CODE, unorderedHandshakeWarning } from "../dap.js";
|
|
3
3
|
import { remedyClause } from "../bridge.js";
|
|
4
4
|
import { resolveSourceFile } from "../paths.js";
|
|
5
5
|
import { gate } from "../confirm.js";
|
|
@@ -64,9 +64,13 @@ function refuse(message, code) {
|
|
|
64
64
|
* adapter-reported failure or a dropped connection). Used to turn a
|
|
65
65
|
* setVariable / evaluate non-response into a clear message rather than the
|
|
66
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.
|
|
67
71
|
*/
|
|
68
72
|
function isDapTimeout(err) {
|
|
69
|
-
return err instanceof DapError &&
|
|
73
|
+
return err instanceof DapError && err.code === DAP_TIMEOUT_CODE;
|
|
70
74
|
}
|
|
71
75
|
/**
|
|
72
76
|
* D4 C3 โ the C#/.NET debugging plane. `cs_dbg_*` tools mirroring the read/inspect
|
|
@@ -259,7 +263,7 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
259
263
|
cwd: cfg.csDapProjectPath,
|
|
260
264
|
stopAtEntry: stop_on_entry ?? false,
|
|
261
265
|
justMyCode: just_my_code ?? true,
|
|
262
|
-
});
|
|
266
|
+
}, cfg.dapRequireInitialized);
|
|
263
267
|
const result = { session_id: "csharp", state: dap.state };
|
|
264
268
|
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
265
269
|
return ok(result);
|
|
@@ -296,7 +300,7 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
296
300
|
`was previously reported as "running" against a process that does not exist.`, "no_such_process");
|
|
297
301
|
}
|
|
298
302
|
}
|
|
299
|
-
const { initializedSeen, initializedWaitMs } = await dap.start("attach", { processId: process_id });
|
|
303
|
+
const { initializedSeen, initializedWaitMs } = await dap.start("attach", { processId: process_id }, cfg.dapRequireInitialized);
|
|
300
304
|
const result = { session_id: "csharp", state: dap.state };
|
|
301
305
|
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
302
306
|
return ok(result);
|
package/dist/tools/dap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DapError, unorderedHandshakeWarning } from "../dap.js";
|
|
2
|
+
import { DapError, DAP_TIMEOUT_CODE, unorderedHandshakeWarning } from "../dap.js";
|
|
3
3
|
import { remedyClause } from "../bridge.js";
|
|
4
4
|
import { resolveSourceFile } from "../paths.js";
|
|
5
5
|
import { gate } from "../confirm.js";
|
|
@@ -137,9 +137,18 @@ function fail(err) {
|
|
|
137
137
|
* True when `err` is a DAP request that hit its own request deadline (as opposed to an
|
|
138
138
|
* adapter-reported failure or a dropped connection). Used to turn the setVariable /
|
|
139
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.
|
|
140
149
|
*/
|
|
141
150
|
function isDapTimeout(err) {
|
|
142
|
-
return err instanceof DapError &&
|
|
151
|
+
return err instanceof DapError && err.code === DAP_TIMEOUT_CODE;
|
|
143
152
|
}
|
|
144
153
|
/**
|
|
145
154
|
* ๐ด The modifier feature-detection lives in `dap.ts` (the client), NOT here, and that
|
|
@@ -335,7 +344,7 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
335
344
|
};
|
|
336
345
|
}
|
|
337
346
|
try {
|
|
338
|
-
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);
|
|
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);
|
|
339
348
|
const result = { session_id: "godot", state: dap.state, scene: scene ?? "main" };
|
|
340
349
|
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
341
350
|
if (stop_on_entry) {
|
|
@@ -372,7 +381,7 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
372
381
|
},
|
|
373
382
|
}, async ({ address, port }) => {
|
|
374
383
|
try {
|
|
375
|
-
const { initializedSeen, initializedWaitMs } = 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);
|
|
376
385
|
const result = { session_id: "godot", state: dap.state };
|
|
377
386
|
reportHandshakeOrder(result, initializedSeen, initializedWaitMs);
|
|
378
387
|
reportDroppedModifiers(result);
|
|
@@ -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/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",
|