dap-mcp-server 0.1.16 → 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.
package/build/server.js CHANGED
@@ -31,7 +31,9 @@ import { DebugSession } from "./session/session.js";
31
31
  import { runDebugInspect } from "./session/inspect.js";
32
32
  import { runDebugRun } from "./session/run.js";
33
33
  import { inspectAdapterAvailability } from "./adapters/detect.js";
34
- import { resolveProjectAdapter, runDebugDoctor } from "./diagnostics/doctor.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";
35
37
  function readPackageVersion() {
36
38
  try {
37
39
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -62,6 +64,8 @@ export function createServer() {
62
64
  "Prefer debug_at for a one-call breakpoint inspection: it selects the adapter, launches, waits, evaluates expressions, and cleans up.",
63
65
  "Use debug_run when output or termination is enough. Use debug_launch only for interactive stepping or multiple inspection points.",
64
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.",
65
69
  "Omit sessionId when exactly one session is active. Adapter-specific DAP fields belong in launchConfig. Tool errors use isError=true.",
66
70
  ].join("\n"),
67
71
  });
@@ -88,6 +92,7 @@ export function createServer() {
88
92
  program: z.string(),
89
93
  transport: z.enum(["stdio", "tcp"]),
90
94
  })),
95
+ otherAdapters: z.array(z.string()).optional(),
91
96
  nextSteps: z.array(z.string()),
92
97
  });
93
98
  const breakpointResultSchema = z.object({
@@ -100,6 +105,14 @@ export function createServer() {
100
105
  adapter: z.string(),
101
106
  hit: z.boolean(),
102
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(),
103
116
  location: z
104
117
  .object({
105
118
  file: z.string(),
@@ -117,9 +130,58 @@ export function createServer() {
117
130
  .optional(),
118
131
  output: z.array(z.string()).optional(),
119
132
  status: z.string().optional(),
133
+ exitCode: z.number().optional(),
120
134
  timedOut: z.boolean().optional(),
121
135
  breakpoint: breakpointResultSchema.optional(),
122
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
+ }
123
185
  // ────────────────────────────────────────────────────────────────────────────
124
186
  // High-level LLM entry points
125
187
  // ────────────────────────────────────────────────────────────────────────────
@@ -129,6 +191,10 @@ export function createServer() {
129
191
  inputSchema: z.object({
130
192
  cwd: z.string().optional().describe("Project directory (default: server working directory)"),
131
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"),
132
198
  }),
133
199
  outputSchema: doctorOutputSchema,
134
200
  annotations: {
@@ -195,11 +261,19 @@ export function createServer() {
195
261
  });
196
262
  const adapter = resolved.result.recommendedAdapter;
197
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
+ };
198
272
  return {
199
273
  content: [
200
274
  {
201
275
  type: "text",
202
- text: `Debugger is not ready:\n${JSON.stringify(resolved.result, null, 2)}`,
276
+ text: `Debugger is not ready: ${JSON.stringify(summary)}\nRun debug_doctor with verbose=true for full adapter details.`,
203
277
  },
204
278
  ],
205
279
  isError: true,
@@ -207,18 +281,22 @@ export function createServer() {
207
281
  }
208
282
  const hasLaunchTarget = options.launchConfig !== undefined &&
209
283
  ["program", "module", "code"].some((key) => options.launchConfig?.[key] !== undefined);
210
- if (!options.program && !hasLaunchTarget && adapter === "cppdbg") {
284
+ const compiledProgram = !options.program && !hasLaunchTarget && adapter === "cppdbg"
285
+ ? await defaultCargoProgram(cwd)
286
+ : undefined;
287
+ if (!options.program && !hasLaunchTarget && adapter === "cppdbg" && !compiledProgram) {
211
288
  return {
212
289
  content: [
213
290
  {
214
291
  type: "text",
215
- text: "Error: C/C++ debugging requires the compiled executable in program or launchConfig.program.",
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>.",
216
293
  },
217
294
  ],
218
295
  isError: true,
219
296
  };
220
297
  }
221
298
  const program = options.program ??
299
+ compiledProgram ??
222
300
  (hasLaunchTarget ? undefined : adapter === "go" ? cwd : sourceFile);
223
301
  const result = await runDebugInspect(registry, sessions, {
224
302
  adapter,
@@ -364,13 +442,7 @@ export function createServer() {
364
442
  .optional()
365
443
  .describe("Stop on program entry. Defaults to false when breakpoints are provided (run to first breakpoint), true otherwise."),
366
444
  breakpoints: z
367
- .array(z.object({
368
- file: z.string().describe("Source file path"),
369
- line: z.number().describe("Line number"),
370
- condition: z.string().optional(),
371
- hitCondition: z.string().optional(),
372
- logMessage: z.string().optional(),
373
- }))
445
+ .array(breakpointInputSchema)
374
446
  .optional()
375
447
  .describe("Breakpoints to set before launching"),
376
448
  exceptionBreakpoints: z
@@ -421,27 +493,15 @@ export function createServer() {
421
493
  sessions.add(session);
422
494
  // Initialize (waits for the initialized event internally)
423
495
  await session.initialize();
424
- // Build launch request
425
- const launchArgs = {
426
- ...config.launchDefaults,
427
- ...launchConfig,
496
+ const launchArgs = await buildLaunchArgs(config, {
497
+ program,
498
+ args,
499
+ cwd,
500
+ env,
501
+ launchConfig,
428
502
  stopOnEntry: shouldStop,
429
- };
430
- if (program !== undefined)
431
- launchArgs.program = program;
432
- if (args !== undefined)
433
- launchArgs.args = args;
434
- if (cwd !== undefined)
435
- launchArgs.cwd = cwd;
436
- if (env !== undefined)
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
- }
503
+ sourceFiles: breakpoints?.map((bp) => bp.file),
504
+ });
445
505
  // Send launch first (don't await — real adapters like debugpy block
446
506
  // the launch response until configurationDone arrives)
447
507
  const launchPromise = session.launch(launchArgs);
@@ -454,25 +514,7 @@ export function createServer() {
454
514
  // initialize response; others (debugpy) only emit it after receiving
455
515
  // launch — that's why we fire launch first and wait for initialized here.
456
516
  await session.waitForInitialized(10000);
457
- // Set breakpoints grouped by file
458
- const breakpointResults = [];
459
- if (breakpoints && breakpoints.length > 0) {
460
- const byFile = new Map();
461
- for (const bp of breakpoints) {
462
- const existing = byFile.get(bp.file) ?? [];
463
- existing.push(bp);
464
- byFile.set(bp.file, existing);
465
- }
466
- for (const [file, bps] of byFile) {
467
- const results = await session.setBreakpoints(file, bps.map((b) => ({
468
- line: b.line,
469
- condition: b.condition,
470
- hitCondition: b.hitCondition,
471
- logMessage: b.logMessage,
472
- })));
473
- breakpointResults.push({ file, breakpoints: results });
474
- }
475
- }
517
+ const breakpointResults = await applyBreakpoints(session, breakpoints);
476
518
  // Set exception breakpoints
477
519
  if (exceptionBreakpoints && exceptionBreakpoints.length > 0) {
478
520
  await session.setExceptionBreakpoints(exceptionBreakpoints);
@@ -482,27 +524,9 @@ export function createServer() {
482
524
  // Now await the launch response
483
525
  await launchPromise;
484
526
  const shouldWait = waitForStop ?? (shouldStop || expectsConfiguredStop);
485
- let stateData;
486
- if (shouldWait) {
487
- const startedAt = Date.now();
488
- const outcome = await session.waitForStopOrTermination(timeout ?? 30000);
489
- if (outcome.status === "stopped") {
490
- stateData = (await session.collectState());
491
- }
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 };
501
- }
502
- }
503
- else {
504
- stateData = { status: session.state };
505
- }
527
+ const stateData = shouldWait
528
+ ? await awaitFirstStop(session, timeout ?? 30000)
529
+ : { status: session.state };
506
530
  return {
507
531
  content: [
508
532
  {
@@ -526,12 +550,25 @@ export function createServer() {
526
550
  // ────────────────────────────────────────────────────────────────────────────
527
551
  // 4. debug_attach
528
552
  // ────────────────────────────────────────────────────────────────────────────
529
- 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.", {
530
554
  adapter: z.string().describe("Adapter type to use"),
531
555
  port: z.number().optional().describe("Debug port to attach to"),
532
556
  host: z.string().optional().describe("Host to attach to (default: localhost)"),
533
557
  processId: z.number().optional().describe("PID of process to attach to"),
534
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)"),
535
572
  adapterConfig: z
536
573
  .object({
537
574
  type: z.string(),
@@ -545,7 +582,9 @@ export function createServer() {
545
582
  })
546
583
  .optional()
547
584
  .describe("Override adapter config inline"),
548
- }, 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);
549
588
  let session;
550
589
  try {
551
590
  await registryReady;
@@ -582,13 +621,24 @@ export function createServer() {
582
621
  // will still throw if attach fails.
583
622
  attachPromise.catch(() => { });
584
623
  await session.waitForInitialized(10000);
624
+ const breakpointResults = await applyBreakpoints(session, breakpoints);
625
+ if (exceptionBreakpoints && exceptionBreakpoints.length > 0) {
626
+ await session.setExceptionBreakpoints(exceptionBreakpoints);
627
+ }
585
628
  await session.configurationDone();
586
629
  await attachPromise;
630
+ const stateData = waitForStop ?? expectsConfiguredStop
631
+ ? await awaitFirstStop(session, timeout ?? 30000)
632
+ : { status: session.state };
587
633
  return {
588
634
  content: [
589
635
  {
590
636
  type: "text",
591
- 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),
592
642
  },
593
643
  ],
594
644
  };
@@ -669,6 +719,13 @@ export function createServer() {
669
719
  threadId: z.number().optional().describe("Thread ID (uses stopped thread if omitted)"),
670
720
  depth: z.number().optional().describe("Stack trace depth (default: 10)"),
671
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)"),
672
729
  timeout: z
673
730
  .number()
674
731
  .optional()
@@ -678,25 +735,24 @@ export function createServer() {
678
735
  destructiveHint: false,
679
736
  idempotentHint: true,
680
737
  openWorldHint: false,
681
- }, async ({ sessionId, threadId, depth, variableDepth, timeout }) => {
738
+ }, async ({ sessionId, threadId, depth, variableDepth, scopes, detail, timeout }) => {
682
739
  try {
683
740
  const session = sessions.resolve(sessionId);
684
741
  const waitMs = timeout ?? 0;
742
+ const stateDetail = detail ?? "full";
743
+ if (waitMs > 0 && session.state === "running") {
744
+ await session.waitForStopOrTermination(waitMs);
745
+ }
685
746
  if (session.state === "stopped") {
686
- 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
+ });
687
752
  return {
688
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
753
+ content: [{ type: "text", text: renderState(state, stateDetail) }],
689
754
  };
690
755
  }
691
- if (waitMs > 0 && session.state === "running") {
692
- const outcome = await session.waitForStopOrTermination(waitMs);
693
- if (outcome.status === "stopped") {
694
- const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
695
- return {
696
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
697
- };
698
- }
699
- }
700
756
  const { output } = session.getOutput();
701
757
  return {
702
758
  content: [
@@ -763,20 +819,29 @@ export function createServer() {
763
819
  .enum(["continue", "next", "stepIn", "stepOut", "pause"])
764
820
  .describe("Execution control action"),
765
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)"),
766
825
  timeout: z
767
826
  .number()
768
827
  .optional()
769
828
  .describe("Milliseconds to wait for the program to stop after the action (default: 30000)"),
770
- }, async ({ sessionId, action, threadId, timeout }) => {
829
+ }, async ({ sessionId, action, threadId, detail, timeout }) => {
771
830
  try {
772
831
  const session = sessions.resolve(sessionId);
773
832
  const waitMs = timeout ?? 30000;
774
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
+ });
775
839
  // If already stopped and asking to pause, just return current state
776
840
  if (action === "pause" && session.state === "stopped") {
777
- const state = await session.collectState(threadId);
778
841
  return {
779
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
842
+ content: [
843
+ { type: "text", text: renderState(await collectSteppedState(), stateDetail) },
844
+ ],
780
845
  };
781
846
  }
782
847
  switch (action) {
@@ -800,13 +865,9 @@ export function createServer() {
800
865
  if (outcome.status !== "stopped") {
801
866
  const elapsed = Date.now() - startTime;
802
867
  if (outcome.status === "terminated") {
803
- const { output } = session.getOutput();
804
868
  return {
805
869
  content: [
806
- {
807
- type: "text",
808
- text: JSON.stringify({ status: "terminated", output: output.map((e) => e.text) }, null, 2),
809
- },
870
+ { type: "text", text: JSON.stringify(await terminatedReport(session)) },
810
871
  ],
811
872
  };
812
873
  }
@@ -819,9 +880,10 @@ export function createServer() {
819
880
  ],
820
881
  };
821
882
  }
822
- const state = await session.collectState(threadId);
823
883
  return {
824
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
884
+ content: [
885
+ { type: "text", text: renderState(await collectSteppedState(), stateDetail) },
886
+ ],
825
887
  };
826
888
  }
827
889
  catch (err) {
@@ -954,22 +1016,9 @@ export function createServer() {
954
1016
  await launchPromise;
955
1017
  const stopOnEntry = launchConfig.stopOnEntry !== false;
956
1018
  const shouldWait = stopOnEntry || savedBreakpoints.size > 0;
957
- let stateData;
958
- if (shouldWait) {
959
- const outcome = await newSession.waitForStopOrTermination(30000);
960
- if (outcome.status === "stopped") {
961
- stateData = (await newSession.collectState());
962
- }
963
- else {
964
- stateData = {
965
- status: outcome.status === "timeout" ? newSession.state : "terminated",
966
- timedOut: outcome.status === "timeout",
967
- };
968
- }
969
- }
970
- else {
971
- stateData = { status: newSession.state };
972
- }
1019
+ const stateData = shouldWait
1020
+ ? await awaitFirstStop(newSession, 30000)
1021
+ : { status: newSession.state };
973
1022
  return {
974
1023
  content: [
975
1024
  {