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 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. The message keeps the
400
- // exact `timed out after <n>ms` phrasing โ€” tools/dap.ts:29 and
401
- // tools/csdap.ts:31 branch on that substring.
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("timeout", `Bridge request '${method}' timed out after ${timeoutMs}ms`, this.holdProbe?.() ?? undefined));
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
- reject(new BridgeError("write_failed", err.message));
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
- // The `timed out after <n>ms` wording is load-bearing โ€” `tools/csdap.ts`'s
234
- // `isDapTimeout` branches on it. The next action rides beside it, not inside it.
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
- const initializedWaitMs = Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
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
- constructor(command, message, remedy) {
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
- // The message keeps its exact `timed out after <n>ms` wording โ€” `tools/dap.ts`'s
301
- // `isDapTimeout` branches on it and `timeout-caveat.ts` says out loud that this
302
- // predicate must not be disturbed. The next action rides beside it, not inside it.
303
- reject(new DapError(command, `DAP '${command}' timed out after ${timeoutMs}ms`, timeoutRemedy(DAP_PEER, DAP_TIMEOUT_KNOB)));
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
- // The window is captured rather than recomputed at the report site: it is
502
- // `min(timeoutMs, 5000)`, so a caller who raised the deadline gets 5000 and one who
503
- // lowered it gets their own number, and a warning naming the wrong one is a warning
504
- // that sends an operator to the wrong knob.
505
- const initializedWaitMs = Math.min(this.timeoutMs, INITIALIZED_WAIT_CEILING_MS);
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.79.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
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
@@ -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
@@ -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 (no editor-side
6
- * mutation is implied), and `tools/dap.ts:29` / `tools/csdap.ts:31` branch on
7
- * their own `/timed out after/` predicate, which this must not disturb.
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 = "Bridge error [timeout]";
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.";
@@ -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 && /timed out after/.test(err.message);
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 && /timed out after/.test(err.message);
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: `Bridge error [${code}]: ${message}${remedyClause(err)}` }],
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.79.0",
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",