dap-mcp-server 0.1.11 → 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 (63) hide show
  1. package/README.md +25 -7
  2. package/SKILL.md +37 -186
  3. package/build/adapters/builtin.js +27 -3
  4. package/build/adapters/builtin.js.map +1 -1
  5. package/build/adapters/detect.d.ts +12 -0
  6. package/build/adapters/detect.js +91 -0
  7. package/build/adapters/detect.js.map +1 -1
  8. package/build/adapters/registry.js +35 -12
  9. package/build/adapters/registry.js.map +1 -1
  10. package/build/dap/dap-client.d.ts +71 -4
  11. package/build/dap/dap-client.js +324 -53
  12. package/build/dap/dap-client.js.map +1 -1
  13. package/build/dap/types.d.ts +17 -0
  14. package/build/diagnostics/doctor.d.ts +64 -0
  15. package/build/diagnostics/doctor.js +209 -0
  16. package/build/diagnostics/doctor.js.map +1 -0
  17. package/build/server.js +337 -83
  18. package/build/server.js.map +1 -1
  19. package/build/session/inspect.d.ts +6 -2
  20. package/build/session/inspect.js +38 -9
  21. package/build/session/inspect.js.map +1 -1
  22. package/build/session/run.js +10 -9
  23. package/build/session/run.js.map +1 -1
  24. package/build/session/session.d.ts +16 -0
  25. package/build/session/session.js +73 -10
  26. package/build/session/session.js.map +1 -1
  27. package/integrations/claude/dap-debugging/.claude-plugin/plugin.json +9 -0
  28. package/integrations/claude/dap-debugging/.mcp.json +8 -0
  29. package/integrations/claude/dap-debugging/skills/dap-debugging/SKILL.md +49 -0
  30. package/package.json +8 -3
  31. package/.forgejo/workflows/publish.yml +0 -63
  32. package/CLAUDE.md +0 -71
  33. package/TODO.md +0 -32
  34. package/docs/plans/2026-03-05-dap-wrapper-mcp-design.md +0 -326
  35. package/docs/plans/2026-03-05-dap-wrapper-mcp-implementation.md +0 -2815
  36. package/src/adapters/builtin.ts +0 -50
  37. package/src/adapters/detect.test.ts +0 -36
  38. package/src/adapters/detect.ts +0 -36
  39. package/src/adapters/registry.test.ts +0 -73
  40. package/src/adapters/registry.ts +0 -86
  41. package/src/dap/dap-client.test.ts +0 -87
  42. package/src/dap/dap-client.ts +0 -238
  43. package/src/dap/protocol.test.ts +0 -82
  44. package/src/dap/protocol.ts +0 -78
  45. package/src/dap/ring-buffer.test.ts +0 -82
  46. package/src/dap/ring-buffer.ts +0 -64
  47. package/src/dap/types.ts +0 -98
  48. package/src/index.ts +0 -30
  49. package/src/integration/debug-lifecycle.test.ts +0 -131
  50. package/src/server.ts +0 -1062
  51. package/src/session/inspect.test.ts +0 -126
  52. package/src/session/inspect.ts +0 -147
  53. package/src/session/run.test.ts +0 -74
  54. package/src/session/run.ts +0 -136
  55. package/src/session/session-manager.test.ts +0 -74
  56. package/src/session/session-manager.ts +0 -67
  57. package/src/session/session.test.ts +0 -126
  58. package/src/session/session.ts +0 -474
  59. package/src/test-fixtures/crash-adapter.js +0 -23
  60. package/src/test-fixtures/mock-adapter.js +0 -164
  61. package/src/test-fixtures/no-stop-adapter.js +0 -77
  62. package/src/test-fixtures/silent-adapter.js +0 -24
  63. 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(),
@@ -193,14 +390,19 @@ export function createServer() {
193
390
  args: z.array(z.string()).optional(),
194
391
  launchDefaults: z.record(z.string(), z.unknown()).optional(),
195
392
  installHint: z.string().optional(),
393
+ transport: z.enum(["stdio", "tcp"]).optional(),
394
+ portMatcher: z.string().optional(),
196
395
  })
197
396
  .optional()
198
397
  .describe("Override adapter config inline instead of looking up from registry"),
199
- }, async ({ adapter, program, args, cwd, env, stopOnEntry, breakpoints, exceptionBreakpoints, adapterConfig, }) => {
200
- // Default: if breakpoints are provided, run to the first one; otherwise stop at entry.
201
- 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;
202
403
  let session;
203
404
  try {
405
+ await registryReady;
204
406
  const config = adapterConfig ??
205
407
  registry.get(adapter);
206
408
  if (!config) {
@@ -222,19 +424,38 @@ export function createServer() {
222
424
  // Build launch request
223
425
  const launchArgs = {
224
426
  ...config.launchDefaults,
225
- program,
427
+ ...launchConfig,
226
428
  stopOnEntry: shouldStop,
227
429
  };
430
+ if (program !== undefined)
431
+ launchArgs.program = program;
228
432
  if (args !== undefined)
229
433
  launchArgs.args = args;
230
434
  if (cwd !== undefined)
231
435
  launchArgs.cwd = cwd;
232
436
  if (env !== undefined)
233
437
  launchArgs.env = env;
438
+ // Adapter-specific arg normalization. delve's `cwd` is the debuggee's
439
+ // working directory; the *build* (go build) runs from delve's own
440
+ // process cwd, controlled by `dlvCwd`. Mirror cwd → dlvCwd so a
441
+ // user-supplied cwd is found by `go build` looking for go.mod.
442
+ if (config.type === "go" && cwd !== undefined && launchArgs.dlvCwd === undefined) {
443
+ launchArgs.dlvCwd = cwd;
444
+ }
234
445
  // Send launch first (don't await — real adapters like debugpy block
235
446
  // the launch response until configurationDone arrives)
236
447
  const launchPromise = session.launch(launchArgs);
448
+ // Suppress unhandled rejection: the real `await launchPromise` below
449
+ // will still throw if launch fails. Without this, an early launch
450
+ // rejection (e.g. adapter rejects the request) crashes the MCP server.
451
+ launchPromise.catch(() => { });
452
+ // Wait for the adapter to emit the "initialized" event before sending
453
+ // configuration requests. Some adapters (delve) emit it right after the
454
+ // initialize response; others (debugpy) only emit it after receiving
455
+ // launch — that's why we fire launch first and wait for initialized here.
456
+ await session.waitForInitialized(10000);
237
457
  // Set breakpoints grouped by file
458
+ const breakpointResults = [];
238
459
  if (breakpoints && breakpoints.length > 0) {
239
460
  const byFile = new Map();
240
461
  for (const bp of breakpoints) {
@@ -243,12 +464,13 @@ export function createServer() {
243
464
  byFile.set(bp.file, existing);
244
465
  }
245
466
  for (const [file, bps] of byFile) {
246
- await session.setBreakpoints(file, bps.map((b) => ({
467
+ const results = await session.setBreakpoints(file, bps.map((b) => ({
247
468
  line: b.line,
248
469
  condition: b.condition,
249
470
  hitCondition: b.hitCondition,
250
471
  logMessage: b.logMessage,
251
472
  })));
473
+ breakpointResults.push({ file, breakpoints: results });
252
474
  }
253
475
  }
254
476
  // Set exception breakpoints
@@ -259,14 +481,23 @@ export function createServer() {
259
481
  await session.configurationDone();
260
482
  // Now await the launch response
261
483
  await launchPromise;
484
+ const shouldWait = waitForStop ?? (shouldStop || expectsConfiguredStop);
262
485
  let stateData;
263
- if (shouldStop) {
264
- try {
265
- 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") {
266
490
  stateData = (await session.collectState());
267
491
  }
268
- catch {
269
- 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 };
270
501
  }
271
502
  }
272
503
  else {
@@ -276,7 +507,7 @@ export function createServer() {
276
507
  content: [
277
508
  {
278
509
  type: "text",
279
- text: JSON.stringify({ sessionId: id, ...stateData }, null, 2),
510
+ text: JSON.stringify({ sessionId: id, breakpoints: breakpointResults, ...stateData }, null, 2),
280
511
  },
281
512
  ],
282
513
  };
@@ -309,12 +540,15 @@ export function createServer() {
309
540
  args: z.array(z.string()).optional(),
310
541
  launchDefaults: z.record(z.string(), z.unknown()).optional(),
311
542
  installHint: z.string().optional(),
543
+ transport: z.enum(["stdio", "tcp"]).optional(),
544
+ portMatcher: z.string().optional(),
312
545
  })
313
546
  .optional()
314
547
  .describe("Override adapter config inline"),
315
548
  }, async ({ adapter, port, host, processId, attachConfig, adapterConfig }) => {
316
549
  let session;
317
550
  try {
551
+ await registryReady;
318
552
  const config = adapterConfig ?? registry.get(adapter);
319
553
  if (!config) {
320
554
  return {
@@ -344,6 +578,10 @@ export function createServer() {
344
578
  attachArgs.processId = processId;
345
579
  // Send attach first (don't await — real adapters block until configurationDone)
346
580
  const attachPromise = session.attach(attachArgs);
581
+ // Suppress unhandled rejection: the real `await attachPromise` below
582
+ // will still throw if attach fails.
583
+ attachPromise.catch(() => { });
584
+ await session.waitForInitialized(10000);
347
585
  await session.configurationDone();
348
586
  await attachPromise;
349
587
  return {
@@ -435,6 +673,11 @@ export function createServer() {
435
673
  .number()
436
674
  .optional()
437
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,
438
681
  }, async ({ sessionId, threadId, depth, variableDepth, timeout }) => {
439
682
  try {
440
683
  const session = sessions.resolve(sessionId);
@@ -446,16 +689,13 @@ export function createServer() {
446
689
  };
447
690
  }
448
691
  if (waitMs > 0 && session.state === "running") {
449
- try {
450
- await session.waitForStopped(waitMs);
692
+ const outcome = await session.waitForStopOrTermination(waitMs);
693
+ if (outcome.status === "stopped") {
451
694
  const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
452
695
  return {
453
696
  content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
454
697
  };
455
698
  }
456
- catch {
457
- // Timed out — fall through to return running status
458
- }
459
699
  }
460
700
  const { output } = session.getOutput();
461
701
  return {
@@ -556,13 +796,10 @@ export function createServer() {
556
796
  await session.pause(threadId);
557
797
  break;
558
798
  }
559
- try {
560
- await session.waitForStopped(waitMs);
561
- }
562
- catch (err) {
563
- // Timeout or terminated
799
+ const outcome = await session.waitForStopOrTermination(waitMs);
800
+ if (outcome.status !== "stopped") {
564
801
  const elapsed = Date.now() - startTime;
565
- if (session.state === "terminated") {
802
+ if (outcome.status === "terminated") {
566
803
  const { output } = session.getOutput();
567
804
  return {
568
805
  content: [
@@ -582,17 +819,6 @@ export function createServer() {
582
819
  ],
583
820
  };
584
821
  }
585
- if (session.state === "terminated") {
586
- const { output } = session.getOutput();
587
- return {
588
- content: [
589
- {
590
- type: "text",
591
- text: JSON.stringify({ status: "terminated", output: output.map((e) => e.text) }, null, 2),
592
- },
593
- ],
594
- };
595
- }
596
822
  const state = await session.collectState(threadId);
597
823
  return {
598
824
  content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
@@ -619,6 +845,11 @@ export function createServer() {
619
845
  .number()
620
846
  .optional()
621
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,
622
853
  }, async ({ sessionId, category, clear, since }) => {
623
854
  try {
624
855
  const session = sessions.resolve(sessionId);
@@ -711,6 +942,10 @@ export function createServer() {
711
942
  await newSession.initialize();
712
943
  // Send launch first (don't await — real adapters block until configurationDone)
713
944
  const launchPromise = newSession.launch(launchConfig);
945
+ // Suppress unhandled rejection: the real `await launchPromise` below
946
+ // will still throw if launch fails.
947
+ launchPromise.catch(() => { });
948
+ await newSession.waitForInitialized(10000);
714
949
  // Re-apply saved breakpoints
715
950
  for (const [file, bps] of savedBreakpoints) {
716
951
  await newSession.setBreakpoints(file, bps);
@@ -718,14 +953,18 @@ export function createServer() {
718
953
  await newSession.configurationDone();
719
954
  await launchPromise;
720
955
  const stopOnEntry = launchConfig.stopOnEntry !== false;
956
+ const shouldWait = stopOnEntry || savedBreakpoints.size > 0;
721
957
  let stateData;
722
- if (stopOnEntry) {
723
- try {
724
- await newSession.waitForStopped(10000);
958
+ if (shouldWait) {
959
+ const outcome = await newSession.waitForStopOrTermination(30000);
960
+ if (outcome.status === "stopped") {
725
961
  stateData = (await newSession.collectState());
726
962
  }
727
- catch {
728
- stateData = { status: newSession.state };
963
+ else {
964
+ stateData = {
965
+ status: outcome.status === "timeout" ? newSession.state : "terminated",
966
+ timedOut: outcome.status === "timeout",
967
+ };
729
968
  }
730
969
  }
731
970
  else {
@@ -776,6 +1015,10 @@ export function createServer() {
776
1015
  .number()
777
1016
  .optional()
778
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"),
779
1022
  adapterConfig: z
780
1023
  .object({
781
1024
  type: z.string(),
@@ -784,11 +1027,14 @@ export function createServer() {
784
1027
  args: z.array(z.string()).optional(),
785
1028
  launchDefaults: z.record(z.string(), z.unknown()).optional(),
786
1029
  installHint: z.string().optional(),
1030
+ transport: z.enum(["stdio", "tcp"]).optional(),
1031
+ portMatcher: z.string().optional(),
787
1032
  })
788
1033
  .optional()
789
1034
  .describe("Override adapter config inline"),
790
1035
  }, async (opts) => {
791
1036
  try {
1037
+ await registryReady;
792
1038
  const result = await runDebugInspect(registry, sessions, opts);
793
1039
  return {
794
1040
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -806,6 +1052,11 @@ export function createServer() {
806
1052
  // ────────────────────────────────────────────────────────────────────────────
807
1053
  server.tool("list_breakpoints", "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.", {
808
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,
809
1060
  }, async ({ sessionId }) => {
810
1061
  try {
811
1062
  const session = sessions.resolve(sessionId);
@@ -860,11 +1111,14 @@ export function createServer() {
860
1111
  args: z.array(z.string()).optional(),
861
1112
  launchDefaults: z.record(z.string(), z.unknown()).optional(),
862
1113
  installHint: z.string().optional(),
1114
+ transport: z.enum(["stdio", "tcp"]).optional(),
1115
+ portMatcher: z.string().optional(),
863
1116
  })
864
1117
  .optional()
865
1118
  .describe("Override adapter config inline"),
866
1119
  }, async (opts) => {
867
1120
  try {
1121
+ await registryReady;
868
1122
  const result = await runDebugRun(registry, sessions, opts);
869
1123
  return {
870
1124
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],