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,377 @@
|
|
|
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: `C# 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 (not an
|
|
19
|
+
* adapter-reported failure or a dropped connection). Used to turn a
|
|
20
|
+
* setVariable / evaluate non-response into a clear message rather than the
|
|
21
|
+
* generic timeout — the same F1 discipline the GDScript DAP plane uses.
|
|
22
|
+
*/
|
|
23
|
+
function isDapTimeout(err) {
|
|
24
|
+
return err instanceof DapError && /timed out after/.test(err.message);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* D4 C3 — the C#/.NET debugging plane. `cs_dbg_*` tools mirroring the read/inspect
|
|
28
|
+
* GDScript `dbg_*` surface, but driven by **netcoredbg** (Samsung, MIT — spawned
|
|
29
|
+
* over stdio by the host) attached to / launching a C# Godot game, instead of
|
|
30
|
+
* Godot's built-in TCP debug adapter.
|
|
31
|
+
*
|
|
32
|
+
* Same disciplines as the GDScript plane: destructive tools (`cs_dbg_evaluate`,
|
|
33
|
+
* `cs_dbg_set_variable`) are elicitation-gated; those two also carry a short
|
|
34
|
+
* bounded deadline so a non-answering adapter fails fast with a clear message
|
|
35
|
+
* (the F1 fix) instead of hanging the full DAP timeout. Adapter absent → the
|
|
36
|
+
* lazy stdio spawn fails with an actionable hint, never a hang. On top of the
|
|
37
|
+
* read/inspect surface this now carries the GDScript extras netcoredbg backs —
|
|
38
|
+
* `cs_dbg_watch`, `cs_dbg_set_exception_breakpoints` (netcoredbg advertises the
|
|
39
|
+
* `all` / `user-unhandled` filters) and `cs_dbg_restart` (terminate + relaunch,
|
|
40
|
+
* since netcoredbg advertises no `supportsRestartRequest`). `goto` and data
|
|
41
|
+
* breakpoints are deliberately NOT ported: netcoredbg advertises neither
|
|
42
|
+
* `supportsGotoTargetsRequest` nor `supportsDataBreakpoints`, so those tools
|
|
43
|
+
* would only ever return "unsupported" here.
|
|
44
|
+
*/
|
|
45
|
+
export function registerCsDapTools(server, dap, cfg) {
|
|
46
|
+
server.registerTool("cs_dbg_launch", {
|
|
47
|
+
title: "Launch C# debug session",
|
|
48
|
+
description: "Start a C# Godot game under netcoredbg. `program` defaults to the configured Mono/.NET Godot binary " +
|
|
49
|
+
"(GODOT_CSHARP_BIN) and `args` to ['--path', <C# project>]; override either to debug a different .NET program. " +
|
|
50
|
+
"Any breakpoints set beforehand are applied during the handshake. Requires netcoredbg (GODOT_CSDAP_CMD) — " +
|
|
51
|
+
"absent, the lazy spawn fails with an actionable hint rather than hanging.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
program: z.string().optional().describe("Path to the program to launch (default: the Mono/.NET Godot binary)"),
|
|
54
|
+
args: z.array(z.string()).optional().describe("Program arguments (default: ['--path', <C# project>])"),
|
|
55
|
+
stop_on_entry: z.boolean().optional().describe("Break at entry (default false)"),
|
|
56
|
+
just_my_code: z.boolean().optional().describe("Restrict stepping/breakpoints to user code (netcoredbg justMyCode; default true)"),
|
|
57
|
+
},
|
|
58
|
+
}, async ({ program, args, stop_on_entry, just_my_code }) => {
|
|
59
|
+
try {
|
|
60
|
+
await dap.start("launch", {
|
|
61
|
+
program: program ?? cfg.csDapProgram,
|
|
62
|
+
args: args ?? ["--path", cfg.csDapProjectPath],
|
|
63
|
+
cwd: cfg.csDapProjectPath,
|
|
64
|
+
stopAtEntry: stop_on_entry ?? false,
|
|
65
|
+
justMyCode: just_my_code ?? true,
|
|
66
|
+
});
|
|
67
|
+
return ok({ session_id: "csharp", state: dap.state });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return fail(err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
server.registerTool("cs_dbg_attach", {
|
|
74
|
+
title: "Attach C# debug session",
|
|
75
|
+
description: "Attach netcoredbg to an already-running .NET process (e.g. a C# Godot game launched separately) by its OS process id. " +
|
|
76
|
+
"Any breakpoints set beforehand are applied during the handshake.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
process_id: z.number().int().describe("OS process id of the running .NET process to attach to"),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ process_id }) => {
|
|
81
|
+
try {
|
|
82
|
+
await dap.start("attach", { processId: process_id });
|
|
83
|
+
return ok({ session_id: "csharp", state: dap.state });
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return fail(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("cs_dbg_set_breakpoints", {
|
|
90
|
+
title: "Set C# breakpoints",
|
|
91
|
+
description: "Set (replace) the breakpoints for a C# source file. Applied immediately if a session is running, else buffered until launch/attach. " +
|
|
92
|
+
"Feature-detected: the per-line `conditions` modifier is only sent when the connected adapter advertises supportsConditionalBreakpoints " +
|
|
93
|
+
"(netcoredbg does); on an adapter that advertises it unsupported the modifier is dropped and the result carries `unsupported_modifiers` " +
|
|
94
|
+
"plus a `warning`. Detection needs a live session, so set conditions after cs_dbg_launch/cs_dbg_attach.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
path: z.string().describe("C# script path (res://..., absolute, or relative to the C# project root)"),
|
|
97
|
+
lines: z.array(z.number().int().positive()).describe("1-based line numbers"),
|
|
98
|
+
conditions: z.array(z.string().nullable()).optional().describe("Optional per-line condition expressions (aligned to lines, null to skip); break only when the expression is true"),
|
|
99
|
+
},
|
|
100
|
+
}, async ({ path, lines, conditions }) => {
|
|
101
|
+
try {
|
|
102
|
+
const fsPath = toFsPath(path, cfg.csDapProjectPath);
|
|
103
|
+
// Feature-detect the condition modifier against the connected adapter. Only when it does
|
|
104
|
+
// not advertise supportsConditionalBreakpoints do we DROP conditions and warn — otherwise
|
|
105
|
+
// a "conditional" breakpoint could halt unconditionally on an adapter that ignores them.
|
|
106
|
+
const wantsCondition = Array.isArray(conditions) && conditions.some((c) => c != null && c !== "");
|
|
107
|
+
const conditionUnsupported = wantsCondition && dap.capabilities != null && dap.capabilities["supportsConditionalBreakpoints"] !== true;
|
|
108
|
+
const body = await dap.setBreakpoints(fsPath, lines, conditionUnsupported ? undefined : conditions);
|
|
109
|
+
const verified = Array.isArray(body["breakpoints"])
|
|
110
|
+
? body["breakpoints"].map((b) => ({ line: b.line ?? 0, verified: Boolean(b.verified) }))
|
|
111
|
+
: [];
|
|
112
|
+
const result = { path: fsPath, buffered: body["buffered"] === true, breakpoints: verified };
|
|
113
|
+
if (conditionUnsupported) {
|
|
114
|
+
result.unsupported_modifiers = ["condition"];
|
|
115
|
+
result.warning =
|
|
116
|
+
"The connected C# debug adapter does not advertise supportsConditionalBreakpoints, so the per-line conditions were " +
|
|
117
|
+
"dropped — the affected breakpoint(s) will halt unconditionally.";
|
|
118
|
+
}
|
|
119
|
+
return ok(result);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return fail(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
server.registerTool("cs_dbg_continue", {
|
|
126
|
+
title: "Continue (C#)",
|
|
127
|
+
description: "Resume execution and wait for the program to settle again (next breakpoint or termination). " +
|
|
128
|
+
"Returns the resulting state; if it runs on with no further breakpoint, reports state 'running'.",
|
|
129
|
+
inputSchema: {},
|
|
130
|
+
}, async () => {
|
|
131
|
+
try {
|
|
132
|
+
const r = await dap.resume("continue", { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
133
|
+
return ok({ state: r.state, stopped_reason: r.reason });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return fail(err);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
server.registerTool("cs_dbg_step", {
|
|
140
|
+
title: "Step (C#)",
|
|
141
|
+
description: "Step execution: 'over' (next), 'in' (stepIn), or 'out' (stepOut), then wait for the step to land. " +
|
|
142
|
+
"Returns the resulting state and stop reason.",
|
|
143
|
+
inputSchema: { kind: z.enum(["in", "over", "out"]).describe("Step kind") },
|
|
144
|
+
}, async ({ kind }) => {
|
|
145
|
+
try {
|
|
146
|
+
const command = kind === "in" ? "stepIn" : kind === "out" ? "stepOut" : "next";
|
|
147
|
+
const r = await dap.resume(command, { threadId: dap.threadId() }, RESUME_WAIT_MS);
|
|
148
|
+
return ok({ state: r.state, stopped_reason: r.reason });
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return fail(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.registerTool("cs_dbg_stack_trace", {
|
|
155
|
+
title: "Stack trace (C#)",
|
|
156
|
+
description: "Return the current C# call stack (only meaningful while stopped at a breakpoint).",
|
|
157
|
+
inputSchema: { levels: z.number().int().positive().optional().describe("Max frames (default 20)") },
|
|
158
|
+
}, async ({ levels }) => {
|
|
159
|
+
try {
|
|
160
|
+
const body = await dap.request("stackTrace", { threadId: dap.threadId(), startFrame: 0, levels: levels ?? 20 });
|
|
161
|
+
const frames = Array.isArray(body["stackFrames"])
|
|
162
|
+
? body["stackFrames"].map((f) => ({
|
|
163
|
+
id: f.id ?? 0, name: f.name ?? "", source: f.source?.path ?? f.source?.name ?? "", line: f.line ?? 0,
|
|
164
|
+
}))
|
|
165
|
+
: [];
|
|
166
|
+
return ok({ frames });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return fail(err);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
server.registerTool("cs_dbg_scopes", {
|
|
173
|
+
title: "Scopes (C#)",
|
|
174
|
+
description: "Return the variable scopes (Locals, etc.) for a C# stack frame.",
|
|
175
|
+
inputSchema: { frame_id: z.number().int().describe("Frame id from cs_dbg_stack_trace") },
|
|
176
|
+
}, async ({ frame_id }) => {
|
|
177
|
+
try {
|
|
178
|
+
const body = await dap.request("scopes", { frameId: frame_id });
|
|
179
|
+
const scopes = Array.isArray(body["scopes"])
|
|
180
|
+
? body["scopes"].map((s) => ({ name: s.name ?? "", variables_ref: s.variablesReference ?? 0 }))
|
|
181
|
+
: [];
|
|
182
|
+
return ok({ scopes });
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return fail(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
server.registerTool("cs_dbg_variables", {
|
|
189
|
+
title: "Variables (C#)",
|
|
190
|
+
description: "List variables under a scope or a complex value (via its variables_ref).",
|
|
191
|
+
inputSchema: { variables_ref: z.number().int().describe("variablesReference from cs_dbg_scopes or a parent variable") },
|
|
192
|
+
}, async ({ variables_ref }) => {
|
|
193
|
+
try {
|
|
194
|
+
const body = await dap.request("variables", { variablesReference: variables_ref });
|
|
195
|
+
const variables = Array.isArray(body["variables"])
|
|
196
|
+
? body["variables"].map((v) => ({
|
|
197
|
+
name: v.name ?? "", value: v.value ?? "", type: v.type ?? "", variables_ref: v.variablesReference ?? 0,
|
|
198
|
+
}))
|
|
199
|
+
: [];
|
|
200
|
+
return ok({ variables });
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return fail(err);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
server.registerTool("cs_dbg_evaluate", {
|
|
207
|
+
title: "Evaluate C# expression",
|
|
208
|
+
description: "Evaluate a C# expression in the context of a stopped frame. DESTRUCTIVE: arbitrary code execution — confirm with the user and keep this gated. " +
|
|
209
|
+
"Bounded by a short deadline so a non-answering adapter fails fast rather than hanging the full DAP timeout.",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
expression: z.string().describe("C# expression to evaluate"),
|
|
212
|
+
frame_id: z.number().int().optional().describe("Frame id (from cs_dbg_stack_trace); omit for the top frame"),
|
|
213
|
+
confirm: z.boolean().optional().describe("Auto-approve this arbitrary-code evaluation (skip the confirmation prompt)"),
|
|
214
|
+
},
|
|
215
|
+
}, async ({ expression, frame_id, confirm }) => {
|
|
216
|
+
try {
|
|
217
|
+
const blocked = await gate(server, confirm, `Evaluate C# expression in the running game: ${expression}`);
|
|
218
|
+
if (blocked)
|
|
219
|
+
return blocked;
|
|
220
|
+
let body;
|
|
221
|
+
try {
|
|
222
|
+
body = await dap.request("evaluate", { expression, frameId: frame_id, context: "repl" }, cfg.csDapEvaluateTimeoutMs);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
if (isDapTimeout(err)) {
|
|
226
|
+
return {
|
|
227
|
+
isError: true,
|
|
228
|
+
content: [{ type: "text", text: `The C# debug adapter did not answer the evaluate request within ${cfg.csDapEvaluateTimeoutMs}ms — no result was returned. The debug session is still alive; use cs_dbg_variables to inspect state.` }],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
return ok({ result: String(body["result"] ?? ""), type: String(body["type"] ?? ""), variables_ref: body["variablesReference"] ?? 0 });
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
return fail(err);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
server.registerTool("cs_dbg_set_variable", {
|
|
240
|
+
title: "Set C# variable value",
|
|
241
|
+
description: "Change a variable's value in a stopped C# frame (DAP setVariable). DESTRUCTIVE: mutates live program state — confirm with the user and keep this gated. " +
|
|
242
|
+
"`variables_ref` is the container's variablesReference (from cs_dbg_scopes, or a complex cs_dbg_variables entry), `name` is the variable within it, " +
|
|
243
|
+
"`value` is the new value as a C# literal/expression. Feature-detected: on an adapter that advertises supportsSetVariable:false it returns a clear " +
|
|
244
|
+
"\"unsupported\" message WITHOUT prompting; otherwise a bounded deadline turns a non-answering adapter into a clear message rather than a hang.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
variables_ref: z.number().int().describe("variablesReference of the containing scope/variable (from cs_dbg_scopes or cs_dbg_variables)"),
|
|
247
|
+
name: z.string().describe("Variable name within that container"),
|
|
248
|
+
value: z.string().describe("New value as a C# literal/expression"),
|
|
249
|
+
confirm: z.boolean().optional().describe("Auto-approve this mutation (skip the confirmation prompt)"),
|
|
250
|
+
},
|
|
251
|
+
}, async ({ variables_ref, name, value, confirm }) => {
|
|
252
|
+
try {
|
|
253
|
+
if (dap.capabilities && dap.capabilities["supportsSetVariable"] === false) {
|
|
254
|
+
return {
|
|
255
|
+
isError: true,
|
|
256
|
+
content: [{ type: "text", text: "cs_dbg_set_variable is unsupported by the connected C# debug adapter (it does not advertise supportsSetVariable). Read-only inspection (cs_dbg_variables) still works." }],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const blocked = await gate(server, confirm, `Set C# variable ${name} = ${value} in the running game`);
|
|
260
|
+
if (blocked)
|
|
261
|
+
return blocked;
|
|
262
|
+
let body;
|
|
263
|
+
try {
|
|
264
|
+
body = await dap.request("setVariable", { variablesReference: variables_ref, name, value }, cfg.csDapSetVarTimeoutMs);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
if (isDapTimeout(err)) {
|
|
268
|
+
return {
|
|
269
|
+
isError: true,
|
|
270
|
+
content: [{ type: "text", text: `The C# debug adapter did not answer the setVariable request within ${cfg.csDapSetVarTimeoutMs}ms — no change was made; the variable is unchanged. Read-only inspection (cs_dbg_variables) still works.` }],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
return ok({ name, value: String(body["value"] ?? value), type: String(body["type"] ?? ""), variables_ref: body["variablesReference"] ?? 0 });
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
return fail(err);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
server.registerTool("cs_dbg_watch", {
|
|
282
|
+
title: "Watch expressions (C#)",
|
|
283
|
+
description: "Manage a persistent set of C# watch expressions and evaluate them in the current stopped frame. " +
|
|
284
|
+
"Pass `add`/`remove`/`clear` to mutate the set (all optional), then every current watch is re-evaluated " +
|
|
285
|
+
"and returned. Call with no mutation args to just re-read the watches after a step/continue. Expressions " +
|
|
286
|
+
"are evaluated in DAP `watch` context (intended to be side-effect-free), so this is not gated; the results " +
|
|
287
|
+
"are only meaningful while the program is stopped at a breakpoint.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
add: z.array(z.string()).optional().describe("Expressions to add to the watch set"),
|
|
290
|
+
remove: z.array(z.string()).optional().describe("Expressions to remove from the watch set"),
|
|
291
|
+
clear: z.boolean().optional().describe("Clear all watches before applying add (default false)"),
|
|
292
|
+
frame_id: z.number().int().optional().describe("Frame id from cs_dbg_stack_trace; omit for the top frame"),
|
|
293
|
+
},
|
|
294
|
+
}, async ({ add, remove, clear, frame_id }) => {
|
|
295
|
+
try {
|
|
296
|
+
if (clear)
|
|
297
|
+
dap.clearWatches();
|
|
298
|
+
if (remove && remove.length)
|
|
299
|
+
dap.removeWatches(remove);
|
|
300
|
+
if (add && add.length)
|
|
301
|
+
dap.addWatches(add);
|
|
302
|
+
// Bound each watch's evaluate to the short deadline (mirrors cs_dbg_evaluate): a watch
|
|
303
|
+
// the adapter never answers fails fast on that entry instead of hanging the full DAP
|
|
304
|
+
// timeout at every stop, while other watches still resolve normally.
|
|
305
|
+
const watches = await dap.evaluateWatches(frame_id, cfg.csDapEvaluateTimeoutMs);
|
|
306
|
+
return ok({ watches });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
return fail(err);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
server.registerTool("cs_dbg_set_exception_breakpoints", {
|
|
313
|
+
title: "Set C# exception breakpoints",
|
|
314
|
+
description: "Enable (replace) the debugger's exception breakpoint filters so execution halts when a matching .NET exception is thrown " +
|
|
315
|
+
"(DAP setExceptionBreakpoints). Pass the filter IDs to enable; call with no filters (or []) to clear them. The result echoes the " +
|
|
316
|
+
"active filters and lists `available_filters` — the exception filters the connected adapter advertises (netcoredbg exposes `all` " +
|
|
317
|
+
"for every thrown exception and `user-unhandled`). Requires a running debug session. Not gated (it only configures the debugger). " +
|
|
318
|
+
"Feature-detected: on an adapter that advertises no exceptionBreakpointFilters it returns a clear \"unsupported\" message WITHOUT sending anything.",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
filters: z.array(z.string()).optional().describe("Exception filter IDs to enable (default none = clear). Choose from available_filters in the result (netcoredbg: 'all', 'user-unhandled')."),
|
|
321
|
+
},
|
|
322
|
+
}, async ({ filters }) => {
|
|
323
|
+
try {
|
|
324
|
+
// Per the DAP spec a client should only send setExceptionBreakpoints when the adapter
|
|
325
|
+
// advertised at least one exception filter. Short-circuit with a clear "unsupported"
|
|
326
|
+
// message otherwise, matching the GDScript dbg_set_exception_breakpoints discipline.
|
|
327
|
+
const advertised = dap.capabilities?.["exceptionBreakpointFilters"];
|
|
328
|
+
const available_filters = Array.isArray(advertised)
|
|
329
|
+
? advertised.map((f) => ({ filter: f.filter ?? "", label: f.label ?? "" }))
|
|
330
|
+
: [];
|
|
331
|
+
if (available_filters.length === 0) {
|
|
332
|
+
return {
|
|
333
|
+
isError: true,
|
|
334
|
+
content: [{ type: "text", text: "cs_dbg_set_exception_breakpoints is unsupported by the connected C# debug adapter (it advertises no exceptionBreakpointFilters). There are no exception filters to enable." }],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const active = filters ?? [];
|
|
338
|
+
const body = await dap.request("setExceptionBreakpoints", { filters: active });
|
|
339
|
+
const breakpoints = Array.isArray(body["breakpoints"])
|
|
340
|
+
? body["breakpoints"].map((b) => ({ verified: Boolean(b.verified) }))
|
|
341
|
+
: [];
|
|
342
|
+
return ok({ filters: active, available_filters, breakpoints });
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
return fail(err);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
server.registerTool("cs_dbg_restart", {
|
|
349
|
+
title: "Restart C# debug session",
|
|
350
|
+
description: "Restart the current C# debug session. Uses the DAP `restart` request when the adapter advertises `supportsRestartRequest`, " +
|
|
351
|
+
"otherwise falls back to terminate + relaunch — so it works on every adapter (netcoredbg advertises none, so the relaunch path runs). " +
|
|
352
|
+
"Reuses the last cs_dbg_launch/cs_dbg_attach parameters; pass `stop_on_entry` / `program` / `args` to override them for a launched " +
|
|
353
|
+
"session. `method` in the result reports which path ran ('restart' = native DAP restart, 'relaunch' = terminate + fresh handshake). " +
|
|
354
|
+
"Requires a session started with cs_dbg_launch/cs_dbg_attach.",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
stop_on_entry: z.boolean().optional().describe("Override stop-at-entry for the restart (launched sessions)"),
|
|
357
|
+
program: z.string().optional().describe("Override the launched program (launched sessions)"),
|
|
358
|
+
args: z.array(z.string()).optional().describe("Override the program arguments (launched sessions)"),
|
|
359
|
+
},
|
|
360
|
+
}, async ({ stop_on_entry, program, args }) => {
|
|
361
|
+
try {
|
|
362
|
+
const override = {};
|
|
363
|
+
if (stop_on_entry !== undefined)
|
|
364
|
+
override.stopAtEntry = stop_on_entry;
|
|
365
|
+
if (program !== undefined)
|
|
366
|
+
override.program = program;
|
|
367
|
+
if (args !== undefined)
|
|
368
|
+
override.args = args;
|
|
369
|
+
const r = await dap.restart(override);
|
|
370
|
+
return ok({ session_id: "csharp", method: r.method, state: r.state });
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
return fail(err);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=csdap.js.map
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { toFileUri, toFsPath, readFileText } from "../paths.js";
|
|
4
|
+
import { gate } from "../confirm.js";
|
|
5
|
+
import { COMPLETION_KIND, SYMBOL_KIND, ok, fail, markupToString, isMethodNotFound, normalizeLocations, applyTextEdits, normalizeWorkspaceEdit, } from "./lsp-common.js";
|
|
6
|
+
/**
|
|
7
|
+
* D4 C2 — the C#/.NET semantic plane. Read-only `cs_*` tools mirroring the proven
|
|
8
|
+
* read-only `gd_*` LSP surface, but driven by OmniSharp (spawned over stdio by the
|
|
9
|
+
* host) against a C# Godot project instead of Godot's built-in GDScript server.
|
|
10
|
+
*
|
|
11
|
+
* The tools are feature-detected the same way the `gd_*` tools are: a capability
|
|
12
|
+
* the server never advertised, or a `-32601 Method not found` from a server that
|
|
13
|
+
* lied about advertising it, yields a clear "unsupported" message rather than a
|
|
14
|
+
* raw JSON-RPC error or a hang. The two mutators — `cs_rename` (elicitation-gated
|
|
15
|
+
* on `apply=true`) and the read-only `cs_code_action` listing — mirror the GDScript
|
|
16
|
+
* `gd_rename` / `gd_code_action`.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Returned when the connected C# language server doesn't implement an optional
|
|
20
|
+
* LSP method. Same graceful-degradation contract as the GDScript plane, worded
|
|
21
|
+
* for OmniSharp (which — unlike Godot's GDScript server — is expected to support
|
|
22
|
+
* workspace/symbol; this only fires on a server that genuinely lacks it).
|
|
23
|
+
*/
|
|
24
|
+
function unsupportedCsLsp(tool, method, capability, alt) {
|
|
25
|
+
return {
|
|
26
|
+
isError: true,
|
|
27
|
+
content: [{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: `${tool} is unsupported by the connected C# language server: it does not implement ` +
|
|
30
|
+
`LSP '${method}' (advertises no ${capability}, or replies -32601 Method not found). ` +
|
|
31
|
+
`This is a language-server limitation, not a host error. ${alt}`,
|
|
32
|
+
}],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function registerCsLspTools(server, cslsp, cfg) {
|
|
36
|
+
const root = cfg.csLspProjectPath;
|
|
37
|
+
const openAndPos = async (path) => {
|
|
38
|
+
const uri = toFileUri(path, root);
|
|
39
|
+
await cslsp.ensureOpen(uri, readFileText(toFsPath(path, root)));
|
|
40
|
+
return uri;
|
|
41
|
+
};
|
|
42
|
+
const posSchema = {
|
|
43
|
+
path: z.string().describe("C# script path (res://..., absolute, or relative to the C# project root)"),
|
|
44
|
+
line: z.number().int().describe("0-based line"),
|
|
45
|
+
character: z.number().int().describe("0-based character"),
|
|
46
|
+
};
|
|
47
|
+
server.registerTool("cs_completion", { title: "C# completion", description: "Type-aware C# code completion at a position via OmniSharp.", inputSchema: posSchema }, async ({ path, line, character }) => {
|
|
48
|
+
try {
|
|
49
|
+
const uri = await openAndPos(path);
|
|
50
|
+
const result = await cslsp.request("textDocument/completion", { textDocument: { uri }, position: { line, character } });
|
|
51
|
+
const raw = Array.isArray(result) ? result : (result?.items ?? []);
|
|
52
|
+
const items = raw.map((i) => {
|
|
53
|
+
const it = i;
|
|
54
|
+
return { label: it.label ?? "", kind: it.kind ? COMPLETION_KIND[it.kind] ?? String(it.kind) : "", detail: it.detail ?? "", insertText: it.insertText ?? it.label ?? "" };
|
|
55
|
+
});
|
|
56
|
+
return ok({ items });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return fail(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
server.registerTool("cs_hover", { title: "C# hover", description: "Hover documentation/type info at a position (e.g. the `Counter : int` type).", inputSchema: posSchema }, async ({ path, line, character }) => {
|
|
63
|
+
try {
|
|
64
|
+
const uri = await openAndPos(path);
|
|
65
|
+
const result = (await cslsp.request("textDocument/hover", { textDocument: { uri }, position: { line, character } }));
|
|
66
|
+
return ok({ contents: markupToString(result?.contents) });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return fail(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
server.registerTool("cs_definition", { title: "C# go-to-definition", description: "Resolve the definition location(s) of the C# symbol at a position.", inputSchema: posSchema }, async ({ path, line, character }) => {
|
|
73
|
+
try {
|
|
74
|
+
const uri = await openAndPos(path);
|
|
75
|
+
const result = await cslsp.request("textDocument/definition", { textDocument: { uri }, position: { line, character } });
|
|
76
|
+
return ok({ locations: normalizeLocations(result) });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
return fail(err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
server.registerTool("cs_references", {
|
|
83
|
+
title: "C# find-references",
|
|
84
|
+
description: "Find all references to the C# symbol at a position.",
|
|
85
|
+
inputSchema: { ...posSchema, include_declaration: z.boolean().optional().describe("Include the declaration (default true)") },
|
|
86
|
+
}, async ({ path, line, character, include_declaration }) => {
|
|
87
|
+
try {
|
|
88
|
+
const uri = await openAndPos(path);
|
|
89
|
+
const result = await cslsp.request("textDocument/references", {
|
|
90
|
+
textDocument: { uri }, position: { line, character },
|
|
91
|
+
context: { includeDeclaration: include_declaration ?? true },
|
|
92
|
+
});
|
|
93
|
+
return ok({ locations: normalizeLocations(result) });
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return fail(err);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("cs_rename", {
|
|
100
|
+
title: "C# rename symbol",
|
|
101
|
+
description: "Rename a C# symbol project-wide via OmniSharp. Returns the planned edit; pass apply=true to WRITE the changes to disk (DESTRUCTIVE — confirm with the user).",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
...posSchema,
|
|
104
|
+
new_name: z.string().describe("New symbol name"),
|
|
105
|
+
apply: z.boolean().optional().describe("Write edits to disk (default false = dry run)"),
|
|
106
|
+
confirm: z.boolean().optional().describe("Auto-approve writing edits (skip the confirmation prompt); only relevant with apply=true"),
|
|
107
|
+
},
|
|
108
|
+
}, async ({ path, line, character, new_name, apply, confirm }) => {
|
|
109
|
+
try {
|
|
110
|
+
const uri = await openAndPos(path);
|
|
111
|
+
const edit = await cslsp.request("textDocument/rename", {
|
|
112
|
+
textDocument: { uri }, position: { line, character }, newName: new_name,
|
|
113
|
+
});
|
|
114
|
+
// OmniSharp returns a WorkspaceEdit as `documentChanges` (versioned
|
|
115
|
+
// TextDocumentEdit[]); normalizeWorkspaceEdit also accepts the legacy
|
|
116
|
+
// `changes` map, so cs_rename handles either encoding.
|
|
117
|
+
const changes = normalizeWorkspaceEdit(edit);
|
|
118
|
+
const files = Object.keys(changes);
|
|
119
|
+
let editCount = 0;
|
|
120
|
+
for (const f of files)
|
|
121
|
+
editCount += changes[f].length;
|
|
122
|
+
let written = [];
|
|
123
|
+
if (apply) {
|
|
124
|
+
const blocked = await gate(server, confirm, `Rename to "${new_name}" — write ${editCount} edit(s) across ${files.length} file(s)`);
|
|
125
|
+
if (blocked)
|
|
126
|
+
return blocked;
|
|
127
|
+
for (const fileUri of files) {
|
|
128
|
+
const fsPath = decodeURIComponent(fileUri.replace(/^file:\/\//, ""));
|
|
129
|
+
const before = fs.readFileSync(fsPath, "utf8");
|
|
130
|
+
fs.writeFileSync(fsPath, applyTextEdits(before, changes[fileUri]), "utf8");
|
|
131
|
+
written.push(fsPath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return ok({ changed_files: files, edit_count: editCount, applied: Boolean(apply), written });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return fail(err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
server.registerTool("cs_document_symbols", {
|
|
141
|
+
title: "C# document symbols",
|
|
142
|
+
description: "List the symbols (classes, methods, properties, fields) declared in a C# script.",
|
|
143
|
+
inputSchema: { path: z.string().describe("C# script path") },
|
|
144
|
+
}, async ({ path }) => {
|
|
145
|
+
try {
|
|
146
|
+
const uri = await openAndPos(path);
|
|
147
|
+
const result = (await cslsp.request("textDocument/documentSymbol", { textDocument: { uri } }));
|
|
148
|
+
const symbols = (result ?? []).map((s) => {
|
|
149
|
+
const sym = s;
|
|
150
|
+
const range = sym.range ?? sym.selectionRange ?? sym.location?.range ?? {};
|
|
151
|
+
return { name: sym.name ?? "", kind: sym.kind ? SYMBOL_KIND[sym.kind] ?? String(sym.kind) : "", line: range.start?.line ?? 0 };
|
|
152
|
+
});
|
|
153
|
+
return ok({ symbols });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
return fail(err);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
server.registerTool("cs_workspace_symbols", {
|
|
160
|
+
title: "C# workspace symbols",
|
|
161
|
+
description: "Search C# symbols across the whole project by name. Unlike Godot's GDScript server, " +
|
|
162
|
+
"OmniSharp implements LSP 'workspace/symbol', so this returns real results; it stays " +
|
|
163
|
+
"feature-detected (with a -32601 belt-and-suspenders) so a server that lacks it degrades " +
|
|
164
|
+
"gracefully rather than erroring opaquely.",
|
|
165
|
+
inputSchema: { query: z.string().describe("Symbol name query") },
|
|
166
|
+
}, async ({ query }) => {
|
|
167
|
+
const alt = "Use cs_document_symbols for a single file's symbols, or cs_definition / cs_references to navigate by a known name.";
|
|
168
|
+
try {
|
|
169
|
+
const caps = await cslsp.getServerCapabilities();
|
|
170
|
+
if (!caps.workspaceSymbolProvider)
|
|
171
|
+
return unsupportedCsLsp("cs_workspace_symbols", "workspace/symbol", "workspaceSymbolProvider", alt);
|
|
172
|
+
const result = (await cslsp.request("workspace/symbol", { query }));
|
|
173
|
+
const symbols = (result ?? []).map((s) => {
|
|
174
|
+
const sym = s;
|
|
175
|
+
return {
|
|
176
|
+
name: sym.name ?? "",
|
|
177
|
+
kind: sym.kind ? SYMBOL_KIND[sym.kind] ?? String(sym.kind) : "",
|
|
178
|
+
uri: sym.location?.uri ?? "",
|
|
179
|
+
line: sym.location?.range?.start?.line ?? 0,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
return ok({ symbols });
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (isMethodNotFound(err))
|
|
186
|
+
return unsupportedCsLsp("cs_workspace_symbols", "workspace/symbol", "workspaceSymbolProvider", alt);
|
|
187
|
+
return fail(err);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
server.registerTool("cs_signature_help", {
|
|
191
|
+
title: "C# signature help",
|
|
192
|
+
description: "Show the call signature(s) and active parameter at a position (the parameter hints an IDE pops up inside a call).",
|
|
193
|
+
inputSchema: posSchema,
|
|
194
|
+
}, async ({ path, line, character }) => {
|
|
195
|
+
try {
|
|
196
|
+
const uri = await openAndPos(path);
|
|
197
|
+
const result = (await cslsp.request("textDocument/signatureHelp", { textDocument: { uri }, position: { line, character } }));
|
|
198
|
+
const signatures = (result?.signatures ?? []).map((s) => {
|
|
199
|
+
const sig = s;
|
|
200
|
+
const sigLabel = sig.label ?? "";
|
|
201
|
+
const parameters = (sig.parameters ?? []).map((p) => {
|
|
202
|
+
const par = p;
|
|
203
|
+
let plabel = "";
|
|
204
|
+
if (typeof par.label === "string")
|
|
205
|
+
plabel = par.label;
|
|
206
|
+
// Per LSP, a parameter label may be a [start,end] offset pair into the signature label.
|
|
207
|
+
else if (Array.isArray(par.label) && par.label.length === 2)
|
|
208
|
+
plabel = sigLabel.slice(Number(par.label[0]), Number(par.label[1]));
|
|
209
|
+
return { label: plabel, documentation: markupToString(par.documentation) };
|
|
210
|
+
});
|
|
211
|
+
return { label: sigLabel, documentation: markupToString(sig.documentation), parameters };
|
|
212
|
+
});
|
|
213
|
+
return ok({ signatures, active_signature: result?.activeSignature ?? 0, active_parameter: result?.activeParameter ?? 0 });
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return fail(err);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
server.registerTool("cs_diagnostics", {
|
|
220
|
+
title: "C# diagnostics",
|
|
221
|
+
description: "Return compile/analyzer diagnostics (errors, warnings) for a C# script. Opens the file and waits briefly for OmniSharp to publish.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
path: z.string().describe("C# script path"),
|
|
224
|
+
wait_ms: z.number().int().positive().optional().describe("Max time to wait for the first publish (default 2000; OmniSharp's first analysis can be slow)"),
|
|
225
|
+
},
|
|
226
|
+
}, async ({ path, wait_ms }) => {
|
|
227
|
+
try {
|
|
228
|
+
const uri = await openAndPos(path);
|
|
229
|
+
const diagnostics = await cslsp.waitForDiagnostics(uri, wait_ms ?? 2000);
|
|
230
|
+
const named = diagnostics.map((d) => ({
|
|
231
|
+
severity: (["", "error", "warning", "info", "hint"][d.severity] ?? "error"),
|
|
232
|
+
message: d.message, line: d.line, character: d.character,
|
|
233
|
+
}));
|
|
234
|
+
return ok({ uri, diagnostics: named });
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
return fail(err);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
server.registerTool("cs_code_action", {
|
|
241
|
+
title: "C# code actions",
|
|
242
|
+
description: "List the code actions (quick fixes / refactors) OmniSharp offers for a range — the lightbulb menu. " +
|
|
243
|
+
"Read-only: returns the available actions (title, kind, whether each carries a WorkspaceEdit or a Command) without applying any. " +
|
|
244
|
+
"Unlike Godot's GDScript server, OmniSharp implements code actions, so this returns real results; it stays " +
|
|
245
|
+
"feature-detected (with a -32601 belt-and-suspenders) so a server that lacks it degrades gracefully. " +
|
|
246
|
+
"end_line/end_character default to the start position (a caret, not a selection).",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
path: z.string().describe("C# script path (res://..., absolute, or relative to the C# project root)"),
|
|
249
|
+
start_line: z.number().int().describe("0-based start line"),
|
|
250
|
+
start_character: z.number().int().describe("0-based start character"),
|
|
251
|
+
end_line: z.number().int().optional().describe("0-based end line (default = start_line)"),
|
|
252
|
+
end_character: z.number().int().optional().describe("0-based end character (default = start_character)"),
|
|
253
|
+
only: z.array(z.string()).optional().describe("Restrict to these CodeActionKind prefixes, e.g. 'quickfix', 'refactor'"),
|
|
254
|
+
},
|
|
255
|
+
}, async ({ path, start_line, start_character, end_line, end_character, only }) => {
|
|
256
|
+
const alt = "Use cs_diagnostics to surface issues and cs_completion / cs_rename to make edits.";
|
|
257
|
+
try {
|
|
258
|
+
const caps = await cslsp.getServerCapabilities();
|
|
259
|
+
if (!caps.codeActionProvider)
|
|
260
|
+
return unsupportedCsLsp("cs_code_action", "textDocument/codeAction", "codeActionProvider", alt);
|
|
261
|
+
const uri = await openAndPos(path);
|
|
262
|
+
const range = {
|
|
263
|
+
start: { line: start_line, character: start_character },
|
|
264
|
+
end: { line: end_line ?? start_line, character: end_character ?? start_character },
|
|
265
|
+
};
|
|
266
|
+
const context = { diagnostics: [] };
|
|
267
|
+
if (only && only.length)
|
|
268
|
+
context.only = only;
|
|
269
|
+
const result = (await cslsp.request("textDocument/codeAction", { textDocument: { uri }, range, context }));
|
|
270
|
+
const actions = (result ?? []).map((a) => {
|
|
271
|
+
const act = a;
|
|
272
|
+
// A bare Command has `command` as a string; a CodeAction nests a Command object under `command`.
|
|
273
|
+
const command = typeof act.command === "string" ? act.command : act.command?.command ?? null;
|
|
274
|
+
return { title: act.title ?? "", kind: act.kind ?? "", has_edit: act.edit !== undefined, command };
|
|
275
|
+
});
|
|
276
|
+
return ok({ actions });
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (isMethodNotFound(err))
|
|
280
|
+
return unsupportedCsLsp("cs_code_action", "textDocument/codeAction", "codeActionProvider", alt);
|
|
281
|
+
return fail(err);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=cslsp.js.map
|