dap-mcp-server 0.1.5

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.
Files changed (65) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/.forgejo/workflows/publish.yml +63 -0
  3. package/CLAUDE.md +71 -0
  4. package/LICENSE +21 -0
  5. package/README.md +105 -0
  6. package/SKILL.md +62 -0
  7. package/build/adapters/builtin.d.ts +24 -0
  8. package/build/adapters/builtin.js +46 -0
  9. package/build/adapters/builtin.js.map +1 -0
  10. package/build/adapters/detect.d.ts +23 -0
  11. package/build/adapters/detect.js +36 -0
  12. package/build/adapters/detect.js.map +1 -0
  13. package/build/adapters/registry.d.ts +39 -0
  14. package/build/adapters/registry.js +79 -0
  15. package/build/adapters/registry.js.map +1 -0
  16. package/build/dap/dap-client.d.ts +46 -0
  17. package/build/dap/dap-client.js +190 -0
  18. package/build/dap/dap-client.js.map +1 -0
  19. package/build/dap/protocol.d.ts +32 -0
  20. package/build/dap/protocol.js +71 -0
  21. package/build/dap/protocol.js.map +1 -0
  22. package/build/dap/ring-buffer.d.ts +35 -0
  23. package/build/dap/ring-buffer.js +57 -0
  24. package/build/dap/ring-buffer.js.map +1 -0
  25. package/build/dap/types.d.ts +87 -0
  26. package/build/dap/types.js +25 -0
  27. package/build/dap/types.js.map +1 -0
  28. package/build/index.d.ts +24 -0
  29. package/build/index.js +30 -0
  30. package/build/index.js.map +1 -0
  31. package/build/server.d.ts +24 -0
  32. package/build/server.js +735 -0
  33. package/build/server.js.map +1 -0
  34. package/build/session/session-manager.d.ts +32 -0
  35. package/build/session/session-manager.js +60 -0
  36. package/build/session/session-manager.js.map +1 -0
  37. package/build/session/session.d.ts +85 -0
  38. package/build/session/session.js +374 -0
  39. package/build/session/session.js.map +1 -0
  40. package/docs/plans/2026-03-05-dap-wrapper-mcp-design.md +326 -0
  41. package/docs/plans/2026-03-05-dap-wrapper-mcp-implementation.md +2815 -0
  42. package/package.json +43 -0
  43. package/src/adapters/builtin.ts +47 -0
  44. package/src/adapters/detect.test.ts +36 -0
  45. package/src/adapters/detect.ts +36 -0
  46. package/src/adapters/registry.test.ts +73 -0
  47. package/src/adapters/registry.ts +86 -0
  48. package/src/dap/dap-client.test.ts +87 -0
  49. package/src/dap/dap-client.ts +216 -0
  50. package/src/dap/protocol.test.ts +82 -0
  51. package/src/dap/protocol.ts +78 -0
  52. package/src/dap/ring-buffer.test.ts +82 -0
  53. package/src/dap/ring-buffer.ts +64 -0
  54. package/src/dap/types.ts +92 -0
  55. package/src/index.ts +30 -0
  56. package/src/integration/debug-lifecycle.test.ts +107 -0
  57. package/src/server.ts +866 -0
  58. package/src/session/session-manager.test.ts +74 -0
  59. package/src/session/session-manager.ts +67 -0
  60. package/src/session/session.test.ts +84 -0
  61. package/src/session/session.ts +435 -0
  62. package/src/test-fixtures/crash-adapter.js +23 -0
  63. package/src/test-fixtures/mock-adapter.js +142 -0
  64. package/src/test-fixtures/silent-adapter.js +24 -0
  65. package/tsconfig.json +16 -0
package/src/server.ts ADDED
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Copyright (c) 2026 Ivan Iraci <ivan.iraci@professioneit.com>
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ import { z } from "zod";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+
28
+ import { AdapterRegistry } from "./adapters/registry.js";
29
+ import { SessionManager } from "./session/session-manager.js";
30
+ import { DebugSession } from "./session/session.js";
31
+ import { checkAdapterAvailable } from "./adapters/detect.js";
32
+
33
+ export function createServer(): McpServer {
34
+ const globalDir = path.join(os.homedir(), ".dap-mcp");
35
+ const projectDir = path.join(process.cwd(), ".dap-mcp");
36
+
37
+ const registry = new AdapterRegistry(globalDir, projectDir);
38
+ const sessions = new SessionManager();
39
+
40
+ // Load persisted adapter configs (best-effort, don't block server start)
41
+ registry.load().catch((err: unknown) => {
42
+ console.error("Warning: failed to load adapter configs:", err);
43
+ });
44
+
45
+ const server = new McpServer(
46
+ {
47
+ name: "dap-mcp-server",
48
+ version: "0.1.0",
49
+ },
50
+ {
51
+ instructions: [
52
+ "This server provides AI-driven debugging via the Debug Adapter Protocol (DAP).",
53
+ "",
54
+ "## Debugging Workflow",
55
+ "",
56
+ "A typical session follows this sequence:",
57
+ "1. `list_adapters` — discover available debug adapters and check which are installed",
58
+ "2. `debug_launch` (or `debug_attach`) — start a debug session. This performs the full DAP handshake,",
59
+ " sets initial breakpoints, and launches the program. Returns a sessionId and initial stopped state.",
60
+ "3. Inspect state with `get_state` — returns threads, call stack, scopes, and variables in one call.",
61
+ " This is a coarse-grained tool designed to minimize round-trips; prefer it over multiple fine-grained queries.",
62
+ "4. `step` — control execution (continue, next, stepIn, stepOut, pause). Each step action blocks until",
63
+ " the program stops again and returns the new state, so you do NOT need to call get_state after stepping.",
64
+ "5. `evaluate` — evaluate expressions in the context of a stopped frame (REPL, watch, or hover context).",
65
+ "6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints during the session.",
66
+ "7. `get_output` — retrieve buffered stdout/stderr/console output (ring buffer, max 10K lines).",
67
+ "8. `debug_disconnect` — end the session. `debug_restart` replays the same launch config with preserved breakpoints.",
68
+ "",
69
+ "## Key Conventions",
70
+ "",
71
+ "- **Auto-session resolution**: All tools accept an optional `sessionId`. When only one session is active,",
72
+ " you can omit it — the server resolves it automatically.",
73
+ "- **Synchronous stepping**: `step` returns the new stopped state directly. Do not follow a step with get_state",
74
+ " unless you need to change parameters like `variableDepth` or `threadId`.",
75
+ "- **Coarse get_state**: Fetches threads + stack frames + scopes + variables in a single call.",
76
+ " Use `depth` (default 10) and `variableDepth` (default 1) to control how much data is returned.",
77
+ "- **Output buffering**: Program output is buffered in a 10K-line ring buffer. Oldest lines are dropped",
78
+ " when the buffer is full. Use `get_output` with `clear: true` to consume and free buffer space.",
79
+ "- **Adapter registration**: Use `configure_adapter` to register adapters. Configs are persisted to",
80
+ " `~/.dap-mcp/adapters.json` (global) or `./.dap-mcp/adapters.json` (project), with project overriding global.",
81
+ "- **Error handling**: All tool errors are returned as `isError: true` results with an error message.",
82
+ " The session remains usable after most errors.",
83
+ ].join("\n"),
84
+ }
85
+ );
86
+
87
+ // ────────────────────────────────────────────────────────────────────────────
88
+ // 1. list_adapters
89
+ // ────────────────────────────────────────────────────────────────────────────
90
+ server.tool(
91
+ "list_adapters",
92
+ "List all known debug adapters with their availability status on this machine.",
93
+ {},
94
+ async () => {
95
+ try {
96
+ const adapters = registry.listAll();
97
+ const results = await Promise.all(
98
+ adapters.map(async (a) => {
99
+ const cmd = a.runtime || a.program;
100
+ const available = cmd ? await checkAdapterAvailable(cmd) : false;
101
+ return { ...a, available };
102
+ })
103
+ );
104
+ return {
105
+ content: [{ type: "text" as const, text: JSON.stringify(results, null, 2) }],
106
+ };
107
+ } catch (err) {
108
+ return {
109
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
110
+ isError: true,
111
+ };
112
+ }
113
+ }
114
+ );
115
+
116
+ // ────────────────────────────────────────────────────────────────────────────
117
+ // 2. configure_adapter
118
+ // ────────────────────────────────────────────────────────────────────────────
119
+ server.tool(
120
+ "configure_adapter",
121
+ "Register or update a debug adapter configuration and persist it to disk.",
122
+ {
123
+ type: z.string().describe("Adapter type identifier (e.g. 'python', 'node', 'go')"),
124
+ program: z.string().describe("Path or command for the adapter executable"),
125
+ runtime: z.string().optional().describe("Optional runtime to invoke the program (e.g. 'node', 'python3')"),
126
+ args: z.array(z.string()).optional().describe("Additional arguments to pass to the adapter"),
127
+ launchDefaults: z.record(z.string(), z.unknown()).optional().describe("Default launch configuration properties"),
128
+ installHint: z.string().optional().describe("Hint shown when adapter is not installed"),
129
+ scope: z.enum(["global", "project"]).optional().describe("Where to persist the adapter config (default: project)"),
130
+ },
131
+ async ({ type, program, runtime, args, launchDefaults, installHint, scope }) => {
132
+ try {
133
+ const config = { type, program, runtime, args, launchDefaults, installHint };
134
+ const saveScope = scope ?? "project";
135
+ registry.register(config, saveScope);
136
+ await registry.save(saveScope);
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text" as const,
141
+ text: JSON.stringify({ success: true, type, scope: saveScope }, null, 2),
142
+ },
143
+ ],
144
+ };
145
+ } catch (err) {
146
+ return {
147
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
148
+ isError: true,
149
+ };
150
+ }
151
+ }
152
+ );
153
+
154
+ // ────────────────────────────────────────────────────────────────────────────
155
+ // 3. debug_launch
156
+ // ────────────────────────────────────────────────────────────────────────────
157
+ server.tool(
158
+ "debug_launch",
159
+ "Launch a program under a debug adapter. Performs the full DAP initialization lifecycle and returns the session ID and initial stopped state.",
160
+ {
161
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
162
+ program: z.string().describe("Path to the program to debug"),
163
+ args: z.array(z.string()).optional().describe("Program arguments"),
164
+ cwd: z.string().optional().describe("Working directory for the program"),
165
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
166
+ stopOnEntry: z.boolean().optional().describe("Stop on program entry (default: true)"),
167
+ breakpoints: z
168
+ .array(
169
+ z.object({
170
+ file: z.string().describe("Source file path"),
171
+ line: z.number().describe("Line number"),
172
+ condition: z.string().optional(),
173
+ hitCondition: z.string().optional(),
174
+ logMessage: z.string().optional(),
175
+ })
176
+ )
177
+ .optional()
178
+ .describe("Breakpoints to set before launching"),
179
+ exceptionBreakpoints: z
180
+ .array(z.string())
181
+ .optional()
182
+ .describe("Exception filter IDs (e.g. ['uncaughtExceptions'])"),
183
+ adapterConfig: z
184
+ .object({
185
+ type: z.string(),
186
+ program: z.string(),
187
+ runtime: z.string().optional(),
188
+ args: z.array(z.string()).optional(),
189
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
190
+ installHint: z.string().optional(),
191
+ })
192
+ .optional()
193
+ .describe("Override adapter config inline instead of looking up from registry"),
194
+ },
195
+ async ({
196
+ adapter,
197
+ program,
198
+ args,
199
+ cwd,
200
+ env,
201
+ stopOnEntry,
202
+ breakpoints,
203
+ exceptionBreakpoints,
204
+ adapterConfig,
205
+ }) => {
206
+ const shouldStop = stopOnEntry !== false;
207
+ let session: DebugSession | undefined;
208
+ try {
209
+ const config =
210
+ adapterConfig ??
211
+ registry.get(adapter);
212
+
213
+ if (!config) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text" as const,
218
+ text: `Error: Adapter '${adapter}' not found. Use list_adapters to see available adapters or configure_adapter to register one.`,
219
+ },
220
+ ],
221
+ isError: true,
222
+ };
223
+ }
224
+
225
+ const id = sessions.generateId();
226
+ session = new DebugSession(id, config);
227
+ sessions.add(session);
228
+
229
+ // Initialize (waits for the initialized event internally)
230
+ await session.initialize();
231
+
232
+ // Set breakpoints grouped by file
233
+ if (breakpoints && breakpoints.length > 0) {
234
+ const byFile = new Map<string, typeof breakpoints>();
235
+ for (const bp of breakpoints) {
236
+ const existing = byFile.get(bp.file) ?? [];
237
+ existing.push(bp);
238
+ byFile.set(bp.file, existing);
239
+ }
240
+ for (const [file, bps] of byFile) {
241
+ await session.setBreakpoints(
242
+ file,
243
+ bps.map((b) => ({
244
+ line: b.line,
245
+ condition: b.condition,
246
+ hitCondition: b.hitCondition,
247
+ logMessage: b.logMessage,
248
+ }))
249
+ );
250
+ }
251
+ }
252
+
253
+ // Set exception breakpoints
254
+ if (exceptionBreakpoints && exceptionBreakpoints.length > 0) {
255
+ await session.setExceptionBreakpoints(exceptionBreakpoints);
256
+ }
257
+
258
+ // Configuration done
259
+ await session.configurationDone();
260
+
261
+ // Build launch request
262
+ const launchArgs: Record<string, unknown> = {
263
+ ...config.launchDefaults,
264
+ program,
265
+ stopOnEntry: shouldStop,
266
+ };
267
+ if (args !== undefined) launchArgs.args = args;
268
+ if (cwd !== undefined) launchArgs.cwd = cwd;
269
+ if (env !== undefined) launchArgs.env = env;
270
+
271
+ await session.launch(launchArgs);
272
+
273
+ let stateData: Record<string, unknown>;
274
+ if (shouldStop) {
275
+ try {
276
+ await session.waitForStopped(10000);
277
+ stateData = (await session.collectState()) as unknown as Record<string, unknown>;
278
+ } catch {
279
+ stateData = { status: session.state };
280
+ }
281
+ } else {
282
+ stateData = { status: session.state };
283
+ }
284
+
285
+ return {
286
+ content: [
287
+ {
288
+ type: "text" as const,
289
+ text: JSON.stringify({ sessionId: id, ...stateData }, null, 2),
290
+ },
291
+ ],
292
+ };
293
+ } catch (err) {
294
+ if (session) {
295
+ sessions.remove(session.id);
296
+ session.disconnect().catch(() => undefined);
297
+ }
298
+ return {
299
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
300
+ isError: true,
301
+ };
302
+ }
303
+ }
304
+ );
305
+
306
+ // ────────────────────────────────────────────────────────────────────────────
307
+ // 4. debug_attach
308
+ // ────────────────────────────────────────────────────────────────────────────
309
+ server.tool(
310
+ "debug_attach",
311
+ "Attach a debug adapter to an already-running process.",
312
+ {
313
+ adapter: z.string().describe("Adapter type to use"),
314
+ port: z.number().optional().describe("Debug port to attach to"),
315
+ host: z.string().optional().describe("Host to attach to (default: localhost)"),
316
+ processId: z.number().optional().describe("PID of process to attach to"),
317
+ attachConfig: z.record(z.string(), z.unknown()).optional().describe("Arbitrary attach configuration properties"),
318
+ adapterConfig: z
319
+ .object({
320
+ type: z.string(),
321
+ program: z.string(),
322
+ runtime: z.string().optional(),
323
+ args: z.array(z.string()).optional(),
324
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
325
+ installHint: z.string().optional(),
326
+ })
327
+ .optional()
328
+ .describe("Override adapter config inline"),
329
+ },
330
+ async ({ adapter, port, host, processId, attachConfig, adapterConfig }) => {
331
+ let session: DebugSession | undefined;
332
+ try {
333
+ const config = adapterConfig ?? registry.get(adapter);
334
+ if (!config) {
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text" as const,
339
+ text: `Error: Adapter '${adapter}' not found.`,
340
+ },
341
+ ],
342
+ isError: true,
343
+ };
344
+ }
345
+
346
+ const id = sessions.generateId();
347
+ session = new DebugSession(id, config);
348
+ sessions.add(session);
349
+
350
+ await session.initialize();
351
+ await session.configurationDone();
352
+
353
+ // Merge order: adapter defaults (lowest priority) < user attachConfig < explicit params (highest)
354
+ const attachArgs: Record<string, unknown> = {
355
+ ...config.launchDefaults,
356
+ ...attachConfig,
357
+ };
358
+ if (port !== undefined) attachArgs.port = port;
359
+ if (host !== undefined) attachArgs.host = host;
360
+ if (processId !== undefined) attachArgs.processId = processId;
361
+
362
+ await session.attach(attachArgs);
363
+
364
+ return {
365
+ content: [
366
+ {
367
+ type: "text" as const,
368
+ text: JSON.stringify({ sessionId: id, status: session.state }, null, 2),
369
+ },
370
+ ],
371
+ };
372
+ } catch (err) {
373
+ if (session) {
374
+ sessions.remove(session.id);
375
+ session.disconnect().catch(() => undefined);
376
+ }
377
+ return {
378
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
379
+ isError: true,
380
+ };
381
+ }
382
+ }
383
+ );
384
+
385
+ // ────────────────────────────────────────────────────────────────────────────
386
+ // 5. set_breakpoints
387
+ // ────────────────────────────────────────────────────────────────────────────
388
+ server.tool(
389
+ "set_breakpoints",
390
+ "Set breakpoints in a source file for an active debug session.",
391
+ {
392
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
393
+ file: z.string().describe("Absolute path to the source file"),
394
+ breakpoints: z
395
+ .array(
396
+ z.object({
397
+ line: z.number().describe("Line number (1-based)"),
398
+ condition: z.string().optional().describe("Condition expression"),
399
+ hitCondition: z.string().optional().describe("Hit count condition"),
400
+ logMessage: z.string().optional().describe("Log message (logpoint)"),
401
+ })
402
+ )
403
+ .describe("Breakpoints to set"),
404
+ },
405
+ async ({ sessionId, file, breakpoints }) => {
406
+ try {
407
+ const session = sessions.resolve(sessionId);
408
+ const results = await session.setBreakpoints(file, breakpoints);
409
+ return {
410
+ content: [{ type: "text" as const, text: JSON.stringify(results, null, 2) }],
411
+ };
412
+ } catch (err) {
413
+ return {
414
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
415
+ isError: true,
416
+ };
417
+ }
418
+ }
419
+ );
420
+
421
+ // ────────────────────────────────────────────────────────────────────────────
422
+ // 6. set_exception_breakpoints
423
+ // ────────────────────────────────────────────────────────────────────────────
424
+ server.tool(
425
+ "set_exception_breakpoints",
426
+ "Configure exception breakpoints for an active debug session.",
427
+ {
428
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
429
+ filters: z
430
+ .array(z.string())
431
+ .describe("Exception filter IDs to enable (e.g. ['uncaughtExceptions', 'userUnhandledExceptions'])"),
432
+ },
433
+ async ({ sessionId, filters }) => {
434
+ try {
435
+ const session = sessions.resolve(sessionId);
436
+ await session.setExceptionBreakpoints(filters);
437
+ return {
438
+ content: [
439
+ {
440
+ type: "text" as const,
441
+ text: JSON.stringify({ success: true, filters }, null, 2),
442
+ },
443
+ ],
444
+ };
445
+ } catch (err) {
446
+ return {
447
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
448
+ isError: true,
449
+ };
450
+ }
451
+ }
452
+ );
453
+
454
+ // ────────────────────────────────────────────────────────────────────────────
455
+ // 7. get_state
456
+ // ────────────────────────────────────────────────────────────────────────────
457
+ server.tool(
458
+ "get_state",
459
+ "Get the current state of a debug session. If stopped, returns full stack/scopes/variables. If running, returns status and buffered output.",
460
+ {
461
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
462
+ threadId: z.number().optional().describe("Thread ID (uses stopped thread if omitted)"),
463
+ depth: z.number().optional().describe("Stack trace depth (default: 10)"),
464
+ variableDepth: z.number().optional().describe("Variable expansion depth (default: 1)"),
465
+ timeout: z
466
+ .number()
467
+ .optional()
468
+ .describe("If running, wait up to this many milliseconds for the program to stop (default: 0 = don't wait)"),
469
+ },
470
+ async ({ sessionId, threadId, depth, variableDepth, timeout }) => {
471
+ try {
472
+ const session = sessions.resolve(sessionId);
473
+ const waitMs = timeout ?? 0;
474
+
475
+ if (session.state === "stopped") {
476
+ const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
477
+ return {
478
+ content: [{ type: "text" as const, text: JSON.stringify(state, null, 2) }],
479
+ };
480
+ }
481
+
482
+ if (waitMs > 0 && session.state === "running") {
483
+ try {
484
+ await session.waitForStopped(waitMs);
485
+ const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
486
+ return {
487
+ content: [{ type: "text" as const, text: JSON.stringify(state, null, 2) }],
488
+ };
489
+ } catch {
490
+ // Timed out — fall through to return running status
491
+ }
492
+ }
493
+
494
+ const { output } = session.getOutput();
495
+ return {
496
+ content: [
497
+ {
498
+ type: "text" as const,
499
+ text: JSON.stringify(
500
+ {
501
+ status: session.state,
502
+ output: output.map((e) => e.text),
503
+ },
504
+ null,
505
+ 2
506
+ ),
507
+ },
508
+ ],
509
+ };
510
+ } catch (err) {
511
+ return {
512
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
513
+ isError: true,
514
+ };
515
+ }
516
+ }
517
+ );
518
+
519
+ // ────────────────────────────────────────────────────────────────────────────
520
+ // 8. evaluate
521
+ // ────────────────────────────────────────────────────────────────────────────
522
+ server.tool(
523
+ "evaluate",
524
+ "Evaluate an expression in the context of a stopped debug session.",
525
+ {
526
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
527
+ expression: z.string().describe("Expression to evaluate"),
528
+ frameId: z.number().optional().describe("Stack frame ID (uses topmost frame if omitted)"),
529
+ context: z
530
+ .enum(["repl", "watch", "hover"])
531
+ .optional()
532
+ .describe("Evaluation context (default: repl)"),
533
+ },
534
+ async ({ sessionId, expression, frameId, context }) => {
535
+ try {
536
+ const session = sessions.resolve(sessionId);
537
+ if (session.state !== "stopped") {
538
+ return {
539
+ content: [
540
+ {
541
+ type: "text" as const,
542
+ text: `Error: Session is not stopped (current state: ${session.state}). Use 'step' with action 'pause' to stop first.`,
543
+ },
544
+ ],
545
+ isError: true,
546
+ };
547
+ }
548
+ const result = await session.evaluate(expression, frameId, context ?? "repl");
549
+ return {
550
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
551
+ };
552
+ } catch (err) {
553
+ return {
554
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
555
+ isError: true,
556
+ };
557
+ }
558
+ }
559
+ );
560
+
561
+ // ────────────────────────────────────────────────────────────────────────────
562
+ // 9. step
563
+ // ────────────────────────────────────────────────────────────────────────────
564
+ server.tool(
565
+ "step",
566
+ "Control execution of a debug session: continue, step over (next), step into, step out, or pause. Waits for the program to stop and returns the new state.",
567
+ {
568
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
569
+ action: z
570
+ .enum(["continue", "next", "stepIn", "stepOut", "pause"])
571
+ .describe("Execution control action"),
572
+ threadId: z.number().optional().describe("Thread ID (uses stopped thread if omitted)"),
573
+ timeout: z
574
+ .number()
575
+ .optional()
576
+ .describe("Milliseconds to wait for the program to stop after the action (default: 30000)"),
577
+ },
578
+ async ({ sessionId, action, threadId, timeout }) => {
579
+ try {
580
+ const session = sessions.resolve(sessionId);
581
+ const waitMs = timeout ?? 30000;
582
+ const startTime = Date.now();
583
+
584
+ // If already stopped and asking to pause, just return current state
585
+ if (action === "pause" && session.state === "stopped") {
586
+ const state = await session.collectState(threadId);
587
+ return {
588
+ content: [{ type: "text" as const, text: JSON.stringify(state, null, 2) }],
589
+ };
590
+ }
591
+
592
+ switch (action) {
593
+ case "continue":
594
+ await session.continueExecution(threadId);
595
+ break;
596
+ case "next":
597
+ await session.next(threadId);
598
+ break;
599
+ case "stepIn":
600
+ await session.stepIn(threadId);
601
+ break;
602
+ case "stepOut":
603
+ await session.stepOut(threadId);
604
+ break;
605
+ case "pause":
606
+ await session.pause(threadId);
607
+ break;
608
+ }
609
+
610
+ try {
611
+ await session.waitForStopped(waitMs);
612
+ } catch (err) {
613
+ // Timeout or terminated
614
+ const elapsed = Date.now() - startTime;
615
+ if (session.state === "terminated") {
616
+ const { output } = session.getOutput();
617
+ return {
618
+ content: [
619
+ {
620
+ type: "text" as const,
621
+ text: JSON.stringify(
622
+ { status: "terminated", output: output.map((e) => e.text) },
623
+ null,
624
+ 2
625
+ ),
626
+ },
627
+ ],
628
+ };
629
+ }
630
+ return {
631
+ content: [
632
+ {
633
+ type: "text" as const,
634
+ text: JSON.stringify({ status: "running", elapsedMs: elapsed }, null, 2),
635
+ },
636
+ ],
637
+ };
638
+ }
639
+
640
+ if (session.state === "terminated") {
641
+ const { output } = session.getOutput();
642
+ return {
643
+ content: [
644
+ {
645
+ type: "text" as const,
646
+ text: JSON.stringify(
647
+ { status: "terminated", output: output.map((e) => e.text) },
648
+ null,
649
+ 2
650
+ ),
651
+ },
652
+ ],
653
+ };
654
+ }
655
+
656
+ const state = await session.collectState(threadId);
657
+ return {
658
+ content: [{ type: "text" as const, text: JSON.stringify(state, null, 2) }],
659
+ };
660
+ } catch (err) {
661
+ return {
662
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
663
+ isError: true,
664
+ };
665
+ }
666
+ }
667
+ );
668
+
669
+ // ────────────────────────────────────────────────────────────────────────────
670
+ // 10. get_output
671
+ // ────────────────────────────────────────────────────────────────────────────
672
+ server.tool(
673
+ "get_output",
674
+ "Get buffered stdout/stderr and debug adapter output for a session.",
675
+ {
676
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
677
+ category: z
678
+ .string()
679
+ .optional()
680
+ .describe("Filter by output category: 'stdout', 'stderr', 'console', etc."),
681
+ clear: z.boolean().optional().describe("Clear the output buffer after reading (default: false)"),
682
+ },
683
+ async ({ sessionId, category, clear }) => {
684
+ try {
685
+ const session = sessions.resolve(sessionId);
686
+ const result = session.getOutput(category, clear ?? false);
687
+ return {
688
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
689
+ };
690
+ } catch (err) {
691
+ return {
692
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
693
+ isError: true,
694
+ };
695
+ }
696
+ }
697
+ );
698
+
699
+ // ────────────────────────────────────────────────────────────────────────────
700
+ // 11. debug_disconnect
701
+ // ────────────────────────────────────────────────────────────────────────────
702
+ server.tool(
703
+ "debug_disconnect",
704
+ "Disconnect from a debug session and optionally terminate the debuggee.",
705
+ {
706
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
707
+ terminateDebuggee: z
708
+ .boolean()
709
+ .optional()
710
+ .describe("Whether to terminate the debuggee process (default: true for launched sessions, false for attached)"),
711
+ },
712
+ async ({ sessionId, terminateDebuggee }) => {
713
+ try {
714
+ const session = sessions.resolve(sessionId);
715
+ await session.disconnect(terminateDebuggee);
716
+ sessions.remove(session.id);
717
+ return {
718
+ content: [
719
+ {
720
+ type: "text" as const,
721
+ text: JSON.stringify({ success: true, sessionId: session.id }, null, 2),
722
+ },
723
+ ],
724
+ };
725
+ } catch (err) {
726
+ return {
727
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
728
+ isError: true,
729
+ };
730
+ }
731
+ }
732
+ );
733
+
734
+ // ────────────────────────────────────────────────────────────────────────────
735
+ // 12. debug_restart
736
+ // ────────────────────────────────────────────────────────────────────────────
737
+ server.tool(
738
+ "debug_restart",
739
+ "Restart a debug session with the same adapter and launch configuration.",
740
+ {
741
+ sessionId: z.string().optional().describe("Session ID to restart (omit if only one session is active)"),
742
+ },
743
+ async ({ sessionId }) => {
744
+ let oldSession: DebugSession | undefined;
745
+ let newSession: DebugSession | undefined;
746
+ try {
747
+ oldSession = sessions.resolve(sessionId);
748
+ const launchMode = oldSession.getLaunchMode();
749
+
750
+ if (launchMode === "attach") {
751
+ return {
752
+ content: [
753
+ {
754
+ type: "text" as const,
755
+ text: "Error: Cannot restart an attached session. Attached sessions cannot be restarted.",
756
+ },
757
+ ],
758
+ isError: true,
759
+ };
760
+ }
761
+
762
+ const launchConfig = oldSession.getLaunchConfig();
763
+ const adapterConfig = oldSession.getAdapterConfig();
764
+ const savedBreakpoints = oldSession.getBreakpoints();
765
+
766
+ if (!launchConfig) {
767
+ return {
768
+ content: [
769
+ {
770
+ type: "text" as const,
771
+ text: "Error: No launch configuration found for this session.",
772
+ },
773
+ ],
774
+ isError: true,
775
+ };
776
+ }
777
+
778
+ // Disconnect old session
779
+ await oldSession.disconnect();
780
+ sessions.remove(oldSession.id);
781
+
782
+ // Create new session with same adapter config
783
+ const newId = sessions.generateId();
784
+ newSession = new DebugSession(newId, adapterConfig);
785
+ sessions.add(newSession);
786
+
787
+ // Initialize (waits for the initialized event internally)
788
+ await newSession.initialize();
789
+
790
+ // Re-apply saved breakpoints
791
+ for (const [file, bps] of savedBreakpoints) {
792
+ await newSession.setBreakpoints(file, bps);
793
+ }
794
+
795
+ await newSession.configurationDone();
796
+ await newSession.launch(launchConfig);
797
+
798
+ const stopOnEntry = launchConfig.stopOnEntry !== false;
799
+ let stateData: Record<string, unknown>;
800
+ if (stopOnEntry) {
801
+ try {
802
+ await newSession.waitForStopped(10000);
803
+ stateData = (await newSession.collectState()) as unknown as Record<string, unknown>;
804
+ } catch {
805
+ stateData = { status: newSession.state };
806
+ }
807
+ } else {
808
+ stateData = { status: newSession.state };
809
+ }
810
+
811
+ return {
812
+ content: [
813
+ {
814
+ type: "text" as const,
815
+ text: JSON.stringify(
816
+ { sessionId: newId, previousSessionId: oldSession.id, ...stateData },
817
+ null,
818
+ 2
819
+ ),
820
+ },
821
+ ],
822
+ };
823
+ } catch (err) {
824
+ if (newSession) {
825
+ sessions.remove(newSession.id);
826
+ newSession.disconnect().catch(() => undefined);
827
+ }
828
+ return {
829
+ content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
830
+ isError: true,
831
+ };
832
+ }
833
+ }
834
+ );
835
+
836
+ // ────────────────────────────────────────────────────────────────────────────
837
+ // Resource: dap://sessions
838
+ // ────────────────────────────────────────────────────────────────────────────
839
+ // NOTE: This resource does not emit change notifications.
840
+ // Clients should poll or use get_state for up-to-date session information.
841
+ server.resource(
842
+ "sessions",
843
+ "dap://sessions",
844
+ {
845
+ description: "List of all active debug sessions with their current status.",
846
+ mimeType: "application/json",
847
+ },
848
+ async (_uri) => {
849
+ const all = sessions.listAll().map((s) => ({
850
+ id: s.id,
851
+ state: s.state,
852
+ adapterType: s.getAdapterConfig().type,
853
+ }));
854
+ return {
855
+ contents: [
856
+ {
857
+ uri: "dap://sessions",
858
+ text: JSON.stringify(all, null, 2),
859
+ },
860
+ ],
861
+ };
862
+ }
863
+ );
864
+
865
+ return server;
866
+ }