breakpoint-mcp 1.0.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/bridge.js +189 -0
- package/dist/config.js +71 -0
- package/dist/confirm.js +45 -0
- package/dist/csdap.js +331 -0
- package/dist/cslsp.js +217 -0
- package/dist/dap.js +341 -0
- package/dist/framing.js +125 -0
- package/dist/index.js +110 -0
- package/dist/logger.js +11 -0
- package/dist/lsp.js +219 -0
- package/dist/paths.js +28 -0
- package/dist/schemas.js +587 -0
- package/dist/stdio.js +101 -0
- package/dist/subscriptions.js +141 -0
- package/dist/tasks.js +146 -0
- package/dist/tools/assetgen.js +360 -0
- package/dist/tools/backend.js +532 -0
- package/dist/tools/cli.js +130 -0
- package/dist/tools/csdap.js +377 -0
- package/dist/tools/cslsp.js +285 -0
- package/dist/tools/dap.js +504 -0
- package/dist/tools/editor.js +1919 -0
- package/dist/tools/knowledge.js +517 -0
- package/dist/tools/lsp-common.js +121 -0
- package/dist/tools/lsp.js +576 -0
- package/dist/tools/netcode.js +411 -0
- package/dist/tools/processes.js +103 -0
- package/dist/tools/resources.js +27 -0
- package/dist/tools/runtime.js +125 -0
- package/package.json +57 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DapError } from "../dap.js";
|
|
3
|
+
import { toFsPath } from "../paths.js";
|
|
4
|
+
import { gate } from "../confirm.js";
|
|
5
|
+
import { ok } from "./lsp-common.js";
|
|
6
|
+
// How long step/continue wait for the program to settle (hit a breakpoint,
|
|
7
|
+
// finish a step, or terminate) before returning. On timeout the tool reports
|
|
8
|
+
// the current state — e.g. `continue` with no further breakpoint stays running.
|
|
9
|
+
const RESUME_WAIT_MS = 15000;
|
|
10
|
+
function fail(err) {
|
|
11
|
+
const e = err;
|
|
12
|
+
return {
|
|
13
|
+
isError: true,
|
|
14
|
+
content: [{ type: "text", text: `DAP error [${e.command ?? "error"}]: ${e.message ?? String(err)}` }],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when `err` is a DAP request that hit its own request deadline (as opposed to an
|
|
19
|
+
* adapter-reported failure or a dropped connection). Used to turn the setVariable /
|
|
20
|
+
* evaluate hangs on advertised-but-unimplemented Godot builds into a clear message.
|
|
21
|
+
*/
|
|
22
|
+
function isDapTimeout(err) {
|
|
23
|
+
return err instanceof DapError && /timed out after/.test(err.message);
|
|
24
|
+
}
|
|
25
|
+
// Per-line breakpoint modifier fields, each gated by an adapter capability. Godot 4.3
|
|
26
|
+
// advertises all three false AND ignores them (a "conditional" breakpoint would halt
|
|
27
|
+
// unconditionally — verified live in the dap-plane's editor-dap-breakpoints probe), so we
|
|
28
|
+
// feature-detect: drop an unsupported modifier and warn, mirroring the
|
|
29
|
+
// dbg_set_exception_breakpoints / dbg_goto / dbg_data_breakpoints discipline.
|
|
30
|
+
const BREAKPOINT_MODIFIER_CAPS = {
|
|
31
|
+
condition: "supportsConditionalBreakpoints",
|
|
32
|
+
hitCondition: "supportsHitConditionalBreakpoints",
|
|
33
|
+
logMessage: "supportsLogPoints",
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Which of condition/hitCondition/logMessage the connected adapter does NOT support, out of
|
|
37
|
+
* the ones actually requested. Returns [] when capabilities are unknown (no session yet) —
|
|
38
|
+
* we can only feature-detect once the adapter has advertised what it supports.
|
|
39
|
+
*/
|
|
40
|
+
function unsupportedBreakpointModifiers(caps, requested) {
|
|
41
|
+
if (!caps)
|
|
42
|
+
return [];
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const field of ["condition", "hitCondition", "logMessage"]) {
|
|
45
|
+
if (requested[field] && caps[BREAKPOINT_MODIFIER_CAPS[field]] !== true)
|
|
46
|
+
out.push(field);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** True when a per-line modifier array carries at least one non-null, non-empty entry. */
|
|
51
|
+
function hasModifier(arr) {
|
|
52
|
+
return Array.isArray(arr) && arr.some((v) => v != null && v !== "");
|
|
53
|
+
}
|
|
54
|
+
export function registerDapTools(server, dap, cfg) {
|
|
55
|
+
server.registerTool("dbg_launch", {
|
|
56
|
+
title: "Launch debug session",
|
|
57
|
+
description: "Start the game under the debugger. scene may be 'main', 'current', or a res:// scene path. " +
|
|
58
|
+
"Any breakpoints set beforehand are applied during the handshake.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
scene: z.string().optional().describe("'main' (default), 'current', or res://scene.tscn"),
|
|
61
|
+
stop_on_entry: z.boolean().optional().describe("Break at entry (default false)"),
|
|
62
|
+
},
|
|
63
|
+
}, async ({ scene, stop_on_entry }) => {
|
|
64
|
+
try {
|
|
65
|
+
await dap.start("launch", {
|
|
66
|
+
project: cfg.projectPath,
|
|
67
|
+
scene: scene ?? "main",
|
|
68
|
+
stopOnEntry: stop_on_entry ?? false,
|
|
69
|
+
});
|
|
70
|
+
return ok({ session_id: "godot", state: dap.state, scene: scene ?? "main" });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return fail(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
server.registerTool("dbg_attach", {
|
|
77
|
+
title: "Attach debug session",
|
|
78
|
+
description: "Attach to an already-running Godot debug session.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
address: z.string().optional().describe("Address of the running game (default 127.0.0.1)"),
|
|
81
|
+
port: z.number().int().optional().describe("Remote debug port"),
|
|
82
|
+
},
|
|
83
|
+
}, async ({ address, port }) => {
|
|
84
|
+
try {
|
|
85
|
+
await dap.start("attach", { address: address ?? "127.0.0.1", port });
|
|
86
|
+
return ok({ session_id: "godot", state: dap.state });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return fail(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
server.registerTool("dbg_set_breakpoints", {
|
|
93
|
+
title: "Set breakpoints",
|
|
94
|
+
description: "Set (replace) the breakpoints for a source file. Applied immediately if a session is running, else buffered until launch. " +
|
|
95
|
+
"Feature-detected: the per-line conditions / hit_conditions / log_messages modifiers are only sent when the connected adapter " +
|
|
96
|
+
"advertises support (supportsConditionalBreakpoints / supportsHitConditionalBreakpoints / supportsLogPoints). On an adapter that " +
|
|
97
|
+
"advertises them unsupported (e.g. Godot 4.3, which ignores them and would otherwise halt unconditionally) the modifier is dropped " +
|
|
98
|
+
"and the result includes `unsupported_modifiers` plus a `warning`. Detection needs a live session, so set modifiers after dbg_launch.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
path: z.string().describe("Script path (res://..., absolute, or project-relative)"),
|
|
101
|
+
lines: z.array(z.number().int().positive()).describe("1-based line numbers"),
|
|
102
|
+
conditions: z.array(z.string().nullable()).optional().describe("Optional per-line condition expressions (aligned to lines, use null to skip a line); break only when the expression is true"),
|
|
103
|
+
hit_conditions: z.array(z.string().nullable()).optional().describe("Optional per-line hit expressions aligned to lines, e.g. '>3' or '%5' — break based on hit count (null to skip)"),
|
|
104
|
+
log_messages: z.array(z.string().nullable()).optional().describe("Optional per-line log messages aligned to lines; a message turns that breakpoint into a LOGPOINT (logs and continues, never halts). {expr} interpolates (null to skip)."),
|
|
105
|
+
},
|
|
106
|
+
}, async ({ path, lines, conditions, hit_conditions, log_messages }) => {
|
|
107
|
+
try {
|
|
108
|
+
const fsPath = toFsPath(path, cfg.projectPath);
|
|
109
|
+
// Feature-detect the per-line modifiers against the connected adapter. When it does
|
|
110
|
+
// not advertise support (Godot 4.3 advertises none AND ignores them, so the
|
|
111
|
+
// breakpoint would halt unconditionally), DROP the field and warn rather than send
|
|
112
|
+
// something the adapter mishandles.
|
|
113
|
+
const dropped = unsupportedBreakpointModifiers(dap.capabilities, {
|
|
114
|
+
condition: hasModifier(conditions),
|
|
115
|
+
hitCondition: hasModifier(hit_conditions),
|
|
116
|
+
logMessage: hasModifier(log_messages),
|
|
117
|
+
});
|
|
118
|
+
const drop = new Set(dropped);
|
|
119
|
+
const body = await dap.setBreakpoints(fsPath, lines, drop.has("condition") ? undefined : conditions, drop.has("hitCondition") ? undefined : hit_conditions, drop.has("logMessage") ? undefined : log_messages);
|
|
120
|
+
const verified = Array.isArray(body["breakpoints"])
|
|
121
|
+
? body["breakpoints"].map((b) => ({ line: b.line ?? 0, verified: Boolean(b.verified) }))
|
|
122
|
+
: [];
|
|
123
|
+
const result = { path: fsPath, buffered: body["buffered"] === true, breakpoints: verified };
|
|
124
|
+
if (dropped.length) {
|
|
125
|
+
result.unsupported_modifiers = dropped;
|
|
126
|
+
result.warning =
|
|
127
|
+
`The connected Godot debug adapter does not support ${dropped.join(", ")} on breakpoints (it advertises ` +
|
|
128
|
+
`${dropped.length > 1 ? "them" : "it"} unsupported), so ${dropped.length > 1 ? "they were" : "it was"} dropped — ` +
|
|
129
|
+
`the affected breakpoint(s) will halt unconditionally.`;
|
|
130
|
+
}
|
|
131
|
+
return ok(result);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return fail(err);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
server.registerTool("dbg_continue", {
|
|
138
|
+
title: "Continue",
|
|
139
|
+
description: "Resume execution and wait for the program to settle again (next breakpoint or termination). " +
|
|
140
|
+
"Returns the resulting state; if it runs on with no further breakpoint, reports state 'running'.",
|
|
141
|
+
inputSchema: {},
|
|
142
|
+
}, async () => {
|
|
143
|
+
try {
|
|
144
|
+
const r = await dap.resume("continue", { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
145
|
+
return ok({ state: r.state, stopped_reason: r.reason });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return fail(err);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
server.registerTool("dbg_step", {
|
|
152
|
+
title: "Step",
|
|
153
|
+
description: "Step execution: 'over' (next), 'in' (stepIn), or 'out' (stepOut), then wait for the step to land. " +
|
|
154
|
+
"Returns the resulting state and stop reason. Note: stepOut may be unsupported on older Godot builds.",
|
|
155
|
+
inputSchema: { kind: z.enum(["in", "over", "out"]).describe("Step kind") },
|
|
156
|
+
}, async ({ kind }) => {
|
|
157
|
+
try {
|
|
158
|
+
const command = kind === "in" ? "stepIn" : kind === "out" ? "stepOut" : "next";
|
|
159
|
+
const r = await dap.resume(command, { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
160
|
+
return ok({ state: r.state, stopped_reason: r.reason });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
return fail(err);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.registerTool("dbg_stack_trace", {
|
|
167
|
+
title: "Stack trace",
|
|
168
|
+
description: "Return the current call stack (only meaningful while stopped at a breakpoint).",
|
|
169
|
+
inputSchema: { levels: z.number().int().positive().optional().describe("Max frames (default 20)") },
|
|
170
|
+
}, async ({ levels }) => {
|
|
171
|
+
try {
|
|
172
|
+
const body = await dap.request("stackTrace", { threadId: dap.threadId(), startFrame: 0, levels: levels ?? 20 });
|
|
173
|
+
const frames = Array.isArray(body["stackFrames"])
|
|
174
|
+
? body["stackFrames"].map((f) => ({
|
|
175
|
+
id: f.id ?? 0, name: f.name ?? "", source: f.source?.path ?? f.source?.name ?? "", line: f.line ?? 0,
|
|
176
|
+
}))
|
|
177
|
+
: [];
|
|
178
|
+
return ok({ frames });
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return fail(err);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
server.registerTool("dbg_scopes", {
|
|
185
|
+
title: "Scopes",
|
|
186
|
+
description: "Return the variable scopes (Locals, Members, Globals) for a stack frame.",
|
|
187
|
+
inputSchema: { frame_id: z.number().int().describe("Frame id from dbg_stack_trace") },
|
|
188
|
+
}, async ({ frame_id }) => {
|
|
189
|
+
try {
|
|
190
|
+
const body = await dap.request("scopes", { frameId: frame_id });
|
|
191
|
+
const scopes = Array.isArray(body["scopes"])
|
|
192
|
+
? body["scopes"].map((s) => ({ name: s.name ?? "", variables_ref: s.variablesReference ?? 0 }))
|
|
193
|
+
: [];
|
|
194
|
+
return ok({ scopes });
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
return fail(err);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
server.registerTool("dbg_variables", {
|
|
201
|
+
title: "Variables",
|
|
202
|
+
description: "List variables under a scope or a complex value (via its variables_ref).",
|
|
203
|
+
inputSchema: { variables_ref: z.number().int().describe("variablesReference from dbg_scopes or a parent variable") },
|
|
204
|
+
}, async ({ variables_ref }) => {
|
|
205
|
+
try {
|
|
206
|
+
const body = await dap.request("variables", { variablesReference: variables_ref });
|
|
207
|
+
const variables = Array.isArray(body["variables"])
|
|
208
|
+
? body["variables"].map((v) => ({
|
|
209
|
+
name: v.name ?? "", value: v.value ?? "", type: v.type ?? "", variables_ref: v.variablesReference ?? 0,
|
|
210
|
+
}))
|
|
211
|
+
: [];
|
|
212
|
+
return ok({ variables });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return fail(err);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
server.registerTool("dbg_evaluate", {
|
|
219
|
+
title: "Evaluate expression",
|
|
220
|
+
description: "Evaluate a GDScript expression in the context of a stopped frame. DESTRUCTIVE: arbitrary code execution — confirm with the user and keep this capability gated.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
expression: z.string().describe("GDScript expression to evaluate"),
|
|
223
|
+
frame_id: z.number().int().optional().describe("Frame id (from dbg_stack_trace); omit for the top frame"),
|
|
224
|
+
confirm: z.boolean().optional().describe("Auto-approve this arbitrary-code evaluation (skip the confirmation prompt)"),
|
|
225
|
+
},
|
|
226
|
+
}, async ({ expression, frame_id, confirm }) => {
|
|
227
|
+
try {
|
|
228
|
+
const blocked = await gate(server, confirm, `Evaluate expression in the running game: ${expression}`);
|
|
229
|
+
if (blocked)
|
|
230
|
+
return blocked;
|
|
231
|
+
// Bound the evaluate request to a short deadline instead of the full dapTimeoutMs:
|
|
232
|
+
// a compliant adapter answers a repl evaluate near-instantly, so a non-response means
|
|
233
|
+
// the adapter is not going to answer — fail fast rather than hang.
|
|
234
|
+
let body;
|
|
235
|
+
try {
|
|
236
|
+
body = await dap.request("evaluate", { expression, frameId: frame_id, context: "repl" }, cfg.dapEvaluateTimeoutMs);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (isDapTimeout(err)) {
|
|
240
|
+
return {
|
|
241
|
+
isError: true,
|
|
242
|
+
content: [{ type: "text", text: `The debug adapter did not answer the evaluate request within ${cfg.dapEvaluateTimeoutMs}ms — no result was returned. The debug session is still alive; use dbg_variables / dbg_watch to inspect state.` }],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
return ok({ result: String(body["result"] ?? ""), type: String(body["type"] ?? ""), variables_ref: body["variablesReference"] ?? 0 });
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
return fail(err);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
server.registerTool("dbg_watch", {
|
|
254
|
+
title: "Watch expressions",
|
|
255
|
+
description: "Manage a persistent set of watch expressions and evaluate them in the current stopped frame. " +
|
|
256
|
+
"Pass `add`/`remove`/`clear` to mutate the set (all optional), then every current watch is re-evaluated " +
|
|
257
|
+
"and returned. Call with no mutation args to just re-read the watches after a step/continue. Expressions " +
|
|
258
|
+
"are evaluated in DAP `watch` context (intended to be side-effect-free), so this is not gated; the results " +
|
|
259
|
+
"are only meaningful while the program is stopped at a breakpoint.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
add: z.array(z.string()).optional().describe("Expressions to add to the watch set"),
|
|
262
|
+
remove: z.array(z.string()).optional().describe("Expressions to remove from the watch set"),
|
|
263
|
+
clear: z.boolean().optional().describe("Clear all watches before applying add (default false)"),
|
|
264
|
+
frame_id: z.number().int().optional().describe("Frame id from dbg_stack_trace; omit for the top frame"),
|
|
265
|
+
},
|
|
266
|
+
}, async ({ add, remove, clear, frame_id }) => {
|
|
267
|
+
try {
|
|
268
|
+
if (clear)
|
|
269
|
+
dap.clearWatches();
|
|
270
|
+
if (remove && remove.length)
|
|
271
|
+
dap.removeWatches(remove);
|
|
272
|
+
if (add && add.length)
|
|
273
|
+
dap.addWatches(add);
|
|
274
|
+
// Bound each watch's evaluate to the short deadline (mirrors dbg_evaluate): a watch
|
|
275
|
+
// expression the adapter never answers fails fast on that entry instead of hanging the
|
|
276
|
+
// full dapTimeoutMs (20 s) at every stop, while other watches still resolve normally.
|
|
277
|
+
const watches = await dap.evaluateWatches(frame_id, cfg.dapEvaluateTimeoutMs);
|
|
278
|
+
return ok({ watches });
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
return fail(err);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
server.registerTool("dbg_set_exception_breakpoints", {
|
|
285
|
+
title: "Set exception breakpoints",
|
|
286
|
+
description: "Enable (replace) the debugger's exception breakpoint filters so execution halts when a matching error/exception is thrown " +
|
|
287
|
+
"(DAP setExceptionBreakpoints). Pass the filter IDs to enable; call with no filters (or []) to clear them. The result echoes the " +
|
|
288
|
+
"active filters and lists `available_filters` — the exception filters the connected adapter actually advertises. " +
|
|
289
|
+
"Requires a running debug session. Not gated (it only configures the debugger). " +
|
|
290
|
+
"Feature-detected: on an adapter that advertises no exceptionBreakpointFilters (e.g. Godot 4.3, which also does not answer " +
|
|
291
|
+
"the request — it would otherwise time out) it returns a clear \"unsupported\" message WITHOUT sending anything.",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
filters: z.array(z.string()).optional().describe("Exception filter IDs to enable (default none = clear). Choose from available_filters in the result."),
|
|
294
|
+
},
|
|
295
|
+
}, async ({ filters }) => {
|
|
296
|
+
try {
|
|
297
|
+
// Per the DAP spec a client should only send setExceptionBreakpoints when the
|
|
298
|
+
// adapter advertised at least one exception filter. Godot 4.3 advertises none and
|
|
299
|
+
// does not answer the request (it would time out), so short-circuit with a clear
|
|
300
|
+
// "unsupported" message instead of hanging until that timeout.
|
|
301
|
+
const advertised = dap.capabilities?.["exceptionBreakpointFilters"];
|
|
302
|
+
const available_filters = Array.isArray(advertised)
|
|
303
|
+
? advertised.map((f) => ({ filter: f.filter ?? "", label: f.label ?? "" }))
|
|
304
|
+
: [];
|
|
305
|
+
if (available_filters.length === 0) {
|
|
306
|
+
return {
|
|
307
|
+
isError: true,
|
|
308
|
+
content: [{ type: "text", text: "dbg_set_exception_breakpoints is unsupported by the connected Godot build's debug adapter (it advertises no exceptionBreakpointFilters). There are no exception filters to enable on this build." }],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const active = filters ?? [];
|
|
312
|
+
const body = await dap.request("setExceptionBreakpoints", { filters: active });
|
|
313
|
+
const breakpoints = Array.isArray(body["breakpoints"])
|
|
314
|
+
? body["breakpoints"].map((b) => ({ verified: Boolean(b.verified) }))
|
|
315
|
+
: [];
|
|
316
|
+
return ok({ filters: active, available_filters, breakpoints });
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
return fail(err);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
server.registerTool("dbg_set_variable", {
|
|
323
|
+
title: "Set variable value",
|
|
324
|
+
description: "Change a variable's value in a stopped frame (DAP setVariable). DESTRUCTIVE: mutates live program state — confirm with the user and keep this gated. " +
|
|
325
|
+
"`variables_ref` is the container's variablesReference (from dbg_scopes, or a complex entry in dbg_variables), `name` is the variable's name within it, " +
|
|
326
|
+
"and `value` is the new value as a GDScript literal/expression. Only meaningful while stopped at a breakpoint.",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
variables_ref: z.number().int().describe("variablesReference of the containing scope/variable (from dbg_scopes or dbg_variables)"),
|
|
329
|
+
name: z.string().describe("Variable name within that container"),
|
|
330
|
+
value: z.string().describe("New value as a GDScript literal/expression"),
|
|
331
|
+
confirm: z.boolean().optional().describe("Auto-approve this mutation (skip the confirmation prompt)"),
|
|
332
|
+
},
|
|
333
|
+
}, async ({ variables_ref, name, value, confirm }) => {
|
|
334
|
+
try {
|
|
335
|
+
// Feature-detect: some debug adapters don't implement setVariable. If the
|
|
336
|
+
// adapter explicitly advertised it as unsupported, say so plainly instead
|
|
337
|
+
// of prompting for a confirmation and then failing.
|
|
338
|
+
if (dap.capabilities && dap.capabilities["supportsSetVariable"] === false) {
|
|
339
|
+
return {
|
|
340
|
+
isError: true,
|
|
341
|
+
content: [{ type: "text", text: "dbg_set_variable is unsupported by the connected Godot build's debug adapter (it does not advertise supportsSetVariable). Read-only inspection (dbg_variables) still works." }],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const blocked = await gate(server, confirm, `Set variable ${name} = ${value} in the running game`);
|
|
345
|
+
if (blocked)
|
|
346
|
+
return blocked;
|
|
347
|
+
// Godot 4.3 advertises supportsSetVariable=true (so the caps short-circuit above does
|
|
348
|
+
// not fire) but never answers the setVariable request. Caps can't detect that — 4.3
|
|
349
|
+
// lies — so bound the request to a short deadline and, on timeout, say plainly that the
|
|
350
|
+
// build does not implement setVariable rather than emitting the generic 20 s DAP timeout.
|
|
351
|
+
let body;
|
|
352
|
+
try {
|
|
353
|
+
body = await dap.request("setVariable", { variablesReference: variables_ref, name, value }, cfg.dapSetVarTimeoutMs);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
if (isDapTimeout(err)) {
|
|
357
|
+
return {
|
|
358
|
+
isError: true,
|
|
359
|
+
content: [{ type: "text", text: `The debug adapter advertises supportsSetVariable but did not answer the setVariable request within ${cfg.dapSetVarTimeoutMs}ms — this Godot build (e.g. 4.3) does not implement setVariable. No change was made; the variable is unchanged. Read-only inspection (dbg_variables) still works.` }],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
return ok({ name, value: String(body["value"] ?? value), type: String(body["type"] ?? ""), variables_ref: body["variablesReference"] ?? 0 });
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
return fail(err);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
server.registerTool("dbg_restart", {
|
|
371
|
+
title: "Restart debug session",
|
|
372
|
+
description: "Restart the current debug session. Uses the DAP `restart` request when the adapter advertises `supportsRestartRequest`, " +
|
|
373
|
+
"otherwise falls back to terminate + relaunch — so it works on every adapter. Reuses the last dbg_launch/dbg_attach parameters; " +
|
|
374
|
+
"pass `scene` / `stop_on_entry` to override them for a launched session. `method` in the result reports which path ran " +
|
|
375
|
+
"('restart' = native DAP restart, 'relaunch' = terminate + fresh handshake). Requires a session started with dbg_launch/dbg_attach.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
scene: z.string().optional().describe("Override the scene for a launched session: 'main', 'current', or res://scene.tscn"),
|
|
378
|
+
stop_on_entry: z.boolean().optional().describe("Override stop-at-entry for the restart (launched sessions)"),
|
|
379
|
+
},
|
|
380
|
+
}, async ({ scene, stop_on_entry }) => {
|
|
381
|
+
try {
|
|
382
|
+
const override = {};
|
|
383
|
+
if (scene !== undefined)
|
|
384
|
+
override.scene = scene;
|
|
385
|
+
if (stop_on_entry !== undefined)
|
|
386
|
+
override.stopOnEntry = stop_on_entry;
|
|
387
|
+
const r = await dap.restart(override);
|
|
388
|
+
return ok({ session_id: "godot", method: r.method, state: r.state, scene: r.scene });
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
return fail(err);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
server.registerTool("dbg_goto", {
|
|
395
|
+
title: "Go to line (set next statement)",
|
|
396
|
+
description: "Move the program counter within the current stopped frame — 'set next statement' (DAP gotoTargets + goto). Call with `path` + `line` to " +
|
|
397
|
+
"list the valid goto targets on that line; when the line has exactly one target (or you pass `target_id`) it jumps there. " +
|
|
398
|
+
"DESTRUCTIVE: skips or repeats code by moving execution — confirm with the user and keep this gated. " +
|
|
399
|
+
"Feature-detected: on an adapter that does not advertise `supportsGotoTargetsRequest` it returns a clear \"unsupported\" message WITHOUT prompting. " +
|
|
400
|
+
"Only meaningful while stopped at a breakpoint.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
path: z.string().describe("Script path (res://..., absolute, or project-relative)"),
|
|
403
|
+
line: z.number().int().positive().describe("1-based target line"),
|
|
404
|
+
target_id: z.number().int().optional().describe("A specific target id from a prior dbg_goto listing; omit to auto-pick when the line has a single target"),
|
|
405
|
+
confirm: z.boolean().optional().describe("Auto-approve the jump (skip the confirmation prompt)"),
|
|
406
|
+
},
|
|
407
|
+
}, async ({ path, line, target_id, confirm }) => {
|
|
408
|
+
try {
|
|
409
|
+
if (!dap.capabilities || dap.capabilities["supportsGotoTargetsRequest"] !== true) {
|
|
410
|
+
return {
|
|
411
|
+
isError: true,
|
|
412
|
+
content: [{ type: "text", text: "dbg_goto is unsupported by the connected Godot build's debug adapter (it does not advertise supportsGotoTargetsRequest)." }],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const fsPath = toFsPath(path, cfg.projectPath);
|
|
416
|
+
const body = await dap.request("gotoTargets", { source: { path: fsPath }, line });
|
|
417
|
+
const targets = Array.isArray(body["targets"])
|
|
418
|
+
? body["targets"].map((t) => ({ id: t.id ?? 0, label: t.label ?? "", line: t.line ?? 0 }))
|
|
419
|
+
: [];
|
|
420
|
+
const chosen = target_id !== undefined
|
|
421
|
+
? targets.find((t) => t.id === target_id)
|
|
422
|
+
: targets.length === 1 ? targets[0] : undefined;
|
|
423
|
+
if (!chosen) {
|
|
424
|
+
if (target_id !== undefined) {
|
|
425
|
+
return {
|
|
426
|
+
isError: true,
|
|
427
|
+
content: [{ type: "text", text: `No goto target with id ${target_id} on ${fsPath}:${line}. Call dbg_goto with just path+line to list the valid targets.` }],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// Zero or multiple targets: report them and jump nowhere.
|
|
431
|
+
return ok({ targets, jumped: false, target_id: null });
|
|
432
|
+
}
|
|
433
|
+
const blocked = await gate(server, confirm, `Move execution to ${fsPath}:${chosen.line} (${chosen.label}) in the running game`);
|
|
434
|
+
if (blocked)
|
|
435
|
+
return blocked;
|
|
436
|
+
await dap.request("goto", { threadId: dap.threadId(), targetId: chosen.id });
|
|
437
|
+
return ok({ targets: [chosen], jumped: true, target_id: chosen.id });
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return fail(err);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
server.registerTool("dbg_data_breakpoints", {
|
|
444
|
+
title: "Set data breakpoints (watchpoints)",
|
|
445
|
+
description: "Set (replace) data breakpoints — 'watchpoints' that halt when a variable's value changes (DAP dataBreakpointInfo + setDataBreakpoints). " +
|
|
446
|
+
"Pass `watch` as a list of { name, variables_ref?, access_type? }: each name is resolved to a dataId via dataBreakpointInfo, then every " +
|
|
447
|
+
"resolvable id is armed in one setDataBreakpoints call. Call with no `watch` (or []) to clear all data breakpoints. The result reports the " +
|
|
448
|
+
"armed `breakpoints` (each with its resolved data_id and verified flag) and any `unresolved` variables the adapter cannot watch. " +
|
|
449
|
+
"Requires a running session; NOT gated (it only configures the debugger). Feature-detected: on an adapter that does not advertise " +
|
|
450
|
+
"`supportsDataBreakpoints` it returns a clear \"unsupported\" message without sending any request.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
watch: z.array(z.object({
|
|
453
|
+
name: z.string().describe("Variable name to watch"),
|
|
454
|
+
variables_ref: z.number().int().optional().describe("variablesReference of the containing scope/variable (from dbg_scopes / dbg_variables); omit for a global/expression name"),
|
|
455
|
+
access_type: z.enum(["read", "write", "readWrite"]).optional().describe("When to break (default the adapter's default, usually write)"),
|
|
456
|
+
})).optional().describe("Variables to watch; omit or [] to clear all data breakpoints"),
|
|
457
|
+
},
|
|
458
|
+
}, async ({ watch }) => {
|
|
459
|
+
try {
|
|
460
|
+
if (!dap.capabilities || dap.capabilities["supportsDataBreakpoints"] !== true) {
|
|
461
|
+
return {
|
|
462
|
+
isError: true,
|
|
463
|
+
content: [{ type: "text", text: "dbg_data_breakpoints is unsupported by the connected Godot build's debug adapter (it does not advertise supportsDataBreakpoints)." }],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const requested = watch ?? [];
|
|
467
|
+
const resolved = [];
|
|
468
|
+
const unresolved = [];
|
|
469
|
+
for (const w of requested) {
|
|
470
|
+
try {
|
|
471
|
+
const info = await dap.request("dataBreakpointInfo", { name: w.name, variablesReference: w.variables_ref });
|
|
472
|
+
const dataId = info["dataId"];
|
|
473
|
+
if (typeof dataId === "string" && dataId.length > 0) {
|
|
474
|
+
resolved.push({ name: w.name, dataId, accessType: w.access_type });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
unresolved.push({ name: w.name, reason: String(info["description"] ?? "adapter returned no dataId for this variable") });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
const e = err;
|
|
482
|
+
unresolved.push({ name: w.name, reason: e.message ?? String(err) });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const body = await dap.request("setDataBreakpoints", {
|
|
486
|
+
breakpoints: resolved.map((r) => {
|
|
487
|
+
const b = { dataId: r.dataId };
|
|
488
|
+
if (r.accessType)
|
|
489
|
+
b.accessType = r.accessType;
|
|
490
|
+
return b;
|
|
491
|
+
}),
|
|
492
|
+
});
|
|
493
|
+
const verified = Array.isArray(body["breakpoints"])
|
|
494
|
+
? body["breakpoints"].map((b) => Boolean(b.verified))
|
|
495
|
+
: [];
|
|
496
|
+
const breakpoints = resolved.map((r, i) => ({ name: r.name, data_id: r.dataId, verified: verified[i] ?? false }));
|
|
497
|
+
return ok({ breakpoints, unresolved });
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
return fail(err);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
//# sourceMappingURL=dap.js.map
|