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 +2 -2
- package/dist/exit-cause.js +110 -0
- package/dist/finiteness.js +36 -0
- package/dist/index.js +1 -1
- package/dist/peers.js +15 -4
- package/dist/tools/processes.js +5 -3
- package/package.json +1 -1
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
|
package/dist/finiteness.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
}
|
package/dist/tools/processes.js
CHANGED
|
@@ -4,6 +4,7 @@ import { log } from "../logger.js";
|
|
|
4
4
|
import { ok, failPath } from "./lsp-common.js";
|
|
5
5
|
import { resolveInsideProject } from "../paths.js";
|
|
6
6
|
import { portFree, portConflictMessage } from "../ports.js";
|
|
7
|
+
import { 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
|
-
|
|
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.
|
|
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",
|