breakpoint-mcp 1.78.2 โ†’ 1.78.3

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
@@ -2,7 +2,7 @@ import net from "node:net";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { log } from "./logger.js";
4
4
  import { OverdueLedger } from "./late-reply.js";
5
- import { findNonFinite, describeNonFinite, tolerate, TOLERANT_METHODS } from "./finiteness.js";
5
+ import { findNonFinite, describeNonFinite, tolerate, TOLERANT_METHODS, nonFiniteRemedy } from "./finiteness.js";
6
6
  import { remedyForWireError } from "./remedies.js";
7
7
  import { closeDetail, closeRemedy } from "./close-cause.js";
8
8
  import { connectHint, connectRemedy } from "./connect-cause.js";
@@ -342,7 +342,7 @@ export class BridgeClient {
342
342
  else {
343
343
  const hits = findNonFinite(result);
344
344
  if (hits.length)
345
- p.reject(new BridgeError("non_finite", describeNonFinite(hits)));
345
+ p.reject(new BridgeError("non_finite", describeNonFinite(hits), nonFiniteRemedy(hits)));
346
346
  else
347
347
  p.resolve(result);
348
348
  }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * What a MANAGED CHILD'S termination discriminates, and the next action for each.
3
+ *
4
+ * ๐Ÿ”ด WHY THIS EXISTS โ€” 265 answered the connect family and left the two peer sites in
5
+ * the census's silent bucket. Driven at 266 against real spawned children, they are the
6
+ * same shape one file over, and one of them was making a claim that is FALSE rather than
7
+ * merely absent:
8
+ *
9
+ * ```
10
+ * what the caller read
11
+ * child exited (code 1) 1 of 1 peer(s) did not become ready. peer-1 on 21101: exited (code 1)
12
+ * โ€” SCRIPT ERROR: could not load main scene. Is the Breakpoint MCP
13
+ * addon enabled in this project (it registers the runtime autoload)?
14
+ *
15
+ * child alive, no ping 1 of 1 peer(s) did not become ready. peer-1 on 21121: never answered
16
+ * ping. Is the Breakpoint MCP addon enabled in this project (it
17
+ * registers the runtime autoload)?
18
+ *
19
+ * two measured causes -> one question -> ZERO next actions
20
+ * ```
21
+ *
22
+ * ๐Ÿ”ด READ THE FIRST ROW. The child EXITED, on its own, with a code and a stderr line
23
+ * naming what stopped it. Whether the addon is enabled had no bearing on that: an absent
24
+ * runtime autoload does not stop a process, it stops a process from ANSWERING. The
25
+ * operator whose scene fails to load is sent to go and look at the plugin list. That is
26
+ * 265 ยง1's ENOTFOUND finding at the process boundary โ€” and `m?.exited`, the thing that
27
+ * says which sentence is true, is computed on the line directly above the one that pastes
28
+ * the question over both.
29
+ *
30
+ * ๐Ÿ”ด THE QUESTION IS SUPPRESSED, NOT REWORDED. `readinessHint` returns the shipped
31
+ * sentence unchanged when NO not-ready peer exited โ€” the family it was always true of โ€”
32
+ * and `""` the moment one did. Every caller that reads the silent case reads the same
33
+ * bytes, and there is a test asserting that direction specifically, because a build that
34
+ * dropped the question unconditionally would be a worse defect than the one being fixed
35
+ * and would pass a one-sided test.
36
+ *
37
+ * ๐Ÿ”ด AND THE SECOND SITE NAMED A CODE THAT IS NOT A CODE. `peer_exited` rendered
38
+ * `Peer "peer-1" exited (code null)` for a SIGKILLed child, because
39
+ * `ProcessRegistry.run` registered `child.on("exit", (code) => โ€ฆ)` and dropped node's
40
+ * SECOND argument. The signal was handed to that callback on every kill since the
41
+ * registry was written and assigned to nothing โ€” 265's "a default that compiles asks no
42
+ * question", spelled here as a parameter nobody declared. `exitSignal` is captured now
43
+ * and `describeExit` says `killed by SIGKILL` where the sentence used to print a null.
44
+ *
45
+ * ๐Ÿ”ต WHAT THIS DELIBERATELY DOES NOT DO. `godot_process_output` still answers
46
+ * `exit_code: null` for a signal-killed child and carries no `signal` key: that is a wire
47
+ * addition to a shipped tool, it would make this cut a MINOR, and it is enqueued rather
48
+ * than folded in. The capture is internal; only the two peer sentences read it today.
49
+ */
50
+ /** How a managed child ended, in the words a person would use. */
51
+ export function describeExit(code, signal) {
52
+ // The signal is checked FIRST because both can be non-null in no case node produces,
53
+ // and because `code` is the one that is null exactly when the signal is the answer.
54
+ if (signal)
55
+ return `killed by ${signal}`;
56
+ if (code === null)
57
+ return "exited without reporting a code";
58
+ return `exited (code ${code})`;
59
+ }
60
+ /**
61
+ * The hint that belongs in the MESSAGE, given what actually happened to the children.
62
+ *
63
+ * Returns the shipped sentence unchanged while every not-ready peer is merely SILENT โ€”
64
+ * the family it describes correctly โ€” and suppresses it as soon as one has exited, where
65
+ * it is a statement about a plugin that could not have stopped a process.
66
+ */
67
+ export function readinessHint(causes, hint) {
68
+ return causes.some((c) => c === "exited") ? "" : hint;
69
+ }
70
+ /**
71
+ * The next action for a peer that did not become ready, branched on the causes in hand.
72
+ *
73
+ * Three populations, because the registry really does produce three: every child gone,
74
+ * every child alive and mute, and a mixture. A mixture gets both actions named rather
75
+ * than the first one that matched, because telling an operator with one dead peer and one
76
+ * mute peer to do only half the work is the same error as the sentence this replaces.
77
+ *
78
+ * Written to the grammar check 28 enforces on `error_remedies.gd`: one imperative at the
79
+ * head, a full stop at the end, and nothing named that does not resolve.
80
+ */
81
+ export function readinessRemedy(causes) {
82
+ if (causes.length === 0)
83
+ return undefined;
84
+ const exited = causes.some((c) => c === "exited");
85
+ const silent = causes.some((c) => c === "silent");
86
+ if (exited && silent) {
87
+ return (`Read the output quoted above for the peer(s) that exited and fix what stopped them, ` +
88
+ `then check the Breakpoint MCP plugin is enabled for the one(s) that stayed up and never ` +
89
+ `answered โ€” two different things failed here and only the second is about the addon.`);
90
+ }
91
+ if (exited) {
92
+ return (`Read the output quoted above and fix what stopped the child โ€” it exited on its own, ` +
93
+ `so the state of the Breakpoint MCP plugin is not what failed here.`);
94
+ }
95
+ return (`Enable the "Breakpoint MCP" plugin in this project and retry โ€” the peer is still running ` +
96
+ `and answered nothing on its port, which is what a missing runtime autoload looks like.`);
97
+ }
98
+ /**
99
+ * The next action for a request addressed to a peer whose process is already gone.
100
+ *
101
+ * Branched on whether the host CAPTURED any output from it: a child that said something
102
+ * before it died has a cause the caller can read, and one that said nothing has only the
103
+ * fact of its death, which makes re-spawning the next action rather than reading.
104
+ */
105
+ export function peerExitRemedy(hasTail) {
106
+ return hasTail
107
+ ? `Read the last output quoted above to see what stopped this peer, then spawn a replacement with runtime_spawn_peers.`
108
+ : `Spawn a replacement with runtime_spawn_peers โ€” this peer captured no output before it went, so there is nothing here to read.`;
109
+ }
110
+ //# sourceMappingURL=exit-cause.js.map
@@ -117,6 +117,42 @@ export function describeNonFinite(hits) {
117
117
  `JSON cannot carry them โ€” Godot stringifies INF as 1e99999 and NAN as null โ€” so the ` +
118
118
  `call is refused rather than reported as a success with the value silently missing.`);
119
119
  }
120
+ /**
121
+ * The next action for a refused non-finite reply.
122
+ *
123
+ * ๐Ÿ”ด ONE FAMILY IS MEASURED AND ONE IS SPOKEN FOR (266). Driven against a raw-bytes fake
124
+ * addon writing what Godot writes, every value that reaches this refusal is `Infinity` or
125
+ * `-Infinity` from a literal `1e99999`, flat or nested, one hit or many. **NAN is NOT this
126
+ * refusal's population**, which the message's own aside makes easy to assume it is:
127
+ * measured, Godot's `null` for NAN parses to `null`, `findNonFinite({current: null})`
128
+ * returns `[]`, and the reply RESOLVES and reaches the client's schema as a null. So the
129
+ * remedy speaks about an infinity and about nothing else, per 265's refusal to invent.
130
+ *
131
+ * ๐Ÿ”ด AND IT DOES NOT SAY "RETRY". This is the one host-raised failure in the census whose
132
+ * cause is neither the transport nor the host's own state but the PROJECT'S DATA โ€” the
133
+ * engine really does hold that value, and it will hold it again on the next call. A
134
+ * sentence offering a retry would be the shape 262 ยง2 found on `dbg_set_variable`: fluent,
135
+ * addressed to the wrong party, and wrong every time it is read.
136
+ *
137
+ * Branched on the one discriminator in hand: how many paths carry it. Naming a single
138
+ * path is a place to go and look; naming six is almost always one upstream value, and
139
+ * sending someone to inspect six properties one at a time is the wrong next action.
140
+ *
141
+ * Written to the grammar check 28 enforces on `error_remedies.gd`: one imperative at the
142
+ * head, a full stop at the end, and nothing named that does not resolve.
143
+ */
144
+ export function nonFiniteRemedy(hits) {
145
+ if (hits.length === 0)
146
+ return undefined;
147
+ if (hits.length === 1) {
148
+ return (`Inspect ${hits[0].path} in the project โ€” the engine's own value there is infinite ` +
149
+ `(a division by zero and an unset scale are the usual sources), so this is data to fix ` +
150
+ `rather than a call to repeat.`);
151
+ }
152
+ return (`Look for the one value feeding all ${hits.length} of the paths named above โ€” an infinity ` +
153
+ `this wide is almost always a single division by zero propagated by the engine, so this is ` +
154
+ `data to fix rather than a call to repeat.`);
155
+ }
120
156
  /**
121
157
  * Drop every non-finite value from a flat monitor record, returning what survived and the
122
158
  * keys that did not. The declared type of the record does not move: a reading that is not
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.78.2" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
55
+ const server = new McpServer({ name: "breakpoint-mcp", version: "1.78.3" }, { 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/peers.js CHANGED
@@ -4,6 +4,7 @@ import { ensureProjectSecret, readProjectSecret } from "./secret.js";
4
4
  import { log } from "./logger.js";
5
5
  import { portFree } from "./ports.js";
6
6
  import { waitForBridge } from "./readiness.js";
7
+ import { describeExit, peerExitRemedy, readinessHint, readinessRemedy } from "./exit-cause.js";
7
8
  /**
8
9
  * Hard ceiling on simultaneously live peers.
9
10
  *
@@ -87,7 +88,10 @@ export class PeerRegistry {
87
88
  const m = this.procs.get(e.managedId);
88
89
  if (m?.exited) {
89
90
  const tail = this.procs.tail(e.managedId, 6).join(" | ");
90
- throw new BridgeError("peer_exited", `Peer "${id}" exited (code ${m.exitCode})${tail ? `; last output: ${tail}` : ""}.`);
91
+ // ๐Ÿ”ด `describeExit` RATHER THAN `exitCode` ALONE (266). A SIGKILLed child has a null
92
+ // code and a signal, and this sentence used to render the null: `exited (code null)`.
93
+ // See exit-cause.ts for the argument node was handing us and nobody declared.
94
+ throw new BridgeError("peer_exited", `Peer "${id}" ${describeExit(m.exitCode, m.exitSignal)}${tail ? `; last output: ${tail}` : ""}.`, peerExitRemedy(tail.length > 0));
91
95
  }
92
96
  return e.client;
93
97
  }
@@ -194,10 +198,16 @@ export class PeerRegistry {
194
198
  }));
195
199
  const notReady = spawned.filter((e) => !e.info.ready);
196
200
  if (notReady.length) {
201
+ // ๐Ÿ”ด THE CAUSE IS COLLECTED, NOT JUST RENDERED (266). `m?.exited` decided one word
202
+ // of the sentence and was then thrown away, so the hint below was pasted over both
203
+ // measured causes and was FALSE on one of them โ€” an absent runtime autoload does
204
+ // not stop a process, it stops a process from ANSWERING. See exit-cause.ts.
205
+ const causes = [];
197
206
  const detail = notReady
198
207
  .map((e) => {
199
208
  const m = this.procs.get(e.managedId);
200
- const why = m?.exited ? `exited (code ${m.exitCode})` : "never answered ping";
209
+ causes.push(m?.exited ? "exited" : "silent");
210
+ const why = m?.exited ? describeExit(m.exitCode, m.exitSignal) : "never answered ping";
201
211
  const tail = this.procs.tail(e.managedId, 6).join(" | ");
202
212
  return `${e.info.id} on ${e.info.port}: ${why}${tail ? ` โ€” ${tail}` : ""}`;
203
213
  })
@@ -206,8 +216,9 @@ export class PeerRegistry {
206
216
  // can still runtime_peer_stop{all:true}, and a half-started peer that
207
217
  // vanished from the registry is harder to reason about than one that
208
218
  // reports why it is not ready.
209
- throw new BridgeError("peer_not_ready", `${notReady.length} of ${spawned.length} peer(s) did not become ready. ${detail}. ` +
210
- `Is the Breakpoint MCP addon enabled in this project (it registers the runtime autoload)?`);
219
+ const hint = readinessHint(causes, `Is the Breakpoint MCP addon enabled in this project (it registers the runtime autoload)?`);
220
+ throw new BridgeError("peer_not_ready", `${notReady.length} of ${spawned.length} peer(s) did not become ready. ${detail}.` +
221
+ (hint ? ` ${hint}` : ""), readinessRemedy(causes));
211
222
  }
212
223
  return spawned.map((e) => e.info);
213
224
  }
@@ -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 { describeExit } from "../exit-cause.js";
7
8
  import { waitForRuntimeBridge, notReadyRemedy } from "../readiness.js";
8
9
  const LINE_CAP = 5000;
9
10
  /**
@@ -33,7 +34,7 @@ export class ProcessRegistry {
33
34
  stdio: ["ignore", "pipe", "pipe"],
34
35
  ...(env ? { env: { ...process.env, ...env } } : {}),
35
36
  });
36
- const m = { id, child, lines: [], seq: 0, exited: false, exitCode: null };
37
+ const m = { id, child, lines: [], seq: 0, exited: false, exitCode: null, exitSignal: null };
37
38
  const ingest = (stream) => (buf) => {
38
39
  const text = typeof buf === "string" ? buf : buf.toString("utf8");
39
40
  for (const line of text.split(/\r?\n/)) {
@@ -47,10 +48,11 @@ export class ProcessRegistry {
47
48
  };
48
49
  child.stdout?.on("data", ingest("stdout"));
49
50
  child.stderr?.on("data", ingest("stderr"));
50
- child.on("exit", (code) => {
51
+ child.on("exit", (code, signal) => {
51
52
  m.exited = true;
52
53
  m.exitCode = code;
53
- log(`managed process ${id} exited (${code})`);
54
+ m.exitSignal = signal;
55
+ log(`managed process ${id} ${describeExit(code, signal)}`);
54
56
  });
55
57
  this.procs.set(id, m);
56
58
  return m;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakpoint-mcp",
3
- "version": "1.78.2",
3
+ "version": "1.78.3",
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",