dap-mcp-server 0.1.12 → 0.1.17

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 (67) hide show
  1. package/README.md +50 -10
  2. package/SKILL.md +39 -184
  3. package/build/adapters/builtin.js +25 -0
  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/recipes.d.ts +46 -0
  9. package/build/adapters/recipes.js +128 -0
  10. package/build/adapters/recipes.js.map +1 -0
  11. package/build/adapters/registry.js +35 -12
  12. package/build/adapters/registry.js.map +1 -1
  13. package/build/dap/types.d.ts +20 -0
  14. package/build/diagnostics/doctor.d.ts +70 -0
  15. package/build/diagnostics/doctor.js +268 -0
  16. package/build/diagnostics/doctor.js.map +1 -0
  17. package/build/server.js +432 -161
  18. package/build/server.js.map +1 -1
  19. package/build/session/inspect.d.ts +12 -2
  20. package/build/session/inspect.js +33 -20
  21. package/build/session/inspect.js.map +1 -1
  22. package/build/session/run.d.ts +2 -0
  23. package/build/session/run.js +13 -23
  24. package/build/session/run.js.map +1 -1
  25. package/build/session/session.d.ts +38 -2
  26. package/build/session/session.js +158 -12
  27. package/build/session/session.js.map +1 -1
  28. package/{src/test-fixtures/crash-adapter.js → build/session/state-format.d.ts} +11 -2
  29. package/build/session/state-format.js +62 -0
  30. package/build/session/state-format.js.map +1 -0
  31. package/integrations/claude/dap-debugging/.claude-plugin/plugin.json +9 -0
  32. package/integrations/claude/dap-debugging/.mcp.json +8 -0
  33. package/integrations/claude/dap-debugging/skills/dap-debugging/SKILL.md +51 -0
  34. package/package.json +8 -3
  35. package/.forgejo/workflows/publish.yml +0 -63
  36. package/CLAUDE.md +0 -71
  37. package/TODO.md +0 -32
  38. package/docs/plans/2026-03-05-dap-wrapper-mcp-design.md +0 -326
  39. package/docs/plans/2026-03-05-dap-wrapper-mcp-implementation.md +0 -2815
  40. package/src/adapters/builtin.ts +0 -75
  41. package/src/adapters/detect.test.ts +0 -36
  42. package/src/adapters/detect.ts +0 -36
  43. package/src/adapters/registry.test.ts +0 -74
  44. package/src/adapters/registry.ts +0 -86
  45. package/src/dap/dap-client.test.ts +0 -118
  46. package/src/dap/dap-client.ts +0 -588
  47. package/src/dap/protocol.test.ts +0 -82
  48. package/src/dap/protocol.ts +0 -78
  49. package/src/dap/ring-buffer.test.ts +0 -82
  50. package/src/dap/ring-buffer.ts +0 -64
  51. package/src/dap/types.ts +0 -115
  52. package/src/index.ts +0 -30
  53. package/src/integration/debug-lifecycle.test.ts +0 -131
  54. package/src/server.ts +0 -1096
  55. package/src/session/inspect.test.ts +0 -126
  56. package/src/session/inspect.ts +0 -160
  57. package/src/session/run.test.ts +0 -74
  58. package/src/session/run.ts +0 -144
  59. package/src/session/session-manager.test.ts +0 -74
  60. package/src/session/session-manager.ts +0 -67
  61. package/src/session/session.test.ts +0 -126
  62. package/src/session/session.ts +0 -482
  63. package/src/test-fixtures/mock-adapter.js +0 -164
  64. package/src/test-fixtures/no-stop-adapter.js +0 -77
  65. package/src/test-fixtures/silent-adapter.js +0 -24
  66. package/src/test-fixtures/tcp-mock-adapter.js +0 -87
  67. package/tsconfig.json +0 -16
package/build/server.js CHANGED
@@ -30,7 +30,10 @@ 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 { conciseDoctorResult, resolveProjectAdapter, runDebugDoctor, } from "./diagnostics/doctor.js";
35
+ import { buildLaunchArgs, defaultCargoProgram } from "./adapters/recipes.js";
36
+ import { COMPACT_STACK_DEPTH, renderState } from "./session/state-format.js";
34
37
  function readPackageVersion() {
35
38
  try {
36
39
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -47,7 +50,7 @@ export function createServer() {
47
50
  const registry = new AdapterRegistry(globalDir, projectDir);
48
51
  const sessions = new SessionManager();
49
52
  // Load persisted adapter configs (best-effort, don't block server start)
50
- registry.load().catch((err) => {
53
+ const registryReady = registry.load().catch((err) => {
51
54
  console.error("Warning: failed to load adapter configs:", err);
52
55
  });
53
56
  const server = new McpServer({
@@ -55,57 +58,303 @@ export function createServer() {
55
58
  version: readPackageVersion(),
56
59
  }, {
57
60
  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.",
61
+ "Local source-code debugger using the Debug Adapter Protocol.",
62
+ "Use it when a test fails, a program crashes, runtime state is unclear, or code behavior cannot be explained statically.",
63
+ "Start with debug_doctor to detect the project and verify adapter readiness.",
64
+ "Prefer debug_at for a one-call breakpoint inspection: it selects the adapter, launches, waits, evaluates expressions, and cleans up.",
65
+ "Use debug_run when output or termination is enough. Use debug_launch only for interactive stepping or multiple inspection points.",
66
+ "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.",
67
+ "step answers compactly (frame-local scopes, short stack) and states carry only output not reported before; use get_state for the full picture and get_output for the whole buffer.",
68
+ "To debug an already-running or suspended process, pass breakpoints to debug_attach so they bind before it resumes.",
69
+ "Omit sessionId when exactly one session is active. Adapter-specific DAP fields belong in launchConfig. Tool errors use isError=true.",
92
70
  ].join("\n"),
93
71
  });
72
+ const doctorOutputSchema = z.object({
73
+ cwd: z.string(),
74
+ ready: z.boolean(),
75
+ recommendedAdapter: z.string().optional(),
76
+ detected: z.object({
77
+ languages: z.array(z.string()),
78
+ testFrameworks: z.array(z.string()),
79
+ packageManager: z.string().optional(),
80
+ pythonPath: z.string().optional(),
81
+ launchJson: z.string().optional(),
82
+ }),
83
+ adapters: z.array(z.object({
84
+ type: z.string(),
85
+ source: z.enum(["builtin", "global", "project"]),
86
+ selected: z.boolean(),
87
+ available: z.boolean(),
88
+ command: z.string(),
89
+ reason: z.string().optional(),
90
+ installHint: z.string().optional(),
91
+ runtime: z.string().optional(),
92
+ program: z.string(),
93
+ transport: z.enum(["stdio", "tcp"]),
94
+ })),
95
+ otherAdapters: z.array(z.string()).optional(),
96
+ nextSteps: z.array(z.string()),
97
+ });
98
+ const breakpointResultSchema = z.object({
99
+ id: z.number().optional(),
100
+ verified: z.boolean(),
101
+ line: z.number(),
102
+ message: z.string().optional(),
103
+ });
104
+ const debugAtOutputSchema = z.object({
105
+ adapter: z.string(),
106
+ hit: z.boolean(),
107
+ reason: z.string().optional(),
108
+ hint: z.string().optional(),
109
+ exception: z
110
+ .object({
111
+ id: z.string().optional(),
112
+ description: z.string().optional(),
113
+ breakMode: z.string().optional(),
114
+ })
115
+ .optional(),
116
+ location: z
117
+ .object({
118
+ file: z.string(),
119
+ line: z.number(),
120
+ functionName: z.string().optional(),
121
+ column: z.number().optional(),
122
+ })
123
+ .optional(),
124
+ values: z
125
+ .record(z.string(), z.object({
126
+ result: z.string().optional(),
127
+ type: z.string().optional(),
128
+ error: z.string().optional(),
129
+ }))
130
+ .optional(),
131
+ output: z.array(z.string()).optional(),
132
+ status: z.string().optional(),
133
+ exitCode: z.number().optional(),
134
+ timedOut: z.boolean().optional(),
135
+ breakpoint: breakpointResultSchema.optional(),
136
+ });
137
+ const breakpointInputSchema = z.object({
138
+ file: z.string().describe("Source file path"),
139
+ line: z.number().describe("Line number"),
140
+ condition: z.string().optional(),
141
+ hitCondition: z.string().optional(),
142
+ logMessage: z.string().optional(),
143
+ });
144
+ const detailSchema = z.enum(["compact", "full"]);
145
+ /** Set breakpoints grouped by file; must run between `initialized` and configurationDone. */
146
+ async function applyBreakpoints(session, breakpoints = []) {
147
+ const byFile = new Map();
148
+ for (const bp of breakpoints) {
149
+ const existing = byFile.get(bp.file) ?? [];
150
+ existing.push(bp);
151
+ byFile.set(bp.file, existing);
152
+ }
153
+ const results = [];
154
+ for (const [file, bps] of byFile) {
155
+ const fileResults = await session.setBreakpoints(file, bps.map(({ line, condition, hitCondition, logMessage }) => ({
156
+ line,
157
+ condition,
158
+ hitCondition,
159
+ logMessage,
160
+ })));
161
+ results.push({ file, breakpoints: fileResults });
162
+ }
163
+ return results;
164
+ }
165
+ /** What to report once the program is no longer running under our control. */
166
+ async function terminatedReport(session) {
167
+ return {
168
+ status: "terminated",
169
+ exitCode: await session.waitForExitCode(),
170
+ ...session.unreportedOutput(true),
171
+ };
172
+ }
173
+ /** Wait for the first stop after launch/attach/restart and describe the outcome. */
174
+ async function awaitFirstStop(session, timeout) {
175
+ const startedAt = Date.now();
176
+ const outcome = await session.waitForStopOrTermination(timeout);
177
+ if (outcome.status === "stopped") {
178
+ const state = await session.collectState(undefined, 10, 1, { consumeOutput: true });
179
+ return state;
180
+ }
181
+ if (outcome.status === "terminated")
182
+ return terminatedReport(session);
183
+ return { status: session.state, timedOut: true, elapsedMs: Date.now() - startedAt };
184
+ }
185
+ // ────────────────────────────────────────────────────────────────────────────
186
+ // High-level LLM entry points
187
+ // ────────────────────────────────────────────────────────────────────────────
188
+ server.registerTool("debug_doctor", {
189
+ title: "Check debugger readiness",
190
+ 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.",
191
+ inputSchema: z.object({
192
+ cwd: z.string().optional().describe("Project directory (default: server working directory)"),
193
+ adapter: z.string().optional().describe("Adapter to validate instead of auto-detecting one"),
194
+ verbose: z
195
+ .boolean()
196
+ .optional()
197
+ .describe("Include full details for every known adapter, not just the selected one"),
198
+ }),
199
+ outputSchema: doctorOutputSchema,
200
+ annotations: {
201
+ readOnlyHint: true,
202
+ destructiveHint: false,
203
+ idempotentHint: true,
204
+ openWorldHint: false,
205
+ },
206
+ }, async (options) => {
207
+ try {
208
+ await registryReady;
209
+ const result = await runDebugDoctor(registry, options);
210
+ return {
211
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
212
+ structuredContent: { ...result },
213
+ };
214
+ }
215
+ catch (err) {
216
+ return {
217
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
218
+ isError: true,
219
+ };
220
+ }
221
+ });
222
+ server.registerTool("debug_at", {
223
+ title: "Inspect runtime state at a line",
224
+ 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.",
225
+ inputSchema: z.object({
226
+ cwd: z.string().optional().describe("Project and debuggee working directory"),
227
+ adapter: z.string().optional().describe("Adapter type; auto-detected from file/project when omitted"),
228
+ program: z
229
+ .string()
230
+ .optional()
231
+ .describe("Launch target; defaults to file for Python/Node and cwd for Go"),
232
+ args: z.array(z.string()).optional().describe("Program arguments"),
233
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
234
+ file: z.string().describe("Source file containing the breakpoint"),
235
+ line: z.number().int().positive().describe("One-based breakpoint line"),
236
+ condition: z.string().optional().describe("Conditional breakpoint expression"),
237
+ hitCondition: z.string().optional().describe("Adapter-specific hit count condition"),
238
+ expressions: z.array(z.string()).optional().describe("Expressions to evaluate in the stopped frame"),
239
+ timeout: z.number().positive().optional().describe("Breakpoint wait timeout in milliseconds"),
240
+ launchConfig: z
241
+ .record(z.string(), z.unknown())
242
+ .optional()
243
+ .describe("Adapter-specific DAP launch fields"),
244
+ }),
245
+ outputSchema: debugAtOutputSchema,
246
+ annotations: {
247
+ readOnlyHint: false,
248
+ destructiveHint: true,
249
+ idempotentHint: false,
250
+ openWorldHint: false,
251
+ },
252
+ }, async (options) => {
253
+ try {
254
+ await registryReady;
255
+ const cwd = path.resolve(options.cwd ?? process.cwd());
256
+ const sourceFile = path.resolve(cwd, options.file);
257
+ const resolved = await resolveProjectAdapter(registry, {
258
+ cwd,
259
+ adapter: options.adapter,
260
+ sourceFile,
261
+ });
262
+ const adapter = resolved.result.recommendedAdapter;
263
+ if (!adapter || !resolved.config || !resolved.result.ready) {
264
+ const { recommendedAdapter, detected, adapters, otherAdapters, nextSteps } = conciseDoctorResult(resolved.result);
265
+ const summary = {
266
+ recommendedAdapter,
267
+ languages: detected.languages,
268
+ reason: adapters[0]?.reason,
269
+ nextSteps,
270
+ otherAdapters,
271
+ };
272
+ return {
273
+ content: [
274
+ {
275
+ type: "text",
276
+ text: `Debugger is not ready: ${JSON.stringify(summary)}\nRun debug_doctor with verbose=true for full adapter details.`,
277
+ },
278
+ ],
279
+ isError: true,
280
+ };
281
+ }
282
+ const hasLaunchTarget = options.launchConfig !== undefined &&
283
+ ["program", "module", "code"].some((key) => options.launchConfig?.[key] !== undefined);
284
+ const compiledProgram = !options.program && !hasLaunchTarget && adapter === "cppdbg"
285
+ ? await defaultCargoProgram(cwd)
286
+ : undefined;
287
+ if (!options.program && !hasLaunchTarget && adapter === "cppdbg" && !compiledProgram) {
288
+ return {
289
+ content: [
290
+ {
291
+ type: "text",
292
+ text: "Error: C/C++/Rust debugging requires the compiled executable in program or launchConfig.program. For a Cargo project, run `cargo build` first and program defaults to target/debug/<package name>.",
293
+ },
294
+ ],
295
+ isError: true,
296
+ };
297
+ }
298
+ const program = options.program ??
299
+ compiledProgram ??
300
+ (hasLaunchTarget ? undefined : adapter === "go" ? cwd : sourceFile);
301
+ const result = await runDebugInspect(registry, sessions, {
302
+ adapter,
303
+ adapterConfig: resolved.config,
304
+ program,
305
+ args: options.args,
306
+ cwd,
307
+ env: options.env,
308
+ at: {
309
+ file: sourceFile,
310
+ line: options.line,
311
+ condition: options.condition,
312
+ hitCondition: options.hitCondition,
313
+ },
314
+ expressions: options.expressions,
315
+ timeout: options.timeout,
316
+ launchConfig: options.launchConfig,
317
+ });
318
+ const structuredContent = { adapter, ...result };
319
+ return {
320
+ content: [
321
+ { type: "text", text: JSON.stringify(structuredContent, null, 2) },
322
+ ],
323
+ structuredContent,
324
+ };
325
+ }
326
+ catch (err) {
327
+ return {
328
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
329
+ isError: true,
330
+ };
331
+ }
332
+ });
94
333
  // ────────────────────────────────────────────────────────────────────────────
95
334
  // 1. list_adapters
96
335
  // ────────────────────────────────────────────────────────────────────────────
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 () => {
336
+ 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.", {}, {
337
+ readOnlyHint: true,
338
+ destructiveHint: false,
339
+ idempotentHint: true,
340
+ openWorldHint: false,
341
+ }, async () => {
98
342
  try {
343
+ await registryReady;
99
344
  const adapters = registry.listAll();
100
345
  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 };
346
+ const availability = await inspectAdapterAvailability(a);
347
+ return { ...a, ...availability };
104
348
  }));
105
349
  const available = results.filter((r) => r.available);
106
350
  const missing = results
107
351
  .filter((r) => !r.available)
108
- .map((r) => ({ type: r.type, installHint: r.installHint }));
352
+ .map((r) => ({
353
+ type: r.type,
354
+ command: r.command,
355
+ reason: r.reason,
356
+ installHint: r.installHint,
357
+ }));
109
358
  const summary = missing.length === 0
110
359
  ? `${available.length}/${results.length} adapters available`
111
360
  : `${available.length}/${results.length} adapters available; missing: ${missing.map((m) => m.type).join(", ")}`;
@@ -135,10 +384,27 @@ export function createServer() {
135
384
  args: z.array(z.string()).optional().describe("Additional arguments to pass to the adapter"),
136
385
  launchDefaults: z.record(z.string(), z.unknown()).optional().describe("Default launch configuration properties"),
137
386
  installHint: z.string().optional().describe("Hint shown when adapter is not installed"),
387
+ transport: z.enum(["stdio", "tcp"]).optional().describe("DAP transport used by the adapter"),
388
+ portMatcher: z.string().optional().describe("Regex whose first capture group is the TCP port"),
138
389
  scope: z.enum(["global", "project"]).optional().describe("Where to persist the adapter config (default: project)"),
139
- }, async ({ type, program, runtime, args, launchDefaults, installHint, scope }) => {
390
+ }, {
391
+ readOnlyHint: false,
392
+ destructiveHint: true,
393
+ idempotentHint: true,
394
+ openWorldHint: false,
395
+ }, async ({ type, program, runtime, args, launchDefaults, installHint, transport, portMatcher, scope }) => {
140
396
  try {
141
- const config = { type, program, runtime, args, launchDefaults, installHint };
397
+ await registryReady;
398
+ const config = {
399
+ type,
400
+ program,
401
+ runtime,
402
+ args,
403
+ launchDefaults,
404
+ installHint,
405
+ transport,
406
+ portMatcher,
407
+ };
142
408
  const saveScope = scope ?? "project";
143
409
  registry.register(config, saveScope);
144
410
  await registry.save(saveScope);
@@ -161,30 +427,33 @@ export function createServer() {
161
427
  // ────────────────────────────────────────────────────────────────────────────
162
428
  // 3. debug_launch
163
429
  // ────────────────────────────────────────────────────────────────────────────
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.", {
430
+ 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
431
  adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
166
- program: z.string().describe("Path to the program to debug"),
432
+ program: z.string().optional().describe("Path to the program to debug; may instead be supplied through launchConfig"),
167
433
  args: z.array(z.string()).optional().describe("Program arguments"),
168
434
  cwd: z.string().optional().describe("Working directory for the program"),
169
435
  env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
436
+ launchConfig: z
437
+ .record(z.string(), z.unknown())
438
+ .optional()
439
+ .describe("Adapter-specific DAP launch properties; explicit common fields take precedence"),
170
440
  stopOnEntry: z
171
441
  .boolean()
172
442
  .optional()
173
443
  .describe("Stop on program entry. Defaults to false when breakpoints are provided (run to first breakpoint), true otherwise."),
174
444
  breakpoints: z
175
- .array(z.object({
176
- file: z.string().describe("Source file path"),
177
- line: z.number().describe("Line number"),
178
- condition: z.string().optional(),
179
- hitCondition: z.string().optional(),
180
- logMessage: z.string().optional(),
181
- }))
445
+ .array(breakpointInputSchema)
182
446
  .optional()
183
447
  .describe("Breakpoints to set before launching"),
184
448
  exceptionBreakpoints: z
185
449
  .array(z.string())
186
450
  .optional()
187
451
  .describe("Exception filter IDs (e.g. ['uncaughtExceptions'])"),
452
+ waitForStop: z
453
+ .boolean()
454
+ .optional()
455
+ .describe("Wait for stopped/terminated outcome; defaults to true when a stop is expected"),
456
+ timeout: z.number().positive().optional().describe("Wait timeout in milliseconds (default: 30000)"),
188
457
  adapterConfig: z
189
458
  .object({
190
459
  type: z.string(),
@@ -198,11 +467,14 @@ export function createServer() {
198
467
  })
199
468
  .optional()
200
469
  .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);
470
+ }, async ({ adapter, program, args, cwd, env, launchConfig, stopOnEntry, breakpoints, exceptionBreakpoints, waitForStop, timeout, adapterConfig, }) => {
471
+ // Default: run to a configured breakpoint/exception; otherwise stop at entry.
472
+ const expectsConfiguredStop = (breakpoints !== undefined && breakpoints.length > 0) ||
473
+ (exceptionBreakpoints !== undefined && exceptionBreakpoints.length > 0);
474
+ const shouldStop = stopOnEntry ?? !expectsConfiguredStop;
204
475
  let session;
205
476
  try {
477
+ await registryReady;
206
478
  const config = adapterConfig ??
207
479
  registry.get(adapter);
208
480
  if (!config) {
@@ -221,25 +493,15 @@ export function createServer() {
221
493
  sessions.add(session);
222
494
  // Initialize (waits for the initialized event internally)
223
495
  await session.initialize();
224
- // Build launch request
225
- const launchArgs = {
226
- ...config.launchDefaults,
496
+ const launchArgs = await buildLaunchArgs(config, {
227
497
  program,
498
+ args,
499
+ cwd,
500
+ env,
501
+ launchConfig,
228
502
  stopOnEntry: shouldStop,
229
- };
230
- if (args !== undefined)
231
- launchArgs.args = args;
232
- if (cwd !== undefined)
233
- launchArgs.cwd = cwd;
234
- if (env !== undefined)
235
- launchArgs.env = env;
236
- // Adapter-specific arg normalization. delve's `cwd` is the debuggee's
237
- // working directory; the *build* (go build) runs from delve's own
238
- // process cwd, controlled by `dlvCwd`. Mirror cwd → dlvCwd so a
239
- // user-supplied cwd is found by `go build` looking for go.mod.
240
- if (config.type === "go" && cwd !== undefined && launchArgs.dlvCwd === undefined) {
241
- launchArgs.dlvCwd = cwd;
242
- }
503
+ sourceFiles: breakpoints?.map((bp) => bp.file),
504
+ });
243
505
  // Send launch first (don't await — real adapters like debugpy block
244
506
  // the launch response until configurationDone arrives)
245
507
  const launchPromise = session.launch(launchArgs);
@@ -252,23 +514,7 @@ export function createServer() {
252
514
  // initialize response; others (debugpy) only emit it after receiving
253
515
  // launch — that's why we fire launch first and wait for initialized here.
254
516
  await session.waitForInitialized(10000);
255
- // Set breakpoints grouped by file
256
- if (breakpoints && breakpoints.length > 0) {
257
- const byFile = new Map();
258
- for (const bp of breakpoints) {
259
- const existing = byFile.get(bp.file) ?? [];
260
- existing.push(bp);
261
- byFile.set(bp.file, existing);
262
- }
263
- for (const [file, bps] of byFile) {
264
- await session.setBreakpoints(file, bps.map((b) => ({
265
- line: b.line,
266
- condition: b.condition,
267
- hitCondition: b.hitCondition,
268
- logMessage: b.logMessage,
269
- })));
270
- }
271
- }
517
+ const breakpointResults = await applyBreakpoints(session, breakpoints);
272
518
  // Set exception breakpoints
273
519
  if (exceptionBreakpoints && exceptionBreakpoints.length > 0) {
274
520
  await session.setExceptionBreakpoints(exceptionBreakpoints);
@@ -277,24 +523,15 @@ export function createServer() {
277
523
  await session.configurationDone();
278
524
  // Now await the launch response
279
525
  await launchPromise;
280
- let stateData;
281
- if (shouldStop) {
282
- try {
283
- await session.waitForStopped(10000);
284
- stateData = (await session.collectState());
285
- }
286
- catch {
287
- stateData = { status: session.state };
288
- }
289
- }
290
- else {
291
- stateData = { status: session.state };
292
- }
526
+ const shouldWait = waitForStop ?? (shouldStop || expectsConfiguredStop);
527
+ const stateData = shouldWait
528
+ ? await awaitFirstStop(session, timeout ?? 30000)
529
+ : { status: session.state };
293
530
  return {
294
531
  content: [
295
532
  {
296
533
  type: "text",
297
- text: JSON.stringify({ sessionId: id, ...stateData }, null, 2),
534
+ text: JSON.stringify({ sessionId: id, breakpoints: breakpointResults, ...stateData }, null, 2),
298
535
  },
299
536
  ],
300
537
  };
@@ -313,12 +550,25 @@ export function createServer() {
313
550
  // ────────────────────────────────────────────────────────────────────────────
314
551
  // 4. debug_attach
315
552
  // ────────────────────────────────────────────────────────────────────────────
316
- server.tool("debug_attach", "Attach a debug adapter to an already-running process.", {
553
+ server.tool("debug_attach", "Attach a debug adapter to an already-running process. Pass breakpoints here rather than afterwards: they bind before the debuggee resumes, which is the only reliable way to stop a process started suspended (e.g. JDWP suspend=y). With breakpoints or exception breakpoints, waits and reports stopped, terminated, or timeout.", {
317
554
  adapter: z.string().describe("Adapter type to use"),
318
555
  port: z.number().optional().describe("Debug port to attach to"),
319
556
  host: z.string().optional().describe("Host to attach to (default: localhost)"),
320
557
  processId: z.number().optional().describe("PID of process to attach to"),
321
558
  attachConfig: z.record(z.string(), z.unknown()).optional().describe("Arbitrary attach configuration properties"),
559
+ breakpoints: z
560
+ .array(breakpointInputSchema)
561
+ .optional()
562
+ .describe("Breakpoints to bind before the debuggee resumes"),
563
+ exceptionBreakpoints: z
564
+ .array(z.string())
565
+ .optional()
566
+ .describe("Exception filter IDs (e.g. ['uncaughtExceptions'])"),
567
+ waitForStop: z
568
+ .boolean()
569
+ .optional()
570
+ .describe("Wait for stopped/terminated outcome; defaults to true when breakpoints or exception breakpoints are given"),
571
+ timeout: z.number().positive().optional().describe("Wait timeout in milliseconds (default: 30000)"),
322
572
  adapterConfig: z
323
573
  .object({
324
574
  type: z.string(),
@@ -332,9 +582,12 @@ export function createServer() {
332
582
  })
333
583
  .optional()
334
584
  .describe("Override adapter config inline"),
335
- }, async ({ adapter, port, host, processId, attachConfig, adapterConfig }) => {
585
+ }, async ({ adapter, port, host, processId, attachConfig, breakpoints, exceptionBreakpoints, waitForStop, timeout, adapterConfig, }) => {
586
+ const expectsConfiguredStop = (breakpoints !== undefined && breakpoints.length > 0) ||
587
+ (exceptionBreakpoints !== undefined && exceptionBreakpoints.length > 0);
336
588
  let session;
337
589
  try {
590
+ await registryReady;
338
591
  const config = adapterConfig ?? registry.get(adapter);
339
592
  if (!config) {
340
593
  return {
@@ -368,13 +621,24 @@ export function createServer() {
368
621
  // will still throw if attach fails.
369
622
  attachPromise.catch(() => { });
370
623
  await session.waitForInitialized(10000);
624
+ const breakpointResults = await applyBreakpoints(session, breakpoints);
625
+ if (exceptionBreakpoints && exceptionBreakpoints.length > 0) {
626
+ await session.setExceptionBreakpoints(exceptionBreakpoints);
627
+ }
371
628
  await session.configurationDone();
372
629
  await attachPromise;
630
+ const stateData = waitForStop ?? expectsConfiguredStop
631
+ ? await awaitFirstStop(session, timeout ?? 30000)
632
+ : { status: session.state };
373
633
  return {
374
634
  content: [
375
635
  {
376
636
  type: "text",
377
- text: JSON.stringify({ sessionId: id, status: session.state }, null, 2),
637
+ text: JSON.stringify({
638
+ sessionId: id,
639
+ breakpoints: breakpointResults.length > 0 ? breakpointResults : undefined,
640
+ ...stateData,
641
+ }, null, 2),
378
642
  },
379
643
  ],
380
644
  };
@@ -455,32 +719,40 @@ export function createServer() {
455
719
  threadId: z.number().optional().describe("Thread ID (uses stopped thread if omitted)"),
456
720
  depth: z.number().optional().describe("Stack trace depth (default: 10)"),
457
721
  variableDepth: z.number().optional().describe("Variable expansion depth (default: 1)"),
722
+ scopes: z
723
+ .array(z.string())
724
+ .optional()
725
+ .describe("Scope names to fetch, e.g. ['Registers']. Default: every scope except CPU registers; skipped ones are listed in omittedScopes"),
726
+ detail: detailSchema
727
+ .optional()
728
+ .describe("'full' (default) or 'compact' (frame-local scopes only, slimmer JSON)"),
458
729
  timeout: z
459
730
  .number()
460
731
  .optional()
461
732
  .describe("If running, wait up to this many milliseconds for the program to stop (default: 0 = don't wait)"),
462
- }, async ({ sessionId, threadId, depth, variableDepth, timeout }) => {
733
+ }, {
734
+ readOnlyHint: true,
735
+ destructiveHint: false,
736
+ idempotentHint: true,
737
+ openWorldHint: false,
738
+ }, async ({ sessionId, threadId, depth, variableDepth, scopes, detail, timeout }) => {
463
739
  try {
464
740
  const session = sessions.resolve(sessionId);
465
741
  const waitMs = timeout ?? 0;
742
+ const stateDetail = detail ?? "full";
743
+ if (waitMs > 0 && session.state === "running") {
744
+ await session.waitForStopOrTermination(waitMs);
745
+ }
466
746
  if (session.state === "stopped") {
467
- const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
747
+ // Read-only: output stays unreported so a later step still carries it.
748
+ const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1, {
749
+ scopes,
750
+ localsOnly: stateDetail === "compact",
751
+ });
468
752
  return {
469
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
753
+ content: [{ type: "text", text: renderState(state, stateDetail) }],
470
754
  };
471
755
  }
472
- if (waitMs > 0 && session.state === "running") {
473
- try {
474
- await session.waitForStopped(waitMs);
475
- const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
476
- return {
477
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
478
- };
479
- }
480
- catch {
481
- // Timed out — fall through to return running status
482
- }
483
- }
484
756
  const { output } = session.getOutput();
485
757
  return {
486
758
  content: [
@@ -547,20 +819,29 @@ export function createServer() {
547
819
  .enum(["continue", "next", "stepIn", "stepOut", "pause"])
548
820
  .describe("Execution control action"),
549
821
  threadId: z.number().optional().describe("Thread ID (uses stopped thread if omitted)"),
822
+ detail: detailSchema
823
+ .optional()
824
+ .describe("'compact' (default: frame-local scopes, short stack, slimmer JSON) or 'full' (same shape as get_state)"),
550
825
  timeout: z
551
826
  .number()
552
827
  .optional()
553
828
  .describe("Milliseconds to wait for the program to stop after the action (default: 30000)"),
554
- }, async ({ sessionId, action, threadId, timeout }) => {
829
+ }, async ({ sessionId, action, threadId, detail, timeout }) => {
555
830
  try {
556
831
  const session = sessions.resolve(sessionId);
557
832
  const waitMs = timeout ?? 30000;
558
833
  const startTime = Date.now();
834
+ const stateDetail = detail ?? "compact";
835
+ const collectSteppedState = () => session.collectState(threadId, stateDetail === "compact" ? COMPACT_STACK_DEPTH : 10, 1, {
836
+ localsOnly: stateDetail === "compact",
837
+ consumeOutput: true,
838
+ });
559
839
  // If already stopped and asking to pause, just return current state
560
840
  if (action === "pause" && session.state === "stopped") {
561
- const state = await session.collectState(threadId);
562
841
  return {
563
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
842
+ content: [
843
+ { type: "text", text: renderState(await collectSteppedState(), stateDetail) },
844
+ ],
564
845
  };
565
846
  }
566
847
  switch (action) {
@@ -580,20 +861,13 @@ export function createServer() {
580
861
  await session.pause(threadId);
581
862
  break;
582
863
  }
583
- try {
584
- await session.waitForStopped(waitMs);
585
- }
586
- catch (err) {
587
- // Timeout or terminated
864
+ const outcome = await session.waitForStopOrTermination(waitMs);
865
+ if (outcome.status !== "stopped") {
588
866
  const elapsed = Date.now() - startTime;
589
- if (session.state === "terminated") {
590
- const { output } = session.getOutput();
867
+ if (outcome.status === "terminated") {
591
868
  return {
592
869
  content: [
593
- {
594
- type: "text",
595
- text: JSON.stringify({ status: "terminated", output: output.map((e) => e.text) }, null, 2),
596
- },
870
+ { type: "text", text: JSON.stringify(await terminatedReport(session)) },
597
871
  ],
598
872
  };
599
873
  }
@@ -606,20 +880,10 @@ export function createServer() {
606
880
  ],
607
881
  };
608
882
  }
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
- const state = await session.collectState(threadId);
621
883
  return {
622
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
884
+ content: [
885
+ { type: "text", text: renderState(await collectSteppedState(), stateDetail) },
886
+ ],
623
887
  };
624
888
  }
625
889
  catch (err) {
@@ -643,6 +907,11 @@ export function createServer() {
643
907
  .number()
644
908
  .optional()
645
909
  .describe("Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."),
910
+ }, {
911
+ readOnlyHint: false,
912
+ destructiveHint: true,
913
+ idempotentHint: false,
914
+ openWorldHint: false,
646
915
  }, async ({ sessionId, category, clear, since }) => {
647
916
  try {
648
917
  const session = sessions.resolve(sessionId);
@@ -746,19 +1015,10 @@ export function createServer() {
746
1015
  await newSession.configurationDone();
747
1016
  await launchPromise;
748
1017
  const stopOnEntry = launchConfig.stopOnEntry !== false;
749
- let stateData;
750
- if (stopOnEntry) {
751
- try {
752
- await newSession.waitForStopped(10000);
753
- stateData = (await newSession.collectState());
754
- }
755
- catch {
756
- stateData = { status: newSession.state };
757
- }
758
- }
759
- else {
760
- stateData = { status: newSession.state };
761
- }
1018
+ const shouldWait = stopOnEntry || savedBreakpoints.size > 0;
1019
+ const stateData = shouldWait
1020
+ ? await awaitFirstStop(newSession, 30000)
1021
+ : { status: newSession.state };
762
1022
  return {
763
1023
  content: [
764
1024
  {
@@ -804,6 +1064,10 @@ export function createServer() {
804
1064
  .number()
805
1065
  .optional()
806
1066
  .describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
1067
+ launchConfig: z
1068
+ .record(z.string(), z.unknown())
1069
+ .optional()
1070
+ .describe("Adapter-specific DAP launch fields"),
807
1071
  adapterConfig: z
808
1072
  .object({
809
1073
  type: z.string(),
@@ -819,6 +1083,7 @@ export function createServer() {
819
1083
  .describe("Override adapter config inline"),
820
1084
  }, async (opts) => {
821
1085
  try {
1086
+ await registryReady;
822
1087
  const result = await runDebugInspect(registry, sessions, opts);
823
1088
  return {
824
1089
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -836,6 +1101,11 @@ export function createServer() {
836
1101
  // ────────────────────────────────────────────────────────────────────────────
837
1102
  server.tool("list_breakpoints", "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.", {
838
1103
  sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
1104
+ }, {
1105
+ readOnlyHint: true,
1106
+ destructiveHint: false,
1107
+ idempotentHint: true,
1108
+ openWorldHint: false,
839
1109
  }, async ({ sessionId }) => {
840
1110
  try {
841
1111
  const session = sessions.resolve(sessionId);
@@ -897,6 +1167,7 @@ export function createServer() {
897
1167
  .describe("Override adapter config inline"),
898
1168
  }, async (opts) => {
899
1169
  try {
1170
+ await registryReady;
900
1171
  const result = await runDebugRun(registry, sessions, opts);
901
1172
  return {
902
1173
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],