dap-mcp-server 0.1.9 → 0.1.11
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/README.md +5 -2
- package/SKILL.md +175 -28
- package/TODO.md +32 -0
- package/build/adapters/builtin.js +3 -0
- package/build/adapters/builtin.js.map +1 -1
- package/build/dap/dap-client.js +23 -0
- package/build/dap/dap-client.js.map +1 -1
- package/build/dap/types.d.ts +12 -7
- package/build/server.js +222 -51
- package/build/server.js.map +1 -1
- package/build/session/inspect.d.ts +64 -0
- package/build/session/inspect.js +103 -0
- package/build/session/inspect.js.map +1 -0
- package/build/session/run.d.ts +52 -0
- package/build/session/run.js +103 -0
- package/build/session/run.js.map +1 -0
- package/build/session/session.d.ts +8 -9
- package/build/session/session.js +57 -7
- package/build/session/session.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/builtin.ts +3 -0
- package/src/dap/dap-client.ts +22 -0
- package/src/dap/types.ts +13 -7
- package/src/integration/debug-lifecycle.test.ts +27 -3
- package/src/server.ts +245 -49
- package/src/session/inspect.test.ts +126 -0
- package/src/session/inspect.ts +147 -0
- package/src/session/run.test.ts +74 -0
- package/src/session/run.ts +136 -0
- package/src/session/session.test.ts +42 -0
- package/src/session/session.ts +58 -19
- package/src/test-fixtures/mock-adapter.js +23 -1
- package/src/test-fixtures/no-stop-adapter.js +77 -0
- package/.claude/settings.local.json +0 -31
|
@@ -46,14 +46,17 @@ describe("Debug Lifecycle Integration", () => {
|
|
|
46
46
|
expect(caps.supportsConfigurationDoneRequest).toBe(true);
|
|
47
47
|
expect(session.state).toBe("configuring");
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Send launch first (don't await — real adapters block until configurationDone)
|
|
50
|
+
const launchPromise = session.launch({ program: "/test/app.py", stopOnEntry: true });
|
|
51
|
+
|
|
52
|
+
// Set breakpoints while launch is pending
|
|
50
53
|
const bps = await session.setBreakpoints("/test/app.py", [{ line: 5 }, { line: 10 }]);
|
|
51
54
|
expect(bps).toHaveLength(2);
|
|
52
55
|
expect(bps[0].verified).toBe(true);
|
|
53
56
|
|
|
54
|
-
// Configuration done + launch
|
|
57
|
+
// Configuration done + await launch
|
|
55
58
|
await session.configurationDone();
|
|
56
|
-
await
|
|
59
|
+
await launchPromise;
|
|
57
60
|
|
|
58
61
|
// Wait for stopped on entry
|
|
59
62
|
const stoppedEvent = await session.waitForStopped(5000);
|
|
@@ -98,6 +101,27 @@ describe("Debug Lifecycle Integration", () => {
|
|
|
98
101
|
expect(session.state).toBe("terminated");
|
|
99
102
|
});
|
|
100
103
|
|
|
104
|
+
it("returns nested children when collectState uses variableDepth > 1", async () => {
|
|
105
|
+
session = new DebugSession("integ-deep", { type: "mock", runtime: "node", program: MOCK_ADAPTER });
|
|
106
|
+
await session.initialize();
|
|
107
|
+
const launchPromise = session.launch({ program: "/test/app.py", stopOnEntry: true });
|
|
108
|
+
await session.configurationDone();
|
|
109
|
+
await launchPromise;
|
|
110
|
+
await session.waitForStopped(5000);
|
|
111
|
+
|
|
112
|
+
const state = await session.collectState(1, 10, 2);
|
|
113
|
+
const locals = state.scopes.find(s => s.name === "Locals");
|
|
114
|
+
const data = locals!.variables.find(v => v.name === "data");
|
|
115
|
+
expect(data).toBeDefined();
|
|
116
|
+
expect(data!.hasChildren).toBe(true);
|
|
117
|
+
expect(data!.children).toBeDefined();
|
|
118
|
+
expect(data!.children!.map(c => c.value)).toEqual(["1", "2", "3"]);
|
|
119
|
+
|
|
120
|
+
// Leaf variables should not get a children property.
|
|
121
|
+
const x = locals!.variables.find(v => v.name === "x");
|
|
122
|
+
expect(x!.children).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
101
125
|
it("handles adapter crash gracefully", async () => {
|
|
102
126
|
const crashAdapter = path.join(__dirname, "../test-fixtures/crash-adapter.js");
|
|
103
127
|
session = new DebugSession("integ-crash", { type: "mock", runtime: "node", program: crashAdapter });
|
package/src/server.ts
CHANGED
|
@@ -24,12 +24,28 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
24
24
|
import { z } from "zod";
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
27
29
|
|
|
28
30
|
import { AdapterRegistry } from "./adapters/registry.js";
|
|
29
31
|
import { SessionManager } from "./session/session-manager.js";
|
|
30
32
|
import { DebugSession } from "./session/session.js";
|
|
33
|
+
import { runDebugInspect } from "./session/inspect.js";
|
|
34
|
+
import { runDebugRun } from "./session/run.js";
|
|
31
35
|
import { checkAdapterAvailable } from "./adapters/detect.js";
|
|
32
36
|
|
|
37
|
+
function readPackageVersion(): string {
|
|
38
|
+
try {
|
|
39
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
const pkg = JSON.parse(
|
|
41
|
+
readFileSync(path.join(here, "..", "package.json"), "utf-8")
|
|
42
|
+
) as { version?: string };
|
|
43
|
+
return pkg.version ?? "0.0.0";
|
|
44
|
+
} catch {
|
|
45
|
+
return "0.0.0";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
export function createServer(): McpServer {
|
|
34
50
|
const globalDir = path.join(os.homedir(), ".dap-mcp");
|
|
35
51
|
const projectDir = path.join(process.cwd(), ".dap-mcp");
|
|
@@ -45,41 +61,44 @@ export function createServer(): McpServer {
|
|
|
45
61
|
const server = new McpServer(
|
|
46
62
|
{
|
|
47
63
|
name: "dap-mcp-server",
|
|
48
|
-
version:
|
|
64
|
+
version: readPackageVersion(),
|
|
49
65
|
},
|
|
50
66
|
{
|
|
51
67
|
instructions: [
|
|
52
68
|
"This server provides AI-driven debugging via the Debug Adapter Protocol (DAP).",
|
|
53
69
|
"",
|
|
54
|
-
"##
|
|
70
|
+
"## When to use which tool",
|
|
71
|
+
"",
|
|
72
|
+
"- **`debug_inspect`** — One-shot 'what is X at line N?'. Launches, hits a breakpoint, evaluates expressions,",
|
|
73
|
+
" disconnects. Use this first; it collapses a multi-call workflow into a single call.",
|
|
74
|
+
"- **`debug_run`** — Run a program with no stops and collect all output. Combine with `logpoints` (breakpoints",
|
|
75
|
+
" with `logMessage`) for printf-style debugging without modifying source.",
|
|
76
|
+
"- **`debug_launch` + step/evaluate** — Interactive multi-step session when you need to explore.",
|
|
55
77
|
"",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"4. `step` —
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"7. `
|
|
67
|
-
"8. `
|
|
78
|
+
"## Interactive Debugging Workflow",
|
|
79
|
+
"",
|
|
80
|
+
"1. `list_adapters` — discover available adapters; the response includes a summary and install hints.",
|
|
81
|
+
"2. `debug_launch` (or `debug_attach`) — start a session. With breakpoints provided, `stopOnEntry` defaults",
|
|
82
|
+
" to false so the program runs to the first breakpoint. Returns sessionId + initial stopped state.",
|
|
83
|
+
"3. Inspect state with `get_state` — threads + call stack + scopes + variables in one call.",
|
|
84
|
+
"4. `step` — continue, next, stepIn, stepOut, pause. Blocks until stopped; returns new state directly.",
|
|
85
|
+
"5. `evaluate` — expressions in the context of a stopped frame.",
|
|
86
|
+
"6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints mid-session. Use `logMessage`",
|
|
87
|
+
" on a breakpoint to make it a logpoint (emits formatted text without pausing).",
|
|
88
|
+
"7. `list_breakpoints` — see what's currently set.",
|
|
89
|
+
"8. `get_output` — retrieve stdout/stderr/console output (10K-line ring buffer).",
|
|
90
|
+
"9. `debug_disconnect` — end. `debug_restart` replays the launch config with preserved breakpoints.",
|
|
68
91
|
"",
|
|
69
92
|
"## Key Conventions",
|
|
70
93
|
"",
|
|
71
|
-
"- **Auto-session resolution**:
|
|
72
|
-
"
|
|
73
|
-
"- **
|
|
74
|
-
"
|
|
75
|
-
"- **
|
|
76
|
-
"
|
|
77
|
-
"-
|
|
78
|
-
"
|
|
79
|
-
"- **Adapter registration**: Use `configure_adapter` to register adapters. Configs are persisted to",
|
|
80
|
-
" `~/.dap-mcp/adapters.json` (global) or `./.dap-mcp/adapters.json` (project), with project overriding global.",
|
|
81
|
-
"- **Error handling**: All tool errors are returned as `isError: true` results with an error message.",
|
|
82
|
-
" The session remains usable after most errors.",
|
|
94
|
+
"- **Auto-session resolution**: tools accept an optional `sessionId`; with one active session, omit it.",
|
|
95
|
+
"- **Synchronous stepping**: `step` returns the new stopped state. Don't follow it with `get_state`.",
|
|
96
|
+
"- **Variable depth**: `get_state` accepts `variableDepth` (default 1). With `> 1`, container variables",
|
|
97
|
+
" expose their entries as a structured `children` array.",
|
|
98
|
+
"- **stopOnEntry default**: false when `debug_launch` is given breakpoints, true otherwise.",
|
|
99
|
+
"- **Adapter registration**: `configure_adapter` persists to `~/.dap-mcp/adapters.json` (global) or",
|
|
100
|
+
" `./.dap-mcp/adapters.json` (project); project overrides global.",
|
|
101
|
+
"- **Error handling**: tool errors return `isError: true`. The session usually remains usable.",
|
|
83
102
|
].join("\n"),
|
|
84
103
|
}
|
|
85
104
|
);
|
|
@@ -89,7 +108,7 @@ export function createServer(): McpServer {
|
|
|
89
108
|
// ────────────────────────────────────────────────────────────────────────────
|
|
90
109
|
server.tool(
|
|
91
110
|
"list_adapters",
|
|
92
|
-
"List all known debug adapters with their availability status on this machine.",
|
|
111
|
+
"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.",
|
|
93
112
|
{},
|
|
94
113
|
async () => {
|
|
95
114
|
try {
|
|
@@ -101,8 +120,21 @@ export function createServer(): McpServer {
|
|
|
101
120
|
return { ...a, available };
|
|
102
121
|
})
|
|
103
122
|
);
|
|
123
|
+
const available = results.filter((r) => r.available);
|
|
124
|
+
const missing = results
|
|
125
|
+
.filter((r) => !r.available)
|
|
126
|
+
.map((r) => ({ type: r.type, installHint: r.installHint }));
|
|
127
|
+
const summary =
|
|
128
|
+
missing.length === 0
|
|
129
|
+
? `${available.length}/${results.length} adapters available`
|
|
130
|
+
: `${available.length}/${results.length} adapters available; missing: ${missing.map((m) => m.type).join(", ")}`;
|
|
104
131
|
return {
|
|
105
|
-
content: [
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text" as const,
|
|
135
|
+
text: JSON.stringify({ summary, adapters: results, missing }, null, 2),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
106
138
|
};
|
|
107
139
|
} catch (err) {
|
|
108
140
|
return {
|
|
@@ -163,7 +195,10 @@ export function createServer(): McpServer {
|
|
|
163
195
|
args: z.array(z.string()).optional().describe("Program arguments"),
|
|
164
196
|
cwd: z.string().optional().describe("Working directory for the program"),
|
|
165
197
|
env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
|
|
166
|
-
stopOnEntry: z
|
|
198
|
+
stopOnEntry: z
|
|
199
|
+
.boolean()
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Stop on program entry. Defaults to false when breakpoints are provided (run to first breakpoint), true otherwise."),
|
|
167
202
|
breakpoints: z
|
|
168
203
|
.array(
|
|
169
204
|
z.object({
|
|
@@ -203,7 +238,9 @@ export function createServer(): McpServer {
|
|
|
203
238
|
exceptionBreakpoints,
|
|
204
239
|
adapterConfig,
|
|
205
240
|
}) => {
|
|
206
|
-
|
|
241
|
+
// Default: if breakpoints are provided, run to the first one; otherwise stop at entry.
|
|
242
|
+
const shouldStop =
|
|
243
|
+
stopOnEntry ?? !(breakpoints !== undefined && breakpoints.length > 0);
|
|
207
244
|
let session: DebugSession | undefined;
|
|
208
245
|
try {
|
|
209
246
|
const config =
|
|
@@ -229,6 +266,20 @@ export function createServer(): McpServer {
|
|
|
229
266
|
// Initialize (waits for the initialized event internally)
|
|
230
267
|
await session.initialize();
|
|
231
268
|
|
|
269
|
+
// Build launch request
|
|
270
|
+
const launchArgs: Record<string, unknown> = {
|
|
271
|
+
...config.launchDefaults,
|
|
272
|
+
program,
|
|
273
|
+
stopOnEntry: shouldStop,
|
|
274
|
+
};
|
|
275
|
+
if (args !== undefined) launchArgs.args = args;
|
|
276
|
+
if (cwd !== undefined) launchArgs.cwd = cwd;
|
|
277
|
+
if (env !== undefined) launchArgs.env = env;
|
|
278
|
+
|
|
279
|
+
// Send launch first (don't await — real adapters like debugpy block
|
|
280
|
+
// the launch response until configurationDone arrives)
|
|
281
|
+
const launchPromise = session.launch(launchArgs);
|
|
282
|
+
|
|
232
283
|
// Set breakpoints grouped by file
|
|
233
284
|
if (breakpoints && breakpoints.length > 0) {
|
|
234
285
|
const byFile = new Map<string, typeof breakpoints>();
|
|
@@ -255,20 +306,11 @@ export function createServer(): McpServer {
|
|
|
255
306
|
await session.setExceptionBreakpoints(exceptionBreakpoints);
|
|
256
307
|
}
|
|
257
308
|
|
|
258
|
-
// Configuration done
|
|
309
|
+
// Configuration done — signals the adapter to proceed with launch
|
|
259
310
|
await session.configurationDone();
|
|
260
311
|
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
...config.launchDefaults,
|
|
264
|
-
program,
|
|
265
|
-
stopOnEntry: shouldStop,
|
|
266
|
-
};
|
|
267
|
-
if (args !== undefined) launchArgs.args = args;
|
|
268
|
-
if (cwd !== undefined) launchArgs.cwd = cwd;
|
|
269
|
-
if (env !== undefined) launchArgs.env = env;
|
|
270
|
-
|
|
271
|
-
await session.launch(launchArgs);
|
|
312
|
+
// Now await the launch response
|
|
313
|
+
await launchPromise;
|
|
272
314
|
|
|
273
315
|
let stateData: Record<string, unknown>;
|
|
274
316
|
if (shouldStop) {
|
|
@@ -348,7 +390,6 @@ export function createServer(): McpServer {
|
|
|
348
390
|
sessions.add(session);
|
|
349
391
|
|
|
350
392
|
await session.initialize();
|
|
351
|
-
await session.configurationDone();
|
|
352
393
|
|
|
353
394
|
// Merge order: adapter defaults (lowest priority) < user attachConfig < explicit params (highest)
|
|
354
395
|
const attachArgs: Record<string, unknown> = {
|
|
@@ -359,7 +400,10 @@ export function createServer(): McpServer {
|
|
|
359
400
|
if (host !== undefined) attachArgs.host = host;
|
|
360
401
|
if (processId !== undefined) attachArgs.processId = processId;
|
|
361
402
|
|
|
362
|
-
|
|
403
|
+
// Send attach first (don't await — real adapters block until configurationDone)
|
|
404
|
+
const attachPromise = session.attach(attachArgs);
|
|
405
|
+
await session.configurationDone();
|
|
406
|
+
await attachPromise;
|
|
363
407
|
|
|
364
408
|
return {
|
|
365
409
|
content: [
|
|
@@ -539,7 +583,7 @@ export function createServer(): McpServer {
|
|
|
539
583
|
content: [
|
|
540
584
|
{
|
|
541
585
|
type: "text" as const,
|
|
542
|
-
text: `Error: Session is not stopped (current state: ${session.state}).
|
|
586
|
+
text: `Error: Session is not stopped (current state: ${session.state}). Expressions can only be evaluated at a stopped frame. Set a breakpoint where you want to inspect (set_breakpoints) and continue to it (step with action 'continue'), then retry.`,
|
|
543
587
|
},
|
|
544
588
|
],
|
|
545
589
|
isError: true,
|
|
@@ -671,7 +715,7 @@ export function createServer(): McpServer {
|
|
|
671
715
|
// ────────────────────────────────────────────────────────────────────────────
|
|
672
716
|
server.tool(
|
|
673
717
|
"get_output",
|
|
674
|
-
"Get buffered stdout/stderr and debug adapter output for a session.",
|
|
718
|
+
"Get buffered stdout/stderr and debug adapter output for a session. Pass `since` (the `nextSince` from a previous call) to get only newly-emitted entries — useful for polling logpoint output without re-reading the full buffer.",
|
|
675
719
|
{
|
|
676
720
|
sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
|
|
677
721
|
category: z
|
|
@@ -679,11 +723,17 @@ export function createServer(): McpServer {
|
|
|
679
723
|
.optional()
|
|
680
724
|
.describe("Filter by output category: 'stdout', 'stderr', 'console', etc."),
|
|
681
725
|
clear: z.boolean().optional().describe("Clear the output buffer after reading (default: false)"),
|
|
726
|
+
since: z
|
|
727
|
+
.number()
|
|
728
|
+
.optional()
|
|
729
|
+
.describe(
|
|
730
|
+
"Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."
|
|
731
|
+
),
|
|
682
732
|
},
|
|
683
|
-
async ({ sessionId, category, clear }) => {
|
|
733
|
+
async ({ sessionId, category, clear, since }) => {
|
|
684
734
|
try {
|
|
685
735
|
const session = sessions.resolve(sessionId);
|
|
686
|
-
const result = session.getOutput(category, clear ?? false);
|
|
736
|
+
const result = session.getOutput(category, clear ?? false, since);
|
|
687
737
|
return {
|
|
688
738
|
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
689
739
|
};
|
|
@@ -787,13 +837,16 @@ export function createServer(): McpServer {
|
|
|
787
837
|
// Initialize (waits for the initialized event internally)
|
|
788
838
|
await newSession.initialize();
|
|
789
839
|
|
|
840
|
+
// Send launch first (don't await — real adapters block until configurationDone)
|
|
841
|
+
const launchPromise = newSession.launch(launchConfig);
|
|
842
|
+
|
|
790
843
|
// Re-apply saved breakpoints
|
|
791
844
|
for (const [file, bps] of savedBreakpoints) {
|
|
792
845
|
await newSession.setBreakpoints(file, bps);
|
|
793
846
|
}
|
|
794
847
|
|
|
795
848
|
await newSession.configurationDone();
|
|
796
|
-
await
|
|
849
|
+
await launchPromise;
|
|
797
850
|
|
|
798
851
|
const stopOnEntry = launchConfig.stopOnEntry !== false;
|
|
799
852
|
let stateData: Record<string, unknown>;
|
|
@@ -833,6 +886,149 @@ export function createServer(): McpServer {
|
|
|
833
886
|
}
|
|
834
887
|
);
|
|
835
888
|
|
|
889
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
890
|
+
// 13. debug_inspect
|
|
891
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
892
|
+
server.tool(
|
|
893
|
+
"debug_inspect",
|
|
894
|
+
"One-shot inspection: launch a program, run to a breakpoint, evaluate a list of expressions in that frame, then disconnect. Collapses launch + wait + evaluate + disconnect into a single call. Use for 'what is X at line N?' debugging.",
|
|
895
|
+
{
|
|
896
|
+
adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
|
|
897
|
+
program: z.string().describe("Path to the program to debug"),
|
|
898
|
+
args: z.array(z.string()).optional().describe("Program arguments"),
|
|
899
|
+
cwd: z.string().optional().describe("Working directory for the program"),
|
|
900
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
|
|
901
|
+
at: z
|
|
902
|
+
.object({
|
|
903
|
+
file: z.string().describe("Absolute path to the source file"),
|
|
904
|
+
line: z.number().describe("Line number (1-based)"),
|
|
905
|
+
condition: z.string().optional().describe("Conditional breakpoint expression"),
|
|
906
|
+
hitCondition: z.string().optional().describe("Hit-count condition (e.g. '5' for 5th hit)"),
|
|
907
|
+
})
|
|
908
|
+
.describe("Breakpoint location to stop at"),
|
|
909
|
+
expressions: z
|
|
910
|
+
.array(z.string())
|
|
911
|
+
.optional()
|
|
912
|
+
.describe("Expressions to evaluate when stopped at the breakpoint. Omit to just verify the line is reached."),
|
|
913
|
+
timeout: z
|
|
914
|
+
.number()
|
|
915
|
+
.optional()
|
|
916
|
+
.describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
|
|
917
|
+
adapterConfig: z
|
|
918
|
+
.object({
|
|
919
|
+
type: z.string(),
|
|
920
|
+
program: z.string(),
|
|
921
|
+
runtime: z.string().optional(),
|
|
922
|
+
args: z.array(z.string()).optional(),
|
|
923
|
+
launchDefaults: z.record(z.string(), z.unknown()).optional(),
|
|
924
|
+
installHint: z.string().optional(),
|
|
925
|
+
})
|
|
926
|
+
.optional()
|
|
927
|
+
.describe("Override adapter config inline"),
|
|
928
|
+
},
|
|
929
|
+
async (opts) => {
|
|
930
|
+
try {
|
|
931
|
+
const result = await runDebugInspect(registry, sessions, opts);
|
|
932
|
+
return {
|
|
933
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
934
|
+
};
|
|
935
|
+
} catch (err) {
|
|
936
|
+
return {
|
|
937
|
+
content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
|
|
938
|
+
isError: true,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
945
|
+
// 14. list_breakpoints
|
|
946
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
947
|
+
server.tool(
|
|
948
|
+
"list_breakpoints",
|
|
949
|
+
"List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.",
|
|
950
|
+
{
|
|
951
|
+
sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
|
|
952
|
+
},
|
|
953
|
+
async ({ sessionId }) => {
|
|
954
|
+
try {
|
|
955
|
+
const session = sessions.resolve(sessionId);
|
|
956
|
+
const bps = session.getBreakpoints();
|
|
957
|
+
const files = Array.from(bps.entries()).map(([file, breakpoints]) => ({
|
|
958
|
+
file,
|
|
959
|
+
breakpoints,
|
|
960
|
+
}));
|
|
961
|
+
const totalCount = files.reduce((n, f) => n + f.breakpoints.length, 0);
|
|
962
|
+
return {
|
|
963
|
+
content: [
|
|
964
|
+
{
|
|
965
|
+
type: "text" as const,
|
|
966
|
+
text: JSON.stringify({ count: totalCount, files }, null, 2),
|
|
967
|
+
},
|
|
968
|
+
],
|
|
969
|
+
};
|
|
970
|
+
} catch (err) {
|
|
971
|
+
return {
|
|
972
|
+
content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
|
|
973
|
+
isError: true,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
980
|
+
// 15. debug_run
|
|
981
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
982
|
+
server.tool(
|
|
983
|
+
"debug_run",
|
|
984
|
+
"Launch a program with no stops, let it run to completion, and return all stdout/stderr/console output. Optionally inject logpoints (breakpoints with logMessage) for printf-style debugging without modifying source. Use for 'what does this script actually do?' cases where you don't need to inspect intermediate state.",
|
|
985
|
+
{
|
|
986
|
+
adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
|
|
987
|
+
program: z.string().describe("Path to the program to debug"),
|
|
988
|
+
args: z.array(z.string()).optional().describe("Program arguments"),
|
|
989
|
+
cwd: z.string().optional().describe("Working directory for the program"),
|
|
990
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
|
|
991
|
+
logpoints: z
|
|
992
|
+
.array(
|
|
993
|
+
z.object({
|
|
994
|
+
file: z.string().describe("Absolute path to the source file"),
|
|
995
|
+
line: z.number().describe("Line number (1-based)"),
|
|
996
|
+
logMessage: z.string().describe("Message template; expressions in {braces} are evaluated in scope"),
|
|
997
|
+
})
|
|
998
|
+
)
|
|
999
|
+
.optional()
|
|
1000
|
+
.describe("Logpoints to emit per-hit messages without pausing execution"),
|
|
1001
|
+
timeout: z
|
|
1002
|
+
.number()
|
|
1003
|
+
.optional()
|
|
1004
|
+
.describe("Total wall-clock timeout for the whole run in milliseconds (default: 60000)"),
|
|
1005
|
+
adapterConfig: z
|
|
1006
|
+
.object({
|
|
1007
|
+
type: z.string(),
|
|
1008
|
+
program: z.string(),
|
|
1009
|
+
runtime: z.string().optional(),
|
|
1010
|
+
args: z.array(z.string()).optional(),
|
|
1011
|
+
launchDefaults: z.record(z.string(), z.unknown()).optional(),
|
|
1012
|
+
installHint: z.string().optional(),
|
|
1013
|
+
})
|
|
1014
|
+
.optional()
|
|
1015
|
+
.describe("Override adapter config inline"),
|
|
1016
|
+
},
|
|
1017
|
+
async (opts) => {
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await runDebugRun(registry, sessions, opts);
|
|
1020
|
+
return {
|
|
1021
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
1022
|
+
};
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
return {
|
|
1025
|
+
content: [{ type: "text" as const, text: `Error: ${String(err)}` }],
|
|
1026
|
+
isError: true,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
);
|
|
1031
|
+
|
|
836
1032
|
// ────────────────────────────────────────────────────────────────────────────
|
|
837
1033
|
// Resource: dap://sessions
|
|
838
1034
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ivan Iraci <ivan.iraci@professioneit.com>
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in
|
|
12
|
+
* all copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from "vitest";
|
|
24
|
+
import { runDebugInspect } from "./inspect.js";
|
|
25
|
+
import { AdapterRegistry } from "../adapters/registry.js";
|
|
26
|
+
import { SessionManager } from "./session-manager.js";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const MOCK_ADAPTER = path.join(__dirname, "../test-fixtures/mock-adapter.js");
|
|
33
|
+
|
|
34
|
+
describe("runDebugInspect", () => {
|
|
35
|
+
it("launches, hits a breakpoint, evaluates expressions, and disconnects", async () => {
|
|
36
|
+
const registry = new AdapterRegistry(
|
|
37
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
|
|
38
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
|
|
39
|
+
);
|
|
40
|
+
const sessions = new SessionManager();
|
|
41
|
+
|
|
42
|
+
const result = await runDebugInspect(registry, sessions, {
|
|
43
|
+
adapter: "mock",
|
|
44
|
+
program: "/test/app.py",
|
|
45
|
+
at: { file: "/test/app.py", line: 5 },
|
|
46
|
+
expressions: ["x", "name"],
|
|
47
|
+
timeout: 5000,
|
|
48
|
+
adapterConfig: {
|
|
49
|
+
type: "mock",
|
|
50
|
+
runtime: "node",
|
|
51
|
+
program: MOCK_ADAPTER,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.hit).toBe(true);
|
|
56
|
+
expect(result.reason).toBe("breakpoint");
|
|
57
|
+
expect(result.location?.file).toBe("/test/app.py");
|
|
58
|
+
expect(result.values).toBeDefined();
|
|
59
|
+
expect(result.values!["x"].result).toBe("42");
|
|
60
|
+
expect(result.values!["name"].result).toBe("42");
|
|
61
|
+
expect(sessions.listAll()).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns hit=true with no values when expressions omitted", async () => {
|
|
65
|
+
const registry = new AdapterRegistry(
|
|
66
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
|
|
67
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
|
|
68
|
+
);
|
|
69
|
+
const sessions = new SessionManager();
|
|
70
|
+
|
|
71
|
+
const result = await runDebugInspect(registry, sessions, {
|
|
72
|
+
adapter: "mock",
|
|
73
|
+
program: "/test/app.py",
|
|
74
|
+
at: { file: "/test/app.py", line: 5 },
|
|
75
|
+
timeout: 5000,
|
|
76
|
+
adapterConfig: {
|
|
77
|
+
type: "mock",
|
|
78
|
+
runtime: "node",
|
|
79
|
+
program: MOCK_ADAPTER,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.hit).toBe(true);
|
|
84
|
+
expect(result.values).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rejects when adapter is unknown and no inline config provided", async () => {
|
|
88
|
+
const registry = new AdapterRegistry(
|
|
89
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
|
|
90
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
|
|
91
|
+
);
|
|
92
|
+
const sessions = new SessionManager();
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
runDebugInspect(registry, sessions, {
|
|
96
|
+
adapter: "nonexistent",
|
|
97
|
+
program: "/test/app.py",
|
|
98
|
+
at: { file: "/test/app.py", line: 5 },
|
|
99
|
+
})
|
|
100
|
+
).rejects.toThrow(/not found/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("cleans up the session even when the breakpoint never hits", async () => {
|
|
104
|
+
const registry = new AdapterRegistry(
|
|
105
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-global`),
|
|
106
|
+
path.join(os.tmpdir(), `dap-test-${Date.now()}-project`)
|
|
107
|
+
);
|
|
108
|
+
const sessions = new SessionManager();
|
|
109
|
+
|
|
110
|
+
const noStopAdapter = path.join(__dirname, "../test-fixtures/no-stop-adapter.js");
|
|
111
|
+
const result = await runDebugInspect(registry, sessions, {
|
|
112
|
+
adapter: "mock",
|
|
113
|
+
program: "/test/app.py",
|
|
114
|
+
at: { file: "/test/app.py", line: 5 },
|
|
115
|
+
timeout: 200,
|
|
116
|
+
adapterConfig: {
|
|
117
|
+
type: "mock",
|
|
118
|
+
runtime: "node",
|
|
119
|
+
program: noStopAdapter,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.hit).toBe(false);
|
|
124
|
+
expect(sessions.listAll()).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
});
|