dap-mcp-server 0.1.12 → 0.1.16

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 (58) hide show
  1. package/README.md +25 -7
  2. package/SKILL.md +37 -186
  3. package/build/adapters/detect.d.ts +12 -0
  4. package/build/adapters/detect.js +91 -0
  5. package/build/adapters/detect.js.map +1 -1
  6. package/build/adapters/registry.js +35 -12
  7. package/build/adapters/registry.js.map +1 -1
  8. package/build/diagnostics/doctor.d.ts +64 -0
  9. package/build/diagnostics/doctor.js +209 -0
  10. package/build/diagnostics/doctor.js.map +1 -0
  11. package/build/server.js +305 -83
  12. package/build/server.js.map +1 -1
  13. package/build/session/inspect.d.ts +6 -2
  14. package/build/session/inspect.js +24 -8
  15. package/build/session/inspect.js.map +1 -1
  16. package/build/session/run.js +2 -9
  17. package/build/session/run.js.map +1 -1
  18. package/build/session/session.d.ts +16 -0
  19. package/build/session/session.js +63 -8
  20. package/build/session/session.js.map +1 -1
  21. package/integrations/claude/dap-debugging/.claude-plugin/plugin.json +9 -0
  22. package/integrations/claude/dap-debugging/.mcp.json +8 -0
  23. package/integrations/claude/dap-debugging/skills/dap-debugging/SKILL.md +49 -0
  24. package/package.json +8 -3
  25. package/.forgejo/workflows/publish.yml +0 -63
  26. package/CLAUDE.md +0 -71
  27. package/TODO.md +0 -32
  28. package/docs/plans/2026-03-05-dap-wrapper-mcp-design.md +0 -326
  29. package/docs/plans/2026-03-05-dap-wrapper-mcp-implementation.md +0 -2815
  30. package/src/adapters/builtin.ts +0 -75
  31. package/src/adapters/detect.test.ts +0 -36
  32. package/src/adapters/detect.ts +0 -36
  33. package/src/adapters/registry.test.ts +0 -74
  34. package/src/adapters/registry.ts +0 -86
  35. package/src/dap/dap-client.test.ts +0 -118
  36. package/src/dap/dap-client.ts +0 -588
  37. package/src/dap/protocol.test.ts +0 -82
  38. package/src/dap/protocol.ts +0 -78
  39. package/src/dap/ring-buffer.test.ts +0 -82
  40. package/src/dap/ring-buffer.ts +0 -64
  41. package/src/dap/types.ts +0 -115
  42. package/src/index.ts +0 -30
  43. package/src/integration/debug-lifecycle.test.ts +0 -131
  44. package/src/server.ts +0 -1096
  45. package/src/session/inspect.test.ts +0 -126
  46. package/src/session/inspect.ts +0 -160
  47. package/src/session/run.test.ts +0 -74
  48. package/src/session/run.ts +0 -144
  49. package/src/session/session-manager.test.ts +0 -74
  50. package/src/session/session-manager.ts +0 -67
  51. package/src/session/session.test.ts +0 -126
  52. package/src/session/session.ts +0 -482
  53. package/src/test-fixtures/crash-adapter.js +0 -23
  54. package/src/test-fixtures/mock-adapter.js +0 -164
  55. package/src/test-fixtures/no-stop-adapter.js +0 -77
  56. package/src/test-fixtures/silent-adapter.js +0 -24
  57. package/src/test-fixtures/tcp-mock-adapter.js +0 -87
  58. package/tsconfig.json +0 -16
package/build/server.js CHANGED
@@ -30,7 +30,8 @@ import { SessionManager } from "./session/session-manager.js";
30
30
  import { DebugSession } from "./session/session.js";
31
31
  import { runDebugInspect } from "./session/inspect.js";
32
32
  import { runDebugRun } from "./session/run.js";
33
- import { checkAdapterAvailable } from "./adapters/detect.js";
33
+ import { inspectAdapterAvailability } from "./adapters/detect.js";
34
+ import { resolveProjectAdapter, runDebugDoctor } from "./diagnostics/doctor.js";
34
35
  function readPackageVersion() {
35
36
  try {
36
37
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -47,7 +48,7 @@ export function createServer() {
47
48
  const registry = new AdapterRegistry(globalDir, projectDir);
48
49
  const sessions = new SessionManager();
49
50
  // Load persisted adapter configs (best-effort, don't block server start)
50
- registry.load().catch((err) => {
51
+ const registryReady = registry.load().catch((err) => {
51
52
  console.error("Warning: failed to load adapter configs:", err);
52
53
  });
53
54
  const server = new McpServer({
@@ -55,57 +56,227 @@ export function createServer() {
55
56
  version: readPackageVersion(),
56
57
  }, {
57
58
  instructions: [
58
- "This server provides AI-driven debugging via the Debug Adapter Protocol (DAP).",
59
- "",
60
- "## When to use which tool",
61
- "",
62
- "- **`debug_inspect`** — One-shot 'what is X at line N?'. Launches, hits a breakpoint, evaluates expressions,",
63
- " disconnects. Use this first; it collapses a multi-call workflow into a single call.",
64
- "- **`debug_run`** — Run a program with no stops and collect all output. Combine with `logpoints` (breakpoints",
65
- " with `logMessage`) for printf-style debugging without modifying source.",
66
- "- **`debug_launch` + step/evaluate** — Interactive multi-step session when you need to explore.",
67
- "",
68
- "## Interactive Debugging Workflow",
69
- "",
70
- "1. `list_adapters` — discover available adapters; the response includes a summary and install hints.",
71
- "2. `debug_launch` (or `debug_attach`) — start a session. With breakpoints provided, `stopOnEntry` defaults",
72
- " to false so the program runs to the first breakpoint. Returns sessionId + initial stopped state.",
73
- "3. Inspect state with `get_state` — threads + call stack + scopes + variables in one call.",
74
- "4. `step` — continue, next, stepIn, stepOut, pause. Blocks until stopped; returns new state directly.",
75
- "5. `evaluate` — expressions in the context of a stopped frame.",
76
- "6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints mid-session. Use `logMessage`",
77
- " on a breakpoint to make it a logpoint (emits formatted text without pausing).",
78
- "7. `list_breakpoints` — see what's currently set.",
79
- "8. `get_output` — retrieve stdout/stderr/console output (10K-line ring buffer).",
80
- "9. `debug_disconnect` — end. `debug_restart` replays the launch config with preserved breakpoints.",
81
- "",
82
- "## Key Conventions",
83
- "",
84
- "- **Auto-session resolution**: tools accept an optional `sessionId`; with one active session, omit it.",
85
- "- **Synchronous stepping**: `step` returns the new stopped state. Don't follow it with `get_state`.",
86
- "- **Variable depth**: `get_state` accepts `variableDepth` (default 1). With `> 1`, container variables",
87
- " expose their entries as a structured `children` array.",
88
- "- **stopOnEntry default**: false when `debug_launch` is given breakpoints, true otherwise.",
89
- "- **Adapter registration**: `configure_adapter` persists to `~/.dap-mcp/adapters.json` (global) or",
90
- " `./.dap-mcp/adapters.json` (project); project overrides global.",
91
- "- **Error handling**: tool errors return `isError: true`. The session usually remains usable.",
59
+ "Local source-code debugger using the Debug Adapter Protocol.",
60
+ "Use it when a test fails, a program crashes, runtime state is unclear, or code behavior cannot be explained statically.",
61
+ "Start with debug_doctor to detect the project and verify adapter readiness.",
62
+ "Prefer debug_at for a one-call breakpoint inspection: it selects the adapter, launches, waits, evaluates expressions, and cleans up.",
63
+ "Use debug_run when output or termination is enough. Use debug_launch only for interactive stepping or multiple inspection points.",
64
+ "For interactive sessions: debug_launch -> get_state/evaluate -> step -> debug_disconnect. A launch with breakpoints waits by default and reports stopped, terminated, or timeout explicitly.",
65
+ "Omit sessionId when exactly one session is active. Adapter-specific DAP fields belong in launchConfig. Tool errors use isError=true.",
92
66
  ].join("\n"),
93
67
  });
68
+ const doctorOutputSchema = z.object({
69
+ cwd: z.string(),
70
+ ready: z.boolean(),
71
+ recommendedAdapter: z.string().optional(),
72
+ detected: z.object({
73
+ languages: z.array(z.string()),
74
+ testFrameworks: z.array(z.string()),
75
+ packageManager: z.string().optional(),
76
+ pythonPath: z.string().optional(),
77
+ launchJson: z.string().optional(),
78
+ }),
79
+ adapters: z.array(z.object({
80
+ type: z.string(),
81
+ source: z.enum(["builtin", "global", "project"]),
82
+ selected: z.boolean(),
83
+ available: z.boolean(),
84
+ command: z.string(),
85
+ reason: z.string().optional(),
86
+ installHint: z.string().optional(),
87
+ runtime: z.string().optional(),
88
+ program: z.string(),
89
+ transport: z.enum(["stdio", "tcp"]),
90
+ })),
91
+ nextSteps: z.array(z.string()),
92
+ });
93
+ const breakpointResultSchema = z.object({
94
+ id: z.number().optional(),
95
+ verified: z.boolean(),
96
+ line: z.number(),
97
+ message: z.string().optional(),
98
+ });
99
+ const debugAtOutputSchema = z.object({
100
+ adapter: z.string(),
101
+ hit: z.boolean(),
102
+ reason: z.string().optional(),
103
+ location: z
104
+ .object({
105
+ file: z.string(),
106
+ line: z.number(),
107
+ functionName: z.string().optional(),
108
+ column: z.number().optional(),
109
+ })
110
+ .optional(),
111
+ values: z
112
+ .record(z.string(), z.object({
113
+ result: z.string().optional(),
114
+ type: z.string().optional(),
115
+ error: z.string().optional(),
116
+ }))
117
+ .optional(),
118
+ output: z.array(z.string()).optional(),
119
+ status: z.string().optional(),
120
+ timedOut: z.boolean().optional(),
121
+ breakpoint: breakpointResultSchema.optional(),
122
+ });
123
+ // ────────────────────────────────────────────────────────────────────────────
124
+ // High-level LLM entry points
125
+ // ────────────────────────────────────────────────────────────────────────────
126
+ server.registerTool("debug_doctor", {
127
+ title: "Check debugger readiness",
128
+ description: "Inspect a local project, detect its language/tooling, select the likely debug adapter, verify the complete adapter command, and return concrete remediation. Use this before debugging or when an adapter fails to start.",
129
+ inputSchema: z.object({
130
+ cwd: z.string().optional().describe("Project directory (default: server working directory)"),
131
+ adapter: z.string().optional().describe("Adapter to validate instead of auto-detecting one"),
132
+ }),
133
+ outputSchema: doctorOutputSchema,
134
+ annotations: {
135
+ readOnlyHint: true,
136
+ destructiveHint: false,
137
+ idempotentHint: true,
138
+ openWorldHint: false,
139
+ },
140
+ }, async (options) => {
141
+ try {
142
+ await registryReady;
143
+ const result = await runDebugDoctor(registry, options);
144
+ return {
145
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
146
+ structuredContent: { ...result },
147
+ };
148
+ }
149
+ catch (err) {
150
+ return {
151
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
152
+ isError: true,
153
+ };
154
+ }
155
+ });
156
+ server.registerTool("debug_at", {
157
+ title: "Inspect runtime state at a line",
158
+ description: "Primary one-shot debugging tool for bugs and failing behavior. Auto-selects a ready adapter, launches the program, stops at file:line, evaluates expressions, reports breakpoint/termination/timeout details, and cleans up.",
159
+ inputSchema: z.object({
160
+ cwd: z.string().optional().describe("Project and debuggee working directory"),
161
+ adapter: z.string().optional().describe("Adapter type; auto-detected from file/project when omitted"),
162
+ program: z
163
+ .string()
164
+ .optional()
165
+ .describe("Launch target; defaults to file for Python/Node and cwd for Go"),
166
+ args: z.array(z.string()).optional().describe("Program arguments"),
167
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
168
+ file: z.string().describe("Source file containing the breakpoint"),
169
+ line: z.number().int().positive().describe("One-based breakpoint line"),
170
+ condition: z.string().optional().describe("Conditional breakpoint expression"),
171
+ hitCondition: z.string().optional().describe("Adapter-specific hit count condition"),
172
+ expressions: z.array(z.string()).optional().describe("Expressions to evaluate in the stopped frame"),
173
+ timeout: z.number().positive().optional().describe("Breakpoint wait timeout in milliseconds"),
174
+ launchConfig: z
175
+ .record(z.string(), z.unknown())
176
+ .optional()
177
+ .describe("Adapter-specific DAP launch fields"),
178
+ }),
179
+ outputSchema: debugAtOutputSchema,
180
+ annotations: {
181
+ readOnlyHint: false,
182
+ destructiveHint: true,
183
+ idempotentHint: false,
184
+ openWorldHint: false,
185
+ },
186
+ }, async (options) => {
187
+ try {
188
+ await registryReady;
189
+ const cwd = path.resolve(options.cwd ?? process.cwd());
190
+ const sourceFile = path.resolve(cwd, options.file);
191
+ const resolved = await resolveProjectAdapter(registry, {
192
+ cwd,
193
+ adapter: options.adapter,
194
+ sourceFile,
195
+ });
196
+ const adapter = resolved.result.recommendedAdapter;
197
+ if (!adapter || !resolved.config || !resolved.result.ready) {
198
+ return {
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: `Debugger is not ready:\n${JSON.stringify(resolved.result, null, 2)}`,
203
+ },
204
+ ],
205
+ isError: true,
206
+ };
207
+ }
208
+ const hasLaunchTarget = options.launchConfig !== undefined &&
209
+ ["program", "module", "code"].some((key) => options.launchConfig?.[key] !== undefined);
210
+ if (!options.program && !hasLaunchTarget && adapter === "cppdbg") {
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: "Error: C/C++ debugging requires the compiled executable in program or launchConfig.program.",
216
+ },
217
+ ],
218
+ isError: true,
219
+ };
220
+ }
221
+ const program = options.program ??
222
+ (hasLaunchTarget ? undefined : adapter === "go" ? cwd : sourceFile);
223
+ const result = await runDebugInspect(registry, sessions, {
224
+ adapter,
225
+ adapterConfig: resolved.config,
226
+ program,
227
+ args: options.args,
228
+ cwd,
229
+ env: options.env,
230
+ at: {
231
+ file: sourceFile,
232
+ line: options.line,
233
+ condition: options.condition,
234
+ hitCondition: options.hitCondition,
235
+ },
236
+ expressions: options.expressions,
237
+ timeout: options.timeout,
238
+ launchConfig: options.launchConfig,
239
+ });
240
+ const structuredContent = { adapter, ...result };
241
+ return {
242
+ content: [
243
+ { type: "text", text: JSON.stringify(structuredContent, null, 2) },
244
+ ],
245
+ structuredContent,
246
+ };
247
+ }
248
+ catch (err) {
249
+ return {
250
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
251
+ isError: true,
252
+ };
253
+ }
254
+ });
94
255
  // ────────────────────────────────────────────────────────────────────────────
95
256
  // 1. list_adapters
96
257
  // ────────────────────────────────────────────────────────────────────────────
97
- server.tool("list_adapters", "List all known debug adapters with their availability status on this machine. Returns a summary line, the full adapter list, and a separate 'missing' list with installation hints for adapters that aren't installed.", {}, async () => {
258
+ server.tool("list_adapters", "List all known debug adapters with their availability status on this machine. Returns a summary line, the full adapter list, and a separate 'missing' list with installation hints for adapters that aren't installed.", {}, {
259
+ readOnlyHint: true,
260
+ destructiveHint: false,
261
+ idempotentHint: true,
262
+ openWorldHint: false,
263
+ }, async () => {
98
264
  try {
265
+ await registryReady;
99
266
  const adapters = registry.listAll();
100
267
  const results = await Promise.all(adapters.map(async (a) => {
101
- const cmd = a.runtime || a.program;
102
- const available = cmd ? await checkAdapterAvailable(cmd) : false;
103
- return { ...a, available };
268
+ const availability = await inspectAdapterAvailability(a);
269
+ return { ...a, ...availability };
104
270
  }));
105
271
  const available = results.filter((r) => r.available);
106
272
  const missing = results
107
273
  .filter((r) => !r.available)
108
- .map((r) => ({ type: r.type, installHint: r.installHint }));
274
+ .map((r) => ({
275
+ type: r.type,
276
+ command: r.command,
277
+ reason: r.reason,
278
+ installHint: r.installHint,
279
+ }));
109
280
  const summary = missing.length === 0
110
281
  ? `${available.length}/${results.length} adapters available`
111
282
  : `${available.length}/${results.length} adapters available; missing: ${missing.map((m) => m.type).join(", ")}`;
@@ -135,10 +306,27 @@ export function createServer() {
135
306
  args: z.array(z.string()).optional().describe("Additional arguments to pass to the adapter"),
136
307
  launchDefaults: z.record(z.string(), z.unknown()).optional().describe("Default launch configuration properties"),
137
308
  installHint: z.string().optional().describe("Hint shown when adapter is not installed"),
309
+ transport: z.enum(["stdio", "tcp"]).optional().describe("DAP transport used by the adapter"),
310
+ portMatcher: z.string().optional().describe("Regex whose first capture group is the TCP port"),
138
311
  scope: z.enum(["global", "project"]).optional().describe("Where to persist the adapter config (default: project)"),
139
- }, async ({ type, program, runtime, args, launchDefaults, installHint, scope }) => {
312
+ }, {
313
+ readOnlyHint: false,
314
+ destructiveHint: true,
315
+ idempotentHint: true,
316
+ openWorldHint: false,
317
+ }, async ({ type, program, runtime, args, launchDefaults, installHint, transport, portMatcher, scope }) => {
140
318
  try {
141
- const config = { type, program, runtime, args, launchDefaults, installHint };
319
+ await registryReady;
320
+ const config = {
321
+ type,
322
+ program,
323
+ runtime,
324
+ args,
325
+ launchDefaults,
326
+ installHint,
327
+ transport,
328
+ portMatcher,
329
+ };
142
330
  const saveScope = scope ?? "project";
143
331
  registry.register(config, saveScope);
144
332
  await registry.save(saveScope);
@@ -161,12 +349,16 @@ export function createServer() {
161
349
  // ────────────────────────────────────────────────────────────────────────────
162
350
  // 3. debug_launch
163
351
  // ────────────────────────────────────────────────────────────────────────────
164
- server.tool("debug_launch", "Launch a program under a debug adapter. Performs the full DAP initialization lifecycle and returns the session ID and initial stopped state.", {
352
+ server.tool("debug_launch", "Launch an interactive debug session. With a breakpoint or stopOnEntry, waits by default and reports whether execution stopped, terminated, or timed out.", {
165
353
  adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
166
- program: z.string().describe("Path to the program to debug"),
354
+ program: z.string().optional().describe("Path to the program to debug; may instead be supplied through launchConfig"),
167
355
  args: z.array(z.string()).optional().describe("Program arguments"),
168
356
  cwd: z.string().optional().describe("Working directory for the program"),
169
357
  env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
358
+ launchConfig: z
359
+ .record(z.string(), z.unknown())
360
+ .optional()
361
+ .describe("Adapter-specific DAP launch properties; explicit common fields take precedence"),
170
362
  stopOnEntry: z
171
363
  .boolean()
172
364
  .optional()
@@ -185,6 +377,11 @@ export function createServer() {
185
377
  .array(z.string())
186
378
  .optional()
187
379
  .describe("Exception filter IDs (e.g. ['uncaughtExceptions'])"),
380
+ waitForStop: z
381
+ .boolean()
382
+ .optional()
383
+ .describe("Wait for stopped/terminated outcome; defaults to true when a stop is expected"),
384
+ timeout: z.number().positive().optional().describe("Wait timeout in milliseconds (default: 30000)"),
188
385
  adapterConfig: z
189
386
  .object({
190
387
  type: z.string(),
@@ -198,11 +395,14 @@ export function createServer() {
198
395
  })
199
396
  .optional()
200
397
  .describe("Override adapter config inline instead of looking up from registry"),
201
- }, async ({ adapter, program, args, cwd, env, stopOnEntry, breakpoints, exceptionBreakpoints, adapterConfig, }) => {
202
- // Default: if breakpoints are provided, run to the first one; otherwise stop at entry.
203
- const shouldStop = stopOnEntry ?? !(breakpoints !== undefined && breakpoints.length > 0);
398
+ }, async ({ adapter, program, args, cwd, env, launchConfig, stopOnEntry, breakpoints, exceptionBreakpoints, waitForStop, timeout, adapterConfig, }) => {
399
+ // Default: run to a configured breakpoint/exception; otherwise stop at entry.
400
+ const expectsConfiguredStop = (breakpoints !== undefined && breakpoints.length > 0) ||
401
+ (exceptionBreakpoints !== undefined && exceptionBreakpoints.length > 0);
402
+ const shouldStop = stopOnEntry ?? !expectsConfiguredStop;
204
403
  let session;
205
404
  try {
405
+ await registryReady;
206
406
  const config = adapterConfig ??
207
407
  registry.get(adapter);
208
408
  if (!config) {
@@ -224,9 +424,11 @@ export function createServer() {
224
424
  // Build launch request
225
425
  const launchArgs = {
226
426
  ...config.launchDefaults,
227
- program,
427
+ ...launchConfig,
228
428
  stopOnEntry: shouldStop,
229
429
  };
430
+ if (program !== undefined)
431
+ launchArgs.program = program;
230
432
  if (args !== undefined)
231
433
  launchArgs.args = args;
232
434
  if (cwd !== undefined)
@@ -253,6 +455,7 @@ export function createServer() {
253
455
  // launch — that's why we fire launch first and wait for initialized here.
254
456
  await session.waitForInitialized(10000);
255
457
  // Set breakpoints grouped by file
458
+ const breakpointResults = [];
256
459
  if (breakpoints && breakpoints.length > 0) {
257
460
  const byFile = new Map();
258
461
  for (const bp of breakpoints) {
@@ -261,12 +464,13 @@ export function createServer() {
261
464
  byFile.set(bp.file, existing);
262
465
  }
263
466
  for (const [file, bps] of byFile) {
264
- await session.setBreakpoints(file, bps.map((b) => ({
467
+ const results = await session.setBreakpoints(file, bps.map((b) => ({
265
468
  line: b.line,
266
469
  condition: b.condition,
267
470
  hitCondition: b.hitCondition,
268
471
  logMessage: b.logMessage,
269
472
  })));
473
+ breakpointResults.push({ file, breakpoints: results });
270
474
  }
271
475
  }
272
476
  // Set exception breakpoints
@@ -277,14 +481,23 @@ export function createServer() {
277
481
  await session.configurationDone();
278
482
  // Now await the launch response
279
483
  await launchPromise;
484
+ const shouldWait = waitForStop ?? (shouldStop || expectsConfiguredStop);
280
485
  let stateData;
281
- if (shouldStop) {
282
- try {
283
- await session.waitForStopped(10000);
486
+ if (shouldWait) {
487
+ const startedAt = Date.now();
488
+ const outcome = await session.waitForStopOrTermination(timeout ?? 30000);
489
+ if (outcome.status === "stopped") {
284
490
  stateData = (await session.collectState());
285
491
  }
286
- catch {
287
- stateData = { status: session.state };
492
+ else if (outcome.status === "terminated") {
493
+ const { output } = session.getOutput();
494
+ stateData = {
495
+ status: "terminated",
496
+ output: output.map((entry) => entry.text),
497
+ };
498
+ }
499
+ else {
500
+ stateData = { status: session.state, timedOut: true, elapsedMs: Date.now() - startedAt };
288
501
  }
289
502
  }
290
503
  else {
@@ -294,7 +507,7 @@ export function createServer() {
294
507
  content: [
295
508
  {
296
509
  type: "text",
297
- text: JSON.stringify({ sessionId: id, ...stateData }, null, 2),
510
+ text: JSON.stringify({ sessionId: id, breakpoints: breakpointResults, ...stateData }, null, 2),
298
511
  },
299
512
  ],
300
513
  };
@@ -335,6 +548,7 @@ export function createServer() {
335
548
  }, async ({ adapter, port, host, processId, attachConfig, adapterConfig }) => {
336
549
  let session;
337
550
  try {
551
+ await registryReady;
338
552
  const config = adapterConfig ?? registry.get(adapter);
339
553
  if (!config) {
340
554
  return {
@@ -459,6 +673,11 @@ export function createServer() {
459
673
  .number()
460
674
  .optional()
461
675
  .describe("If running, wait up to this many milliseconds for the program to stop (default: 0 = don't wait)"),
676
+ }, {
677
+ readOnlyHint: true,
678
+ destructiveHint: false,
679
+ idempotentHint: true,
680
+ openWorldHint: false,
462
681
  }, async ({ sessionId, threadId, depth, variableDepth, timeout }) => {
463
682
  try {
464
683
  const session = sessions.resolve(sessionId);
@@ -470,16 +689,13 @@ export function createServer() {
470
689
  };
471
690
  }
472
691
  if (waitMs > 0 && session.state === "running") {
473
- try {
474
- await session.waitForStopped(waitMs);
692
+ const outcome = await session.waitForStopOrTermination(waitMs);
693
+ if (outcome.status === "stopped") {
475
694
  const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
476
695
  return {
477
696
  content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
478
697
  };
479
698
  }
480
- catch {
481
- // Timed out — fall through to return running status
482
- }
483
699
  }
484
700
  const { output } = session.getOutput();
485
701
  return {
@@ -580,13 +796,10 @@ export function createServer() {
580
796
  await session.pause(threadId);
581
797
  break;
582
798
  }
583
- try {
584
- await session.waitForStopped(waitMs);
585
- }
586
- catch (err) {
587
- // Timeout or terminated
799
+ const outcome = await session.waitForStopOrTermination(waitMs);
800
+ if (outcome.status !== "stopped") {
588
801
  const elapsed = Date.now() - startTime;
589
- if (session.state === "terminated") {
802
+ if (outcome.status === "terminated") {
590
803
  const { output } = session.getOutput();
591
804
  return {
592
805
  content: [
@@ -606,17 +819,6 @@ export function createServer() {
606
819
  ],
607
820
  };
608
821
  }
609
- if (session.state === "terminated") {
610
- const { output } = session.getOutput();
611
- return {
612
- content: [
613
- {
614
- type: "text",
615
- text: JSON.stringify({ status: "terminated", output: output.map((e) => e.text) }, null, 2),
616
- },
617
- ],
618
- };
619
- }
620
822
  const state = await session.collectState(threadId);
621
823
  return {
622
824
  content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
@@ -643,6 +845,11 @@ export function createServer() {
643
845
  .number()
644
846
  .optional()
645
847
  .describe("Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."),
848
+ }, {
849
+ readOnlyHint: false,
850
+ destructiveHint: true,
851
+ idempotentHint: false,
852
+ openWorldHint: false,
646
853
  }, async ({ sessionId, category, clear, since }) => {
647
854
  try {
648
855
  const session = sessions.resolve(sessionId);
@@ -746,14 +953,18 @@ export function createServer() {
746
953
  await newSession.configurationDone();
747
954
  await launchPromise;
748
955
  const stopOnEntry = launchConfig.stopOnEntry !== false;
956
+ const shouldWait = stopOnEntry || savedBreakpoints.size > 0;
749
957
  let stateData;
750
- if (stopOnEntry) {
751
- try {
752
- await newSession.waitForStopped(10000);
958
+ if (shouldWait) {
959
+ const outcome = await newSession.waitForStopOrTermination(30000);
960
+ if (outcome.status === "stopped") {
753
961
  stateData = (await newSession.collectState());
754
962
  }
755
- catch {
756
- stateData = { status: newSession.state };
963
+ else {
964
+ stateData = {
965
+ status: outcome.status === "timeout" ? newSession.state : "terminated",
966
+ timedOut: outcome.status === "timeout",
967
+ };
757
968
  }
758
969
  }
759
970
  else {
@@ -804,6 +1015,10 @@ export function createServer() {
804
1015
  .number()
805
1016
  .optional()
806
1017
  .describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
1018
+ launchConfig: z
1019
+ .record(z.string(), z.unknown())
1020
+ .optional()
1021
+ .describe("Adapter-specific DAP launch fields"),
807
1022
  adapterConfig: z
808
1023
  .object({
809
1024
  type: z.string(),
@@ -819,6 +1034,7 @@ export function createServer() {
819
1034
  .describe("Override adapter config inline"),
820
1035
  }, async (opts) => {
821
1036
  try {
1037
+ await registryReady;
822
1038
  const result = await runDebugInspect(registry, sessions, opts);
823
1039
  return {
824
1040
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -836,6 +1052,11 @@ export function createServer() {
836
1052
  // ────────────────────────────────────────────────────────────────────────────
837
1053
  server.tool("list_breakpoints", "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.", {
838
1054
  sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
1055
+ }, {
1056
+ readOnlyHint: true,
1057
+ destructiveHint: false,
1058
+ idempotentHint: true,
1059
+ openWorldHint: false,
839
1060
  }, async ({ sessionId }) => {
840
1061
  try {
841
1062
  const session = sessions.resolve(sessionId);
@@ -897,6 +1118,7 @@ export function createServer() {
897
1118
  .describe("Override adapter config inline"),
898
1119
  }, async (opts) => {
899
1120
  try {
1121
+ await registryReady;
900
1122
  const result = await runDebugRun(registry, sessions, opts);
901
1123
  return {
902
1124
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],