dap-mcp-server 0.1.10 → 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 CHANGED
@@ -24,12 +24,15 @@ Additional adapters (e.g. Node.js via js-debug) can be registered via the `confi
24
24
 
25
25
  | Tool | Description |
26
26
  |------|-------------|
27
- | `debug_launch` | Start a debug session (launch a program with breakpoints) |
27
+ | `debug_inspect` | **One-shot:** launch → run to breakpoint → evaluate expressions → disconnect. Use first when you just need a value at a line. |
28
+ | `debug_run` | **One-shot:** launch → run to completion → return all output. Combine with logpoints for printf-style debugging without editing source. |
29
+ | `debug_launch` | Start an interactive debug session (launch a program with breakpoints) |
28
30
  | `debug_attach` | Attach to a running process |
29
31
  | `debug_restart` | Restart with the same configuration |
30
32
  | `debug_disconnect` | End a debug session |
31
- | `set_breakpoints` | Set/replace breakpoints in a file |
33
+ | `set_breakpoints` | Set/replace breakpoints in a file (use `logMessage` for logpoints) |
32
34
  | `set_exception_breakpoints` | Break on exceptions |
35
+ | `list_breakpoints` | List currently-registered breakpoints for a session |
33
36
  | `get_state` | Get threads, stack trace, scopes, and variables |
34
37
  | `evaluate` | Evaluate an expression in the debuggee context |
35
38
  | `step` | Continue, next, stepIn, stepOut, or pause |
package/SKILL.md CHANGED
@@ -7,56 +7,203 @@ description: Use when debugging a program — when you need to set breakpoints,
7
7
 
8
8
  You have access to a real debugger via the `dap` MCP server. **Use it instead of adding print/log statements** whenever you need to understand runtime behavior, track down a bug, or verify a fix.
9
9
 
10
- ## When to Use the Debugger
10
+ ## When to switch from print() to the debugger
11
11
 
12
- - A test fails and the cause is not obvious from the error message or stack trace
13
- - You need to inspect the value of variables at a specific point in execution
14
- - You suspect a logic error in a loop, conditional, or data transformation
15
- - A function returns an unexpected result and you want to trace the execution path
16
- - You want to verify your fix actually changes behavior at the right point
12
+ Reach for the debugger — don't keep guessing — when any of these are true:
17
13
 
18
- ## Quick Start
14
+ - **Your first fix attempt was wrong.** Stop iterating on guesses. Inspect actual values.
15
+ - **You're about to add 3+ print statements.** Use a logpoint instead (see below) — same information, no code edits.
16
+ - **You need a value on iteration N of a loop.** Use `hitCondition: "N"` instead of conditional prints.
17
+ - **A test fails with a non-obvious traceback.** Launch the test under the debugger; you'll see locals at the failure site.
18
+ - **You're reasoning about state that's modified across many call sites.** A conditional breakpoint on the value beats grepping for assignments.
19
19
 
20
- 1. Check available adapters: call `list_adapters` to see what's installed.
21
- 2. Launch with breakpoints in a single call:
20
+ Print debugging is fine for: trivial one-line checks, scripts you'll throw away in the next message, or when you genuinely just want to see "did this code path execute?".
21
+
22
+ ## Logpoints — print debugging without editing code
23
+
24
+ A **logpoint** is a breakpoint that doesn't pause execution. It just emits a formatted message every time it's hit. This is the single most useful feature for AI agents.
25
+
26
+ Set it via the `logMessage` field on a breakpoint:
22
27
 
23
28
  ```
24
- debug_launch({
25
- adapter: "python", // or "node", "go", "cpp"
26
- program: "/absolute/path/to/file.py",
27
- stopOnEntry: false,
29
+ set_breakpoints({
30
+ file: "/abs/path/src/foo.py",
28
31
  breakpoints: [
29
- { file: "/absolute/path/to/file.py", line: 42 }
32
+ { line: 42, logMessage: "x={x} y={y} state={self.state}" },
33
+ { line: 88, logMessage: "iteration {i}: items={len(items)}" }
30
34
  ]
31
35
  })
32
36
  ```
33
37
 
34
- 3. When the program stops, the response already contains the full state (threads, stack, scopes, variables). Read it — no extra call needed.
35
- 4. Step through code with `step({ action: "next" })` — the response includes the new state.
36
- 5. Evaluate expressions with `evaluate({ expression: "len(items)" })`.
37
- 6. When done, call `debug_disconnect`.
38
+ Then `step({ action: "continue" })` and let the program run. Retrieve every log with `get_output`. You get the same information as inserting `print(f"x={x} y={y}")` at five places — without touching the source file, without re-running, and with the program's natural execution preserved.
39
+
40
+ **When to use logpoints over breakpoints**: when you want to *observe* values across many hits rather than *interrupt* execution to inspect one. They are the right tool ~70% of the time.
41
+
42
+ Expression syntax inside `{...}` depends on the adapter (debugpy: Python; node: JS). The expression is evaluated in the breakpoint's scope.
43
+
44
+ ## Debugging a failing test
45
+
46
+ This is the dominant case. Use the test runner as the program, with the specific test as an argument.
47
+
48
+ ### pytest
49
+
50
+ ```
51
+ debug_launch({
52
+ adapter: "python",
53
+ program: "/abs/path/.venv/bin/pytest",
54
+ args: ["tests/test_foo.py::test_bar", "-x", "--no-header"],
55
+ cwd: "/abs/path",
56
+ breakpoints: [{ file: "/abs/path/src/foo.py", line: 42 }],
57
+ stopOnEntry: false,
58
+ })
59
+ ```
60
+
61
+ For pytest, also consider setting `exceptionBreakpoints: ["raised"]` — pytest swallows exceptions before you see them, but the debugger will stop at the raise site.
62
+
63
+ ### vitest / jest (Node.js)
64
+
65
+ Requires the `node` adapter configured (see "Node.js setup" below). Use the runner binary from `node_modules/.bin`:
66
+
67
+ ```
68
+ debug_launch({
69
+ adapter: "node",
70
+ program: "${cwd}/node_modules/.bin/vitest",
71
+ args: ["run", "tests/foo.test.ts", "-t", "the failing test name"],
72
+ cwd: "/abs/path",
73
+ breakpoints: [{ file: "/abs/path/src/foo.ts", line: 42 }],
74
+ stopOnEntry: false,
75
+ })
76
+ ```
77
+
78
+ ### go test
79
+
80
+ ```
81
+ debug_launch({
82
+ adapter: "go",
83
+ program: "${cwd}", // delve takes a package dir, not a binary
84
+ args: ["-test.run", "TestFoo", "-test.v"],
85
+ cwd: "/abs/path",
86
+ breakpoints: [{ file: "/abs/path/foo.go", line: 42 }],
87
+ stopOnEntry: false,
88
+ })
89
+ ```
90
+
91
+ Delve uses `dlv test` semantics under the hood when the program is a package directory containing test files.
92
+
93
+ ### Node.js setup
94
+
95
+ If `list_adapters` shows no `node` entry, register it once (path depends on installed extension):
96
+
97
+ ```
98
+ configure_adapter({
99
+ type: "node",
100
+ runtime: "node",
101
+ program: "/abs/path/to/js-debug/src/dapDebugServer.js",
102
+ args: [],
103
+ scope: "global",
104
+ })
105
+ ```
106
+
107
+ `js-debug-dap` ships with the VS Code Node debugger extension. Alternatively use `@vscode/js-debug` from npm.
108
+
109
+ ## Quick Start (single-file program)
110
+
111
+ 1. `list_adapters` — see what's installed.
112
+ 2. Launch with breakpoints in one call:
113
+
114
+ ```
115
+ debug_launch({
116
+ adapter: "python",
117
+ program: "/abs/path/file.py",
118
+ stopOnEntry: false,
119
+ breakpoints: [{ file: "/abs/path/file.py", line: 42 }],
120
+ })
121
+ ```
122
+
123
+ 3. The response already contains the full state (threads, stack, scopes, variables) when the breakpoint is hit. No follow-up `get_state` needed.
124
+ 4. Step with `step({ action: "next" })` — response includes the new state.
125
+ 5. `evaluate({ expression: "len(items)" })` for ad-hoc inspection.
126
+ 6. `debug_disconnect` when done.
127
+
128
+ ## Stepping strategy
129
+
130
+ - **`next`** (step over) is the default choice. Use it unless you have a specific reason not to.
131
+ - **`stepIn`** only when you suspect the bug is *inside* the function being called — not at the call site. Stepping into library code will get you lost fast.
132
+ - **`stepOut`** to bail out of a function you stepped into by mistake.
133
+ - **`continue`** to run to the next breakpoint. With logpoints set, this is how you collect runtime traces.
134
+
135
+ If you find yourself stepping line-by-line for more than ~10 steps, you're probably doing it wrong. Set a breakpoint at the next interesting location and continue to it.
136
+
137
+ ## Worked example
138
+
139
+ Bug: `compute_total(items)` returns `0` when items contain a tax-exempt entry.
140
+
141
+ ```
142
+ # Launch under pytest with a breakpoint at the suspect line
143
+ debug_launch({
144
+ adapter: "python",
145
+ program: "/proj/.venv/bin/pytest",
146
+ args: ["tests/test_billing.py::test_tax_exempt", "-x"],
147
+ cwd: "/proj",
148
+ breakpoints: [{ file: "/proj/src/billing.py", line: 57 }],
149
+ stopOnEntry: false,
150
+ })
151
+
152
+ # Response (paraphrased):
153
+ # {
154
+ # sessionId: "s-1",
155
+ # status: "stopped",
156
+ # location: { file: ".../billing.py", line: 57, functionName: "compute_total" },
157
+ # scopes: [{ name: "Locals", variables: [
158
+ # { name: "items", value: "[{...}, {...}]", ... },
159
+ # { name: "subtotal", value: "100", ... },
160
+ # { name: "tax", value: "0", ... }
161
+ # ] }]
162
+ # }
163
+
164
+ # Inspect the exempt item more closely
165
+ evaluate({ expression: "[i for i in items if i.get('tax_exempt')]" })
166
+ # → [{name: 'book', price: 100, tax_exempt: True}]
167
+
168
+ # Step over to see what compute_total actually returns
169
+ step({ action: "next" })
170
+ # → see that `total = subtotal + tax` returns 100, not 0. Bug is upstream.
171
+
172
+ # Continue to verify nothing else changes the return
173
+ step({ action: "continue" })
174
+
175
+ debug_disconnect()
176
+ ```
177
+
178
+ Fix locally, re-run the test, done.
38
179
 
39
180
  ## Key Rules
40
181
 
41
182
  - **Do NOT call `get_state` after `step`** — `step` already returns the new stopped state.
42
- - **Omit `sessionId`** when only one debug session is active — the server resolves it automatically.
183
+ - **Omit `sessionId`** when only one debug session is active.
43
184
  - **Use absolute paths** for `program` and breakpoint `file` parameters.
44
- - **Prefer `stopOnEntry: false` with breakpoints** over `stopOnEntry: true` — it gets you directly to the interesting code.
45
- - **Check `get_output`** for program stdout/stderr if you need to see what the program printed.
185
+ - **Prefer `stopOnEntry: false` with breakpoints** — it gets you directly to the interesting code.
186
+ - **Use `get_output`** for program stdout/stderr (including logpoint output).
46
187
 
47
188
  ## Debugging Strategies
48
189
 
49
190
  ### Narrowing down a bug
50
- 1. Set a breakpoint where you know the state is still correct.
51
- 2. Set another breakpoint where the state is wrong.
52
- 3. Use `step` with `action: "next"` to walk through the code between them.
53
- 4. Use `evaluate` to check intermediate values.
191
+ 1. Set a breakpoint where state is known good.
192
+ 2. Set another where state is wrong.
193
+ 3. Use `step` with `next` to walk between them, or set intermediate logpoints.
54
194
 
55
195
  ### Understanding a crash
56
- 1. Launch with `exceptionBreakpoints: ["uncaughtExceptions"]`.
57
- 2. When it stops on the exception, inspect the stack and variables.
196
+ 1. Launch with `exceptionBreakpoints: ["uncaughtExceptions"]` (or `"raised"` for any raise).
197
+ 2. When stopped at the exception, inspect stack and locals.
58
198
 
59
199
  ### Verifying a fix
60
200
  1. Set a breakpoint at the fixed code.
61
- 2. Run to it, inspect variables, confirm the new behavior.
201
+ 2. Run to it, inspect variables, confirm new behavior.
62
202
  3. Continue to verify the program completes successfully.
203
+
204
+ ### Observing a hot loop without stopping it
205
+ 1. Set logpoints at the relevant lines with `{i}` and any state expression.
206
+ 2. `continue`.
207
+ 3. Read the trace via `get_output`. For long-running programs, poll incrementally:
208
+ pass the `nextSince` value from each call as the `since` parameter of the next call
209
+ to receive only newly-emitted entries.
package/TODO.md ADDED
@@ -0,0 +1,32 @@
1
+ # DAP MCP Server — Improvement TODO
2
+
3
+ Findings from a self-review of how usable this MCP is for an AI agent. Ordered by priority (descending). Check items off as completed.
4
+
5
+ ## Tier 1 — Documentation (SKILL.md). High leverage, low risk.
6
+
7
+ - [x] **P1 — Logpoints section.** `breakpoints[].logMessage` is already wired through (session.ts:138) but is invisible in SKILL.md. It is the closest thing to a drop-in replacement for `print()` debugging: no code edits, captures every hit, no execution pause. Should have its own headline section with an example.
8
+ - [x] **P2 — "Debug a failing test" section.** Most real bugs surface inside test runners (pytest, vitest, jest, go test, cargo test). Currently zero guidance. Add a per-framework template for launching a single test under the debugger.
9
+ - [x] **P3 — "Switch from print() to debugger" triggers.** Concrete rules an AI agent will actually follow: "if first fix attempt was wrong, stop guessing"; "if about to add 3+ prints, use logpoints"; "if reasoning about iteration N, use `hitCondition`"; "if traceback is non-obvious, launch test under debugger".
10
+ - [x] **P4 — Stepping strategy.** Default to `next`. Use `stepIn` only when you suspect the bug is *inside* the called function. Otherwise agents get lost in library internals.
11
+ - [x] **P5 — Full worked example.** Real bug → real `debug_launch` call → real returned state → identified fix. Replaces all the abstract advice with one concrete walkthrough.
12
+ - [x] **P5b — Node.js adapter recipe.** Builtin was removed; add a ready-to-paste `configure_adapter` snippet for `js-debug-dap`.
13
+
14
+ ## Tier 2 — Code changes that materially improve UX.
15
+
16
+ - [x] **P6 — `debug_inspect` macro tool.** One call: launch, run to a breakpoint, evaluate a list of expressions, disconnect. Collapses a 5–7 round-trip workflow into 1. Most likely change to make me reach for the debugger reflexively.
17
+ - [x] **P7 — Flip `stopOnEntry` default to `false` when breakpoints are provided.** Today defaults to `true` regardless (server.ts:206). Doing the right thing automatically is better than relying on the agent reading the SKILL.md.
18
+ - [x] **P8 — Structured deep variables.** `getVariables` with `depth > 1` flattens children into the value string (session.ts:271–278). Hard for the AI to consume. Return nested `children: [...]` instead.
19
+ - [x] **P9 — `list_adapters` summary line + surfaced install hints.** Add `summary: "2/4 installed"` and lift `installHint` for missing adapters to the top level so it's scannable.
20
+ - [x] **P10 — `debug_run` macro tool.** Launch, run to completion, return all stdout/stderr. Zero breakpoints, zero round-trips. For "what does this script actually print/crash with?" cases.
21
+
22
+ ## Tier 3 — Polish.
23
+
24
+ - [x] **P11 — Better "session is not stopped" error.** Currently says "use step with action: pause" (server.ts:549) — bad advice (Python `pause` lands in arbitrary library code). Suggest "set a breakpoint where you want to inspect, then continue to it".
25
+ - [x] **P12 — Version drift.** `McpServer` registers as `0.1.0` (server.ts:48); package.json is `0.1.10`. Read from `package.json`.
26
+ - [x] **P13 — `list_breakpoints` tool.** After several `set_breakpoints` calls, no way to introspect what's set. Minor.
27
+
28
+ ## Tier 4 — Speculative.
29
+
30
+ - [x] **`get_output({ since })` for incremental polling.** Smaller substitute for streaming subscriptions. Caller passes the `nextSince` value from a prior call to receive only newly-emitted entries. Useful for watching logpoint output during `continue` against long-running programs.
31
+ - [ ] ~~Streaming output via MCP resource subscriptions.~~ AI agents don't watch streams in real time; the request/response pattern doesn't benefit. `since`-based polling covers the practical case.
32
+ - [ ] ~~Auto-detect test runner from program path.~~ Too magic; SKILL.md recipes (P2) make the right call explicit, and silent misfires would be worse than no detection at all.
@@ -20,6 +20,15 @@
20
20
  * SOFTWARE.
21
21
  */
22
22
  export type SessionState = "idle" | "configuring" | "running" | "stopped" | "terminated";
23
+ export interface Variable {
24
+ name: string;
25
+ value: string;
26
+ type?: string;
27
+ hasChildren: boolean;
28
+ variablesReference: number;
29
+ /** Populated when getVariables is called with depth > 1 and this variable has children. */
30
+ children?: Variable[];
31
+ }
23
32
  export interface StoppedState {
24
33
  status: "stopped";
25
34
  reason: string;
@@ -43,13 +52,7 @@ export interface StoppedState {
43
52
  }>;
44
53
  scopes: Array<{
45
54
  name: string;
46
- variables: Array<{
47
- name: string;
48
- value: string;
49
- type?: string;
50
- hasChildren: boolean;
51
- variablesReference: number;
52
- }>;
55
+ variables: Variable[];
53
56
  }>;
54
57
  output?: string[];
55
58
  }
@@ -68,6 +71,8 @@ export interface AdapterConfig {
68
71
  installHint?: string;
69
72
  }
70
73
  export interface OutputEntry {
74
+ /** Monotonic per-session sequence number assigned at push time. Survives ring-buffer eviction. */
75
+ seq: number;
71
76
  category: string;
72
77
  text: string;
73
78
  timestamp: string;
package/build/server.js CHANGED
@@ -23,10 +23,24 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
23
  import { z } from "zod";
24
24
  import os from "node:os";
25
25
  import path from "node:path";
26
+ import { readFileSync } from "node:fs";
27
+ import { fileURLToPath } from "node:url";
26
28
  import { AdapterRegistry } from "./adapters/registry.js";
27
29
  import { SessionManager } from "./session/session-manager.js";
28
30
  import { DebugSession } from "./session/session.js";
31
+ import { runDebugInspect } from "./session/inspect.js";
32
+ import { runDebugRun } from "./session/run.js";
29
33
  import { checkAdapterAvailable } from "./adapters/detect.js";
34
+ function readPackageVersion() {
35
+ try {
36
+ const here = path.dirname(fileURLToPath(import.meta.url));
37
+ const pkg = JSON.parse(readFileSync(path.join(here, "..", "package.json"), "utf-8"));
38
+ return pkg.version ?? "0.0.0";
39
+ }
40
+ catch {
41
+ return "0.0.0";
42
+ }
43
+ }
30
44
  export function createServer() {
31
45
  const globalDir = path.join(os.homedir(), ".dap-mcp");
32
46
  const projectDir = path.join(process.cwd(), ".dap-mcp");
@@ -38,46 +52,49 @@ export function createServer() {
38
52
  });
39
53
  const server = new McpServer({
40
54
  name: "dap-mcp-server",
41
- version: "0.1.0",
55
+ version: readPackageVersion(),
42
56
  }, {
43
57
  instructions: [
44
58
  "This server provides AI-driven debugging via the Debug Adapter Protocol (DAP).",
45
59
  "",
46
- "## Debugging Workflow",
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.",
47
67
  "",
48
- "A typical session follows this sequence:",
49
- "1. `list_adapters` — discover available debug adapters and check which are installed",
50
- "2. `debug_launch` (or `debug_attach`) — start a debug session. This performs the full DAP handshake,",
51
- " sets initial breakpoints, and launches the program. Returns a sessionId and initial stopped state.",
52
- "3. Inspect state with `get_state` — returns threads, call stack, scopes, and variables in one call.",
53
- " This is a coarse-grained tool designed to minimize round-trips; prefer it over multiple fine-grained queries.",
54
- "4. `step` — control execution (continue, next, stepIn, stepOut, pause). Each step action blocks until",
55
- " the program stops again and returns the new state, so you do NOT need to call get_state after stepping.",
56
- "5. `evaluate` — evaluate expressions in the context of a stopped frame (REPL, watch, or hover context).",
57
- "6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints during the session.",
58
- "7. `get_output` — retrieve buffered stdout/stderr/console output (ring buffer, max 10K lines).",
59
- "8. `debug_disconnect` — end the session. `debug_restart` replays the same launch config with preserved breakpoints.",
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.",
60
81
  "",
61
82
  "## Key Conventions",
62
83
  "",
63
- "- **Auto-session resolution**: All tools accept an optional `sessionId`. When only one session is active,",
64
- " you can omit it — the server resolves it automatically.",
65
- "- **Synchronous stepping**: `step` returns the new stopped state directly. Do not follow a step with get_state",
66
- " unless you need to change parameters like `variableDepth` or `threadId`.",
67
- "- **Coarse get_state**: Fetches threads + stack frames + scopes + variables in a single call.",
68
- " Use `depth` (default 10) and `variableDepth` (default 1) to control how much data is returned.",
69
- "- **Output buffering**: Program output is buffered in a 10K-line ring buffer. Oldest lines are dropped",
70
- " when the buffer is full. Use `get_output` with `clear: true` to consume and free buffer space.",
71
- "- **Adapter registration**: Use `configure_adapter` to register adapters. Configs are persisted to",
72
- " `~/.dap-mcp/adapters.json` (global) or `./.dap-mcp/adapters.json` (project), with project overriding global.",
73
- "- **Error handling**: All tool errors are returned as `isError: true` results with an error message.",
74
- " The session remains usable after most errors.",
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.",
75
92
  ].join("\n"),
76
93
  });
77
94
  // ────────────────────────────────────────────────────────────────────────────
78
95
  // 1. list_adapters
79
96
  // ────────────────────────────────────────────────────────────────────────────
80
- server.tool("list_adapters", "List all known debug adapters with their availability status on this machine.", {}, async () => {
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 () => {
81
98
  try {
82
99
  const adapters = registry.listAll();
83
100
  const results = await Promise.all(adapters.map(async (a) => {
@@ -85,8 +102,20 @@ export function createServer() {
85
102
  const available = cmd ? await checkAdapterAvailable(cmd) : false;
86
103
  return { ...a, available };
87
104
  }));
105
+ const available = results.filter((r) => r.available);
106
+ const missing = results
107
+ .filter((r) => !r.available)
108
+ .map((r) => ({ type: r.type, installHint: r.installHint }));
109
+ const summary = missing.length === 0
110
+ ? `${available.length}/${results.length} adapters available`
111
+ : `${available.length}/${results.length} adapters available; missing: ${missing.map((m) => m.type).join(", ")}`;
88
112
  return {
89
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: JSON.stringify({ summary, adapters: results, missing }, null, 2),
117
+ },
118
+ ],
90
119
  };
91
120
  }
92
121
  catch (err) {
@@ -138,7 +167,10 @@ export function createServer() {
138
167
  args: z.array(z.string()).optional().describe("Program arguments"),
139
168
  cwd: z.string().optional().describe("Working directory for the program"),
140
169
  env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
141
- stopOnEntry: z.boolean().optional().describe("Stop on program entry (default: true)"),
170
+ stopOnEntry: z
171
+ .boolean()
172
+ .optional()
173
+ .describe("Stop on program entry. Defaults to false when breakpoints are provided (run to first breakpoint), true otherwise."),
142
174
  breakpoints: z
143
175
  .array(z.object({
144
176
  file: z.string().describe("Source file path"),
@@ -165,7 +197,8 @@ export function createServer() {
165
197
  .optional()
166
198
  .describe("Override adapter config inline instead of looking up from registry"),
167
199
  }, async ({ adapter, program, args, cwd, env, stopOnEntry, breakpoints, exceptionBreakpoints, adapterConfig, }) => {
168
- const shouldStop = stopOnEntry !== false;
200
+ // Default: if breakpoints are provided, run to the first one; otherwise stop at entry.
201
+ const shouldStop = stopOnEntry ?? !(breakpoints !== undefined && breakpoints.length > 0);
169
202
  let session;
170
203
  try {
171
204
  const config = adapterConfig ??
@@ -463,7 +496,7 @@ export function createServer() {
463
496
  content: [
464
497
  {
465
498
  type: "text",
466
- text: `Error: Session is not stopped (current state: ${session.state}). Use 'step' with action 'pause' to stop first.`,
499
+ 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.`,
467
500
  },
468
501
  ],
469
502
  isError: true,
@@ -575,17 +608,21 @@ export function createServer() {
575
608
  // ────────────────────────────────────────────────────────────────────────────
576
609
  // 10. get_output
577
610
  // ────────────────────────────────────────────────────────────────────────────
578
- server.tool("get_output", "Get buffered stdout/stderr and debug adapter output for a session.", {
611
+ server.tool("get_output", "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.", {
579
612
  sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
580
613
  category: z
581
614
  .string()
582
615
  .optional()
583
616
  .describe("Filter by output category: 'stdout', 'stderr', 'console', etc."),
584
617
  clear: z.boolean().optional().describe("Clear the output buffer after reading (default: false)"),
585
- }, async ({ sessionId, category, clear }) => {
618
+ since: z
619
+ .number()
620
+ .optional()
621
+ .describe("Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."),
622
+ }, async ({ sessionId, category, clear, since }) => {
586
623
  try {
587
624
  const session = sessions.resolve(sessionId);
588
- const result = session.getOutput(category, clear ?? false);
625
+ const result = session.getOutput(category, clear ?? false, since);
589
626
  return {
590
627
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
591
628
  };
@@ -715,6 +752,132 @@ export function createServer() {
715
752
  }
716
753
  });
717
754
  // ────────────────────────────────────────────────────────────────────────────
755
+ // 13. debug_inspect
756
+ // ────────────────────────────────────────────────────────────────────────────
757
+ server.tool("debug_inspect", "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.", {
758
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
759
+ program: z.string().describe("Path to the program to debug"),
760
+ args: z.array(z.string()).optional().describe("Program arguments"),
761
+ cwd: z.string().optional().describe("Working directory for the program"),
762
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
763
+ at: z
764
+ .object({
765
+ file: z.string().describe("Absolute path to the source file"),
766
+ line: z.number().describe("Line number (1-based)"),
767
+ condition: z.string().optional().describe("Conditional breakpoint expression"),
768
+ hitCondition: z.string().optional().describe("Hit-count condition (e.g. '5' for 5th hit)"),
769
+ })
770
+ .describe("Breakpoint location to stop at"),
771
+ expressions: z
772
+ .array(z.string())
773
+ .optional()
774
+ .describe("Expressions to evaluate when stopped at the breakpoint. Omit to just verify the line is reached."),
775
+ timeout: z
776
+ .number()
777
+ .optional()
778
+ .describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
779
+ adapterConfig: z
780
+ .object({
781
+ type: z.string(),
782
+ program: z.string(),
783
+ runtime: z.string().optional(),
784
+ args: z.array(z.string()).optional(),
785
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
786
+ installHint: z.string().optional(),
787
+ })
788
+ .optional()
789
+ .describe("Override adapter config inline"),
790
+ }, async (opts) => {
791
+ try {
792
+ const result = await runDebugInspect(registry, sessions, opts);
793
+ return {
794
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
795
+ };
796
+ }
797
+ catch (err) {
798
+ return {
799
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
800
+ isError: true,
801
+ };
802
+ }
803
+ });
804
+ // ────────────────────────────────────────────────────────────────────────────
805
+ // 14. list_breakpoints
806
+ // ────────────────────────────────────────────────────────────────────────────
807
+ server.tool("list_breakpoints", "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.", {
808
+ sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
809
+ }, async ({ sessionId }) => {
810
+ try {
811
+ const session = sessions.resolve(sessionId);
812
+ const bps = session.getBreakpoints();
813
+ const files = Array.from(bps.entries()).map(([file, breakpoints]) => ({
814
+ file,
815
+ breakpoints,
816
+ }));
817
+ const totalCount = files.reduce((n, f) => n + f.breakpoints.length, 0);
818
+ return {
819
+ content: [
820
+ {
821
+ type: "text",
822
+ text: JSON.stringify({ count: totalCount, files }, null, 2),
823
+ },
824
+ ],
825
+ };
826
+ }
827
+ catch (err) {
828
+ return {
829
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
830
+ isError: true,
831
+ };
832
+ }
833
+ });
834
+ // ────────────────────────────────────────────────────────────────────────────
835
+ // 15. debug_run
836
+ // ────────────────────────────────────────────────────────────────────────────
837
+ server.tool("debug_run", "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.", {
838
+ adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
839
+ program: z.string().describe("Path to the program to debug"),
840
+ args: z.array(z.string()).optional().describe("Program arguments"),
841
+ cwd: z.string().optional().describe("Working directory for the program"),
842
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
843
+ logpoints: z
844
+ .array(z.object({
845
+ file: z.string().describe("Absolute path to the source file"),
846
+ line: z.number().describe("Line number (1-based)"),
847
+ logMessage: z.string().describe("Message template; expressions in {braces} are evaluated in scope"),
848
+ }))
849
+ .optional()
850
+ .describe("Logpoints to emit per-hit messages without pausing execution"),
851
+ timeout: z
852
+ .number()
853
+ .optional()
854
+ .describe("Total wall-clock timeout for the whole run in milliseconds (default: 60000)"),
855
+ adapterConfig: z
856
+ .object({
857
+ type: z.string(),
858
+ program: z.string(),
859
+ runtime: z.string().optional(),
860
+ args: z.array(z.string()).optional(),
861
+ launchDefaults: z.record(z.string(), z.unknown()).optional(),
862
+ installHint: z.string().optional(),
863
+ })
864
+ .optional()
865
+ .describe("Override adapter config inline"),
866
+ }, async (opts) => {
867
+ try {
868
+ const result = await runDebugRun(registry, sessions, opts);
869
+ return {
870
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
871
+ };
872
+ }
873
+ catch (err) {
874
+ return {
875
+ content: [{ type: "text", text: `Error: ${String(err)}` }],
876
+ isError: true,
877
+ };
878
+ }
879
+ });
880
+ // ────────────────────────────────────────────────────────────────────────────
718
881
  // Resource: dap://sessions
719
882
  // ────────────────────────────────────────────────────────────────────────────
720
883
  // NOTE: This resource does not emit change notifications.