breakpoint-mcp 1.77.0 → 1.78.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/csdap.js +59 -0
- package/dist/index.js +1 -1
- package/dist/tools/csdap.js +121 -0
- package/package.json +1 -1
package/dist/csdap.js
CHANGED
|
@@ -53,10 +53,61 @@ export class CsDapClient extends EventEmitter {
|
|
|
53
53
|
* being emitted as an unlistened `error` event (which throws).
|
|
54
54
|
*/
|
|
55
55
|
startFailure = null;
|
|
56
|
+
/**
|
|
57
|
+
* Whether a launch/attach this client sent was accepted. Tracked SEPARATELY from
|
|
58
|
+
* `state` for the reason `DapClient` tracks it separately, measured here on this
|
|
59
|
+
* plane against a real netcoredbg: `cs_dbg_continue` on a client that had never
|
|
60
|
+
* launched anything left `state = "running"` behind — `resume()` assigns it
|
|
61
|
+
* optimistically before it waits — so every call after it read as a live session.
|
|
62
|
+
* A guard built on `state` alone would be poisoned by the very call it exists to
|
|
63
|
+
* refuse.
|
|
64
|
+
*/
|
|
65
|
+
sessionStarted = false;
|
|
56
66
|
capabilities = null;
|
|
57
67
|
state = "disconnected";
|
|
58
68
|
lastStoppedThreadId = null;
|
|
59
69
|
lastStoppedReason = null;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a launch/attach the adapter accepted is still in force. The `cs_dbg_*`
|
|
72
|
+
* tools that can only answer from a live session consult this instead of asking
|
|
73
|
+
* netcoredbg and reporting whatever came back.
|
|
74
|
+
*
|
|
75
|
+
* 🔴 Measured against a real netcoredbg 3.2.0-1092 on a client that had never
|
|
76
|
+
* launched anything: seven of the nine readers answered with a raw hex code —
|
|
77
|
+
* `Failed command 'stackTrace' : 0x80004005` (E_FAIL), `0x80070057` (E_INVALIDARG)
|
|
78
|
+
* — `cs_dbg_set_variable` reported "the debug adapter reported a failure with no
|
|
79
|
+
* message", and `cs_dbg_watch` answered `isError:false` with every entry carrying
|
|
80
|
+
* `error: "error: 0x80004005"`. Only `cs_dbg_restart` refused, and only because it
|
|
81
|
+
* happens to read `lastStartMode`. That is the same single exception session 259
|
|
82
|
+
* measured one plane over, for the same accidental reason.
|
|
83
|
+
*/
|
|
84
|
+
get hasSession() {
|
|
85
|
+
return this.sessionStarted && this.state !== "terminated" && this.state !== "disconnected";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether the adapter has reported a stop, read through a call so TypeScript's
|
|
89
|
+
* control-flow narrowing does not collapse it — `state` is mutated by the event
|
|
90
|
+
* handler across every `await` in `start()`, so a literal comparison after an
|
|
91
|
+
* assignment is (wrongly) an impossible one to tsc. `DapClient.stoppedNow()`
|
|
92
|
+
* carries the same note for the same reason.
|
|
93
|
+
*/
|
|
94
|
+
stoppedNow() {
|
|
95
|
+
return this.state === "stopped";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether there is a FRAME to answer from — the public half of `stoppedNow()`.
|
|
99
|
+
*
|
|
100
|
+
* 🔴 `hasSession` IS NOT THIS, and session 262 spent a whole session on the
|
|
101
|
+
* difference on the GDScript plane. Measured here with a session live and the
|
|
102
|
+
* program merely RUNNING: `stackTrace` 0x80070057, `scopes` / `variables` /
|
|
103
|
+
* `step` 0x80004005, `evaluate` 0x80070057, `set_variable` blaming the adapter,
|
|
104
|
+
* `watch` an `isError:false` answer whose every entry carried a hex code, and
|
|
105
|
+
* `cs_dbg_continue` waiting the full 15 s to answer `{"state":"running"}` —
|
|
106
|
+
* about 45 s of adapter round trips for a question answerable here in none.
|
|
107
|
+
*/
|
|
108
|
+
get isStopped() {
|
|
109
|
+
return this.stoppedNow();
|
|
110
|
+
}
|
|
60
111
|
constructor(channel, timeoutMs) {
|
|
61
112
|
super();
|
|
62
113
|
this.channel = channel;
|
|
@@ -65,6 +116,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
65
116
|
this.channel.onClose((cause) => {
|
|
66
117
|
this.state = "terminated";
|
|
67
118
|
this.configured = false;
|
|
119
|
+
this.sessionStarted = false;
|
|
68
120
|
const detail = cause ? ` (${cause.message})` : "";
|
|
69
121
|
for (const [, p] of this.pending) {
|
|
70
122
|
clearTimeout(p.timer);
|
|
@@ -262,6 +314,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
262
314
|
async start(mode, args) {
|
|
263
315
|
this.lastStartMode = mode;
|
|
264
316
|
this.lastStartArgs = args;
|
|
317
|
+
this.sessionStarted = false;
|
|
265
318
|
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
266
319
|
const onInit = this.waitEvent("initialized", Math.min(this.timeoutMs, 5000));
|
|
267
320
|
this.capabilities = await this.request("initialize", {
|
|
@@ -287,6 +340,9 @@ export class CsDapClient extends EventEmitter {
|
|
|
287
340
|
const startReq = this.request(mode, args).then(() => undefined, (err) => {
|
|
288
341
|
this.startFailure = err;
|
|
289
342
|
this.configured = false;
|
|
343
|
+
// A rejection arriving after `start()` returned ends the session, so the
|
|
344
|
+
// tools' session guard refuses from then on rather than reading `running`.
|
|
345
|
+
this.sessionStarted = false;
|
|
290
346
|
this.state = "terminated";
|
|
291
347
|
// A DISTINCT event name, deliberately: `error` is special-cased by
|
|
292
348
|
// EventEmitter and throws when unlistened. This one is safe to ignore.
|
|
@@ -313,6 +369,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
313
369
|
const fatal = this.startFailure ?? (configureDoneAdvertised ? configureFailure : null);
|
|
314
370
|
if (fatal) {
|
|
315
371
|
this.configured = false;
|
|
372
|
+
this.sessionStarted = false;
|
|
316
373
|
this.state = "terminated";
|
|
317
374
|
void startReq;
|
|
318
375
|
const detail = fatal?.message ?? String(fatal);
|
|
@@ -321,6 +378,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
321
378
|
`${JSON.stringify(args).slice(0, 200)}).`);
|
|
322
379
|
}
|
|
323
380
|
this.configured = true;
|
|
381
|
+
this.sessionStarted = true;
|
|
324
382
|
// 🔴 ONLY from `initialized`, never unconditionally. The adapter can emit `stopped`
|
|
325
383
|
// (or `terminated`) between the configurationDone RESPONSE and this line, and the
|
|
326
384
|
// event handler has already moved the state — a blind `= "running"` clobbers it and
|
|
@@ -430,6 +488,7 @@ export class CsDapClient extends EventEmitter {
|
|
|
430
488
|
this.channel.close();
|
|
431
489
|
this.state = "disconnected";
|
|
432
490
|
this.configured = false;
|
|
491
|
+
this.sessionStarted = false;
|
|
433
492
|
}
|
|
434
493
|
}
|
|
435
494
|
//# sourceMappingURL=csdap.js.map
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ async function main() {
|
|
|
50
50
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
51
51
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
52
52
|
// godot://… resources and receive notifications/resources/updated.
|
|
53
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
53
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.78.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
54
54
|
// 208 §7.1 — delete from the wire what the protocol already says: the SDK's
|
|
55
55
|
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
56
56
|
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
package/dist/tools/csdap.js
CHANGED
|
@@ -105,6 +105,67 @@ const CS_DAP_PATHS = {
|
|
|
105
105
|
};
|
|
106
106
|
export function registerCsDapTools(server, dap, cfg) {
|
|
107
107
|
const root = cfg.csDapProjectPath;
|
|
108
|
+
/**
|
|
109
|
+
* Refuse when no launch/attach the adapter accepted is in force.
|
|
110
|
+
*
|
|
111
|
+
* 🔴 Measured against a real netcoredbg 3.2.0-1092 on a client that had never
|
|
112
|
+
* launched anything. Nine readers, and the only one that refused did so by
|
|
113
|
+
* accident:
|
|
114
|
+
*
|
|
115
|
+
* ```
|
|
116
|
+
* cs_dbg_stack_trace 11ms Failed command 'stackTrace' : 0x80004005
|
|
117
|
+
* cs_dbg_scopes 1ms Failed command 'scopes' : 0x80004005
|
|
118
|
+
* cs_dbg_variables 1ms Failed command 'variables' : 0x80004005
|
|
119
|
+
* cs_dbg_evaluate 1ms error: 0x80004005
|
|
120
|
+
* cs_dbg_watch 1ms isError:FALSE, every entry error "error: 0x80004005"
|
|
121
|
+
* cs_dbg_set_variable 1ms "the debug adapter reported a failure with no message"
|
|
122
|
+
* cs_dbg_step 1ms Failed command 'next' : 0x80004005
|
|
123
|
+
* cs_dbg_continue 0ms Failed command 'continue' : 0x80004005
|
|
124
|
+
* cs_dbg_restart 1ms refuses — because it reads `lastStartMode`
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* `0x80004005` is E_FAIL and `0x80070057` is E_INVALIDARG: hex for "no", naming
|
|
128
|
+
* nothing the caller can act on and pointing at the adapter for a state the host
|
|
129
|
+
* already knew. The refusal names the two ways to open a session instead.
|
|
130
|
+
*/
|
|
131
|
+
const requireSession = (tool) => {
|
|
132
|
+
if (dap.hasSession)
|
|
133
|
+
return;
|
|
134
|
+
refuse(`${tool} needs a C# debug session: call cs_dbg_launch or cs_dbg_attach first. ` +
|
|
135
|
+
`There is none, so any state, frame or scope reported here would be invented — ` +
|
|
136
|
+
`this call previously answered with the adapter's raw failure code.`, "no_session");
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Refuse when the session is live but the program is not AT A STOP.
|
|
140
|
+
*
|
|
141
|
+
* 🔴 THE SAME PROXY, ONE PLANE OVER — session 262 §1's whole finding, ported after
|
|
142
|
+
* measuring it here rather than assumed. A session existing is a proxy for a frame
|
|
143
|
+
* existing, and the two part company the moment the program runs on. Measured with
|
|
144
|
+
* a launched session and nothing stopped:
|
|
145
|
+
*
|
|
146
|
+
* ```
|
|
147
|
+
* cs_dbg_stack_trace 2ms Failed command 'stackTrace' : 0x80070057
|
|
148
|
+
* cs_dbg_scopes 1ms Failed command 'scopes' : 0x80004005
|
|
149
|
+
* cs_dbg_variables 1ms Failed command 'variables' : 0x80004005
|
|
150
|
+
* cs_dbg_evaluate 1ms error: 0x80070057
|
|
151
|
+
* cs_dbg_set_variable 1ms "the debug adapter reported a failure with no message"
|
|
152
|
+
* cs_dbg_step 1ms Failed command 'next' : 0x80004005
|
|
153
|
+
* cs_dbg_continue 15,000ms isError:FALSE {"state":"running"}
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* 🔴 RAISED BEFORE THE ADAPTER ROUND TRIP, which is most of the point: that 15 s —
|
|
157
|
+
* with `cs_dbg_restart`'s 30 s beside it — is time the caller spent being told
|
|
158
|
+
* nothing, for a question `dap.isStopped` answers in none. The refusal names the
|
|
159
|
+
* state it READ rather than the state it assumed, and names both ways to reach a
|
|
160
|
+
* stop, because "not stopped" is not itself an instruction.
|
|
161
|
+
*/
|
|
162
|
+
const requireStopped = (tool) => {
|
|
163
|
+
if (dap.isStopped)
|
|
164
|
+
return;
|
|
165
|
+
refuse(`${tool} needs the program stopped at a breakpoint: the C# debug session is live but the program is ${dap.state}. ` +
|
|
166
|
+
`Arm a line with cs_dbg_set_breakpoints and trigger that code path, or step from a stop you already have — ` +
|
|
167
|
+
`nothing here can be read from a program that is still running.`, "not_stopped");
|
|
168
|
+
};
|
|
108
169
|
/**
|
|
109
170
|
* Resolve a breakpoint source path and REFUSE one that cannot carry a breakpoint.
|
|
110
171
|
*
|
|
@@ -272,6 +333,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
272
333
|
inputSchema: {},
|
|
273
334
|
}, async () => {
|
|
274
335
|
try {
|
|
336
|
+
requireSession("cs_dbg_continue");
|
|
337
|
+
requireStopped("cs_dbg_continue");
|
|
275
338
|
const r = await dap.resume("continue", { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
276
339
|
return ok({ state: r.state, stopped_reason: r.reason });
|
|
277
340
|
}
|
|
@@ -286,6 +349,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
286
349
|
inputSchema: { kind: z.enum(["in", "over", "out"]).describe("Step kind") },
|
|
287
350
|
}, async ({ kind }) => {
|
|
288
351
|
try {
|
|
352
|
+
requireSession("cs_dbg_step");
|
|
353
|
+
requireStopped("cs_dbg_step");
|
|
289
354
|
const command = kind === "in" ? "stepIn" : kind === "out" ? "stepOut" : "next";
|
|
290
355
|
const r = await dap.resume(command, { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
291
356
|
return ok({ state: r.state, stopped_reason: r.reason });
|
|
@@ -300,6 +365,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
300
365
|
inputSchema: { levels: z.number().int().positive().optional().describe("Max frames (default 20)") },
|
|
301
366
|
}, async ({ levels }) => {
|
|
302
367
|
try {
|
|
368
|
+
requireSession("cs_dbg_stack_trace");
|
|
369
|
+
requireStopped("cs_dbg_stack_trace");
|
|
303
370
|
const body = await dap.request("stackTrace", { threadId: dap.threadId(), startFrame: 0, levels: levels ?? 20 });
|
|
304
371
|
const frames = Array.isArray(body["stackFrames"])
|
|
305
372
|
? body["stackFrames"].map((f) => ({
|
|
@@ -318,6 +385,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
318
385
|
inputSchema: { frame_id: z.number().int().describe("Frame id from cs_dbg_stack_trace") },
|
|
319
386
|
}, async ({ frame_id }) => {
|
|
320
387
|
try {
|
|
388
|
+
requireSession("cs_dbg_scopes");
|
|
389
|
+
requireStopped("cs_dbg_scopes");
|
|
321
390
|
const body = await dap.request("scopes", { frameId: frame_id });
|
|
322
391
|
const scopes = Array.isArray(body["scopes"])
|
|
323
392
|
? body["scopes"].map((s) => ({ name: s.name ?? "", variables_ref: s.variablesReference ?? 0 }))
|
|
@@ -334,6 +403,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
334
403
|
inputSchema: { variables_ref: z.number().int().describe("variablesReference from cs_dbg_scopes or a parent variable") },
|
|
335
404
|
}, async ({ variables_ref }) => {
|
|
336
405
|
try {
|
|
406
|
+
requireSession("cs_dbg_variables");
|
|
407
|
+
requireStopped("cs_dbg_variables");
|
|
337
408
|
const body = await dap.request("variables", { variablesReference: variables_ref });
|
|
338
409
|
const variables = Array.isArray(body["variables"])
|
|
339
410
|
? body["variables"].map((v) => ({
|
|
@@ -357,6 +428,11 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
357
428
|
},
|
|
358
429
|
}, async ({ expression, frame_id, confirm }) => {
|
|
359
430
|
try {
|
|
431
|
+
// 🔴 BEFORE the confirmation prompt, not after. Asking the operator to approve
|
|
432
|
+
// an evaluate that cannot run — and that previously answered `0x80070057` when
|
|
433
|
+
// they approved it — spends their attention on a call already known to fail.
|
|
434
|
+
requireSession("cs_dbg_evaluate");
|
|
435
|
+
requireStopped("cs_dbg_evaluate");
|
|
360
436
|
const blocked = await gate(server, confirm, `Evaluate C# expression in the running game: ${expression}`);
|
|
361
437
|
if (blocked)
|
|
362
438
|
return blocked;
|
|
@@ -393,6 +469,11 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
393
469
|
},
|
|
394
470
|
}, async ({ variables_ref, name, value, confirm }) => {
|
|
395
471
|
try {
|
|
472
|
+
// Before the capability read as well as before the prompt: `capabilities` is
|
|
473
|
+
// null until `initialize`, so with no session the check below is a question
|
|
474
|
+
// asked of nobody.
|
|
475
|
+
requireSession("cs_dbg_set_variable");
|
|
476
|
+
requireStopped("cs_dbg_set_variable");
|
|
396
477
|
if (dap.capabilities && dap.capabilities["supportsSetVariable"] === false) {
|
|
397
478
|
return {
|
|
398
479
|
isError: true,
|
|
@@ -436,12 +517,37 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
436
517
|
},
|
|
437
518
|
}, async ({ add, remove, clear, frame_id }) => {
|
|
438
519
|
try {
|
|
520
|
+
requireSession("cs_dbg_watch");
|
|
521
|
+
// 🔴 THE ONE READER HERE NOT REFUSED WHILE THE PROGRAM RUNS, ON PURPOSE — the
|
|
522
|
+
// same deliberate divergence session 262 made on the GDScript plane, for the
|
|
523
|
+
// same reason. `cs_dbg_watch` is two things at once: it MANAGES a persistent
|
|
524
|
+
// set and it evaluates that set, and this tool's own description teaches
|
|
525
|
+
// arming the set and re-reading it "after a step/continue". Refusing the
|
|
526
|
+
// mutation while the program runs would refuse the documented workflow.
|
|
527
|
+
// The set change is applied; only the VALUES are unavailable, and the entry's
|
|
528
|
+
// own `error` field says which.
|
|
439
529
|
if (clear)
|
|
440
530
|
dap.clearWatches();
|
|
441
531
|
if (remove && remove.length)
|
|
442
532
|
dap.removeWatches(remove);
|
|
443
533
|
if (add && add.length)
|
|
444
534
|
dap.addWatches(add);
|
|
535
|
+
if (!dap.isStopped) {
|
|
536
|
+
// 🔴 What this replaces was worse than empty. Measured against a real
|
|
537
|
+
// netcoredbg with no session and again with the program running: an
|
|
538
|
+
// `isError:false` answer whose every entry carried `error: "error:
|
|
539
|
+
// 0x80004005"` — a hex code offered to the caller as the reason their watch
|
|
540
|
+
// has no value. The reason is that the program is not stopped, the host
|
|
541
|
+
// knows it, and it costs no round trip to say so.
|
|
542
|
+
return ok({
|
|
543
|
+
watches: dap.listWatches().map((expression) => ({
|
|
544
|
+
expression,
|
|
545
|
+
value: "",
|
|
546
|
+
type: "",
|
|
547
|
+
error: `not stopped (${dap.state}) — a watch is evaluated in a stopped frame, so values arrive at the next stop`,
|
|
548
|
+
})),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
445
551
|
// Bound each watch's evaluate to the short deadline (mirrors cs_dbg_evaluate): a watch
|
|
446
552
|
// the adapter never answers fails fast on that entry instead of hanging the full DAP
|
|
447
553
|
// timeout at every stop, while other watches still resolve normally.
|
|
@@ -468,6 +574,21 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
468
574
|
// Per the DAP spec a client should only send setExceptionBreakpoints when the adapter
|
|
469
575
|
// advertised at least one exception filter. Short-circuit with a clear "unsupported"
|
|
470
576
|
// message otherwise, matching the GDScript dbg_set_exception_breakpoints discipline.
|
|
577
|
+
// 🔴 "THE CONNECTED ADAPTER" MUST FIRST BE ONE. `capabilities` is null until
|
|
578
|
+
// `initialize`, so with no session this read found nothing and the tool told
|
|
579
|
+
// the caller their debugger advertises no exception filters. Measured against
|
|
580
|
+
// a real netcoredbg 3.2.0-1092: with no session it answered "unsupported by
|
|
581
|
+
// the connected C# debug adapter (it advertises no exceptionBreakpointFilters)"
|
|
582
|
+
// — while that same adapter advertises `all` and `user-unhandled`, which the
|
|
583
|
+
// cs-dap cohort asserts on every CI leg. A capability read is a question for
|
|
584
|
+
// somebody, and there was nobody. Session 262 §2 fixed this shape one plane
|
|
585
|
+
// over, where the message blamed the user's engine BUILD; here it blamed their
|
|
586
|
+
// debugger for a handshake that never happened.
|
|
587
|
+
//
|
|
588
|
+
// NOT `requireStopped`: exception filters are armed for the FUTURE, so a live
|
|
589
|
+
// running session is a legitimate caller — the same call `dbg_data_breakpoints`
|
|
590
|
+
// gets on the GDScript plane, and for the same reason.
|
|
591
|
+
requireSession("cs_dbg_set_exception_breakpoints");
|
|
471
592
|
const advertised = dap.capabilities?.["exceptionBreakpointFilters"];
|
|
472
593
|
const available_filters = Array.isArray(advertised)
|
|
473
594
|
? advertised.map((f) => ({ filter: f.filter ?? "", label: f.label ?? "" }))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.78.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",
|