dap-mcp-server 0.1.10 → 0.1.12

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.
@@ -0,0 +1,160 @@
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 { AdapterRegistry } from "../adapters/registry.js";
24
+ import { SessionManager } from "./session-manager.js";
25
+ import { DebugSession } from "./session.js";
26
+ import { AdapterConfig } from "../dap/types.js";
27
+
28
+ export interface InspectOptions {
29
+ adapter: string;
30
+ program: string;
31
+ args?: string[];
32
+ cwd?: string;
33
+ env?: Record<string, string>;
34
+ at: {
35
+ file: string;
36
+ line: number;
37
+ condition?: string;
38
+ hitCondition?: string;
39
+ };
40
+ expressions?: string[];
41
+ timeout?: number;
42
+ adapterConfig?: AdapterConfig;
43
+ }
44
+
45
+ export interface InspectValueResult {
46
+ result?: string;
47
+ type?: string;
48
+ error?: string;
49
+ }
50
+
51
+ export interface InspectResult {
52
+ hit: boolean;
53
+ reason?: string;
54
+ location?: { file: string; line: number; functionName?: string; column?: number };
55
+ values?: Record<string, InspectValueResult>;
56
+ output?: string[];
57
+ status?: string;
58
+ }
59
+
60
+ /**
61
+ * Launch a program, run until it hits the given breakpoint, evaluate a set of
62
+ * expressions in that frame, and disconnect. Collapses a multi-call workflow
63
+ * into a single operation suitable for "what is X at line N?" debugging.
64
+ */
65
+ export async function runDebugInspect(
66
+ registry: AdapterRegistry,
67
+ sessions: SessionManager,
68
+ opts: InspectOptions
69
+ ): Promise<InspectResult> {
70
+ const config = opts.adapterConfig ?? registry.get(opts.adapter);
71
+ if (!config) {
72
+ throw new Error(
73
+ `Adapter '${opts.adapter}' not found. Use list_adapters to see available adapters or configure_adapter to register one.`
74
+ );
75
+ }
76
+
77
+ const id = sessions.generateId();
78
+ const session = new DebugSession(id, config);
79
+ sessions.add(session);
80
+
81
+ try {
82
+ await session.initialize();
83
+
84
+ const launchArgs: Record<string, unknown> = {
85
+ ...config.launchDefaults,
86
+ program: opts.program,
87
+ stopOnEntry: false,
88
+ };
89
+ if (opts.args !== undefined) launchArgs.args = opts.args;
90
+ if (opts.cwd !== undefined) launchArgs.cwd = opts.cwd;
91
+ if (opts.env !== undefined) launchArgs.env = opts.env;
92
+ // delve: mirror cwd → dlvCwd so `go build` finds go.mod from the right dir.
93
+ if (config.type === "go" && opts.cwd !== undefined && launchArgs.dlvCwd === undefined) {
94
+ launchArgs.dlvCwd = opts.cwd;
95
+ }
96
+
97
+ const launchPromise = session.launch(launchArgs);
98
+ // Suppress unhandled rejection: the real `await launchPromise` below will
99
+ // still throw if launch fails. Without this, an early launch rejection
100
+ // (e.g. adapter rejects the request) crashes the MCP server.
101
+ launchPromise.catch(() => { /* handled at await */ });
102
+ await session.waitForInitialized(10000);
103
+
104
+ await session.setBreakpoints(opts.at.file, [
105
+ {
106
+ line: opts.at.line,
107
+ condition: opts.at.condition,
108
+ hitCondition: opts.at.hitCondition,
109
+ },
110
+ ]);
111
+
112
+ await session.configurationDone();
113
+ await launchPromise;
114
+
115
+ const waitMs = opts.timeout ?? 30000;
116
+ try {
117
+ await session.waitForStopped(waitMs);
118
+ } catch {
119
+ const { output } = session.getOutput();
120
+ return {
121
+ hit: false,
122
+ status: session.state,
123
+ output: output.map((e) => e.text),
124
+ };
125
+ }
126
+
127
+ const state = await session.collectState();
128
+ const values: Record<string, InspectValueResult> = {};
129
+ if (opts.expressions && opts.expressions.length > 0) {
130
+ // Some adapters (vscode-js-debug) require a frameId to evaluate names
131
+ // bound in the current function's scope. Use the top frame's id when
132
+ // available so locals resolve correctly.
133
+ const topFrameId = state.stackTrace?.[0]?.id;
134
+ for (const expr of opts.expressions) {
135
+ try {
136
+ const r = await session.evaluate(expr, topFrameId);
137
+ values[expr] = { result: r.result, type: r.type };
138
+ } catch (err) {
139
+ values[expr] = { error: err instanceof Error ? err.message : String(err) };
140
+ }
141
+ }
142
+ }
143
+
144
+ return {
145
+ hit: true,
146
+ reason: state.reason,
147
+ location: state.location,
148
+ values: opts.expressions && opts.expressions.length > 0 ? values : undefined,
149
+ output: state.output,
150
+ status: "stopped",
151
+ };
152
+ } finally {
153
+ sessions.remove(session.id);
154
+ try {
155
+ await session.disconnect();
156
+ } catch {
157
+ /* best-effort cleanup */
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,74 @@
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 { runDebugRun } from "./run.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
+ function freshSetup() {
35
+ return {
36
+ registry: new AdapterRegistry(
37
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-${Math.random()}-global`),
38
+ path.join(os.tmpdir(), `dap-test-${Date.now()}-${Math.random()}-project`)
39
+ ),
40
+ sessions: new SessionManager(),
41
+ };
42
+ }
43
+
44
+ describe("runDebugRun", () => {
45
+ it("runs a program to completion and returns its output", async () => {
46
+ const { registry, sessions } = freshSetup();
47
+ const result = await runDebugRun(registry, sessions, {
48
+ adapter: "mock",
49
+ program: "/test/app.py",
50
+ timeout: 5000,
51
+ adapterConfig: {
52
+ type: "mock",
53
+ runtime: "node",
54
+ program: MOCK_ADAPTER,
55
+ },
56
+ });
57
+
58
+ expect(result.terminated).toBe(true);
59
+ expect(result.status).toBe("terminated");
60
+ expect(result.output.some((s) => s.includes("Program started"))).toBe(true);
61
+ expect(result.output.some((s) => s.includes("Program completed"))).toBe(true);
62
+ expect(sessions.listAll()).toHaveLength(0);
63
+ });
64
+
65
+ it("rejects when adapter is unknown and no inline config provided", async () => {
66
+ const { registry, sessions } = freshSetup();
67
+ await expect(
68
+ runDebugRun(registry, sessions, {
69
+ adapter: "nonexistent",
70
+ program: "/test/app.py",
71
+ })
72
+ ).rejects.toThrow(/not found/);
73
+ });
74
+ });
@@ -0,0 +1,144 @@
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 { AdapterRegistry } from "../adapters/registry.js";
24
+ import { SessionManager } from "./session-manager.js";
25
+ import { DebugSession } from "./session.js";
26
+ import { AdapterConfig } from "../dap/types.js";
27
+
28
+ export interface RunOptions {
29
+ adapter: string;
30
+ program: string;
31
+ args?: string[];
32
+ cwd?: string;
33
+ env?: Record<string, string>;
34
+ /** Optional logpoints — break with logMessage emits formatted output without pausing. */
35
+ logpoints?: Array<{ file: string; line: number; logMessage: string }>;
36
+ /** Total wall-clock timeout for the whole run (default: 60_000ms). */
37
+ timeout?: number;
38
+ adapterConfig?: AdapterConfig;
39
+ }
40
+
41
+ export interface RunResult {
42
+ terminated: boolean;
43
+ output: string[];
44
+ status: string;
45
+ }
46
+
47
+ /**
48
+ * Launch a program with no stops, let it run to completion, and return the
49
+ * collected output. Use for "what does this script actually do / print / crash with?"
50
+ * cases where you don't need to inspect intermediate state. Pair with logpoints
51
+ * for printf-style debugging.
52
+ */
53
+ export async function runDebugRun(
54
+ registry: AdapterRegistry,
55
+ sessions: SessionManager,
56
+ opts: RunOptions
57
+ ): Promise<RunResult> {
58
+ const config = opts.adapterConfig ?? registry.get(opts.adapter);
59
+ if (!config) {
60
+ throw new Error(
61
+ `Adapter '${opts.adapter}' not found. Use list_adapters to see available adapters or configure_adapter to register one.`
62
+ );
63
+ }
64
+
65
+ const id = sessions.generateId();
66
+ const session = new DebugSession(id, config);
67
+ sessions.add(session);
68
+
69
+ const totalTimeout = opts.timeout ?? 60_000;
70
+ const deadline = Date.now() + totalTimeout;
71
+
72
+ try {
73
+ await session.initialize();
74
+
75
+ const launchArgs: Record<string, unknown> = {
76
+ ...config.launchDefaults,
77
+ program: opts.program,
78
+ stopOnEntry: false,
79
+ };
80
+ if (opts.args !== undefined) launchArgs.args = opts.args;
81
+ if (opts.cwd !== undefined) launchArgs.cwd = opts.cwd;
82
+ if (opts.env !== undefined) launchArgs.env = opts.env;
83
+ // delve: mirror cwd → dlvCwd so `go build` finds go.mod from the right dir.
84
+ if (config.type === "go" && opts.cwd !== undefined && launchArgs.dlvCwd === undefined) {
85
+ launchArgs.dlvCwd = opts.cwd;
86
+ }
87
+
88
+ const launchPromise = session.launch(launchArgs);
89
+ // Suppress unhandled rejection: the real `await launchPromise` below will
90
+ // still throw if launch fails.
91
+ launchPromise.catch(() => { /* handled at await */ });
92
+ await session.waitForInitialized(10000);
93
+
94
+ if (opts.logpoints && opts.logpoints.length > 0) {
95
+ const byFile = new Map<string, Array<{ line: number; logMessage: string }>>();
96
+ for (const lp of opts.logpoints) {
97
+ const existing = byFile.get(lp.file) ?? [];
98
+ existing.push({ line: lp.line, logMessage: lp.logMessage });
99
+ byFile.set(lp.file, existing);
100
+ }
101
+ for (const [file, bps] of byFile) {
102
+ await session.setBreakpoints(file, bps);
103
+ }
104
+ }
105
+
106
+ await session.configurationDone();
107
+ await launchPromise;
108
+
109
+ // Loop: if the program stops unexpectedly (e.g. exception, surprise breakpoint),
110
+ // continue. Exit when the program terminates or the deadline passes.
111
+ while (session.state !== "terminated") {
112
+ const remaining = deadline - Date.now();
113
+ if (remaining <= 0) break;
114
+
115
+ if (session.state === "stopped") {
116
+ await session.continueExecution();
117
+ }
118
+
119
+ try {
120
+ await Promise.race([
121
+ session.waitForTerminated(remaining),
122
+ session.waitForStopped(remaining),
123
+ ]);
124
+ } catch {
125
+ // Both rejected (timeout). Bail.
126
+ break;
127
+ }
128
+ }
129
+
130
+ const { output } = session.getOutput();
131
+ return {
132
+ terminated: session.state === "terminated",
133
+ output: output.map((e) => e.text),
134
+ status: session.state,
135
+ };
136
+ } finally {
137
+ sessions.remove(session.id);
138
+ try {
139
+ await session.disconnect();
140
+ } catch {
141
+ /* best-effort cleanup */
142
+ }
143
+ }
144
+ }
@@ -69,6 +69,48 @@ describe("DebugSession", () => {
69
69
  expect(output.output).toHaveLength(1);
70
70
  expect(output.output[0].text).toBe("hello world");
71
71
  expect(output.output[0].category).toBe("stdout");
72
+ expect(output.output[0].seq).toBe(1);
73
+ expect(output.nextSince).toBe(1);
74
+ });
75
+
76
+ it("returns only new entries when called with since", async () => {
77
+ session = new DebugSession("test-since", {
78
+ type: "mock",
79
+ runtime: "node",
80
+ program: MOCK_ADAPTER,
81
+ });
82
+ session.addOutput("stdout", "line 1");
83
+ session.addOutput("stdout", "line 2");
84
+ const first = session.getOutput();
85
+ expect(first.output).toHaveLength(2);
86
+ expect(first.nextSince).toBe(2);
87
+
88
+ session.addOutput("stdout", "line 3");
89
+ session.addOutput("stderr", "err 1");
90
+ const incremental = session.getOutput(undefined, false, first.nextSince);
91
+ expect(incremental.output.map((e) => e.text)).toEqual(["line 3", "err 1"]);
92
+ expect(incremental.nextSince).toBe(4);
93
+
94
+ // No new entries since the latest nextSince
95
+ const empty = session.getOutput(undefined, false, incremental.nextSince);
96
+ expect(empty.output).toHaveLength(0);
97
+ expect(empty.nextSince).toBe(4);
98
+ });
99
+
100
+ it("combines since with category filter", async () => {
101
+ session = new DebugSession("test-since-cat", {
102
+ type: "mock",
103
+ runtime: "node",
104
+ program: MOCK_ADAPTER,
105
+ });
106
+ session.addOutput("stdout", "out 1");
107
+ session.addOutput("stderr", "err 1");
108
+ session.addOutput("stdout", "out 2");
109
+ session.addOutput("stderr", "err 2");
110
+
111
+ const stderrOnly = session.getOutput("stderr", false, 1);
112
+ expect(stderrOnly.output.map((e) => e.text)).toEqual(["err 1", "err 2"]);
113
+ expect(stderrOnly.nextSince).toBe(4);
72
114
  });
73
115
 
74
116
  it("detects adapter crash", async () => {
@@ -29,6 +29,7 @@ import {
29
29
  OutputEntry,
30
30
  BreakpointConfig,
31
31
  BreakpointResult,
32
+ Variable,
32
33
  } from "../dap/types.js";
33
34
  import { DebugProtocol } from "@vscode/debugprotocol";
34
35
  import path from "node:path";
@@ -42,6 +43,7 @@ export class DebugSession {
42
43
  private adapterConfig: AdapterConfig;
43
44
  private _state: SessionState = "idle";
44
45
  private outputBuffer = new RingBuffer<OutputEntry>(MAX_OUTPUT_LINES);
46
+ private nextOutputSeq = 1;
45
47
  private breakpointsMap = new Map<string, BreakpointConfig[]>();
46
48
  private capabilities: DebugProtocol.Capabilities = {};
47
49
  private stoppedThreadId?: number;
@@ -51,6 +53,7 @@ export class DebugSession {
51
53
  private launchMode: "launch" | "attach" = "launch";
52
54
  private stoppedResolvers: Array<(event: DebugProtocol.StoppedEvent) => void> = [];
53
55
  private initializedResolvers: Array<() => void> = [];
56
+ private terminatedResolvers: Array<() => void> = [];
54
57
  private _initializedReceived = false;
55
58
 
56
59
  constructor(id: string, config: AdapterConfig) {
@@ -59,7 +62,10 @@ export class DebugSession {
59
62
  const runtime = config.runtime || "";
60
63
  const program = config.program;
61
64
  const args = config.args || [];
62
- this.client = new DAPClient(runtime, program, args);
65
+ this.client = new DAPClient(runtime, program, args, {
66
+ transport: config.transport,
67
+ portMatcher: config.portMatcher,
68
+ });
63
69
 
64
70
  this.client.on("event", (event: DebugProtocol.Event) => {
65
71
  this.handleEvent(event);
@@ -69,9 +75,16 @@ export class DebugSession {
69
75
  if (this._state !== "terminated") {
70
76
  this._state = "terminated";
71
77
  }
78
+ this.resolveTerminatedWaiters();
72
79
  });
73
80
  }
74
81
 
82
+ private resolveTerminatedWaiters(): void {
83
+ const resolvers = this.terminatedResolvers;
84
+ this.terminatedResolvers = [];
85
+ for (const r of resolvers) r();
86
+ }
87
+
75
88
  get state(): SessionState {
76
89
  return this._state;
77
90
  }
@@ -94,7 +107,12 @@ export class DebugSession {
94
107
  });
95
108
  this.capabilities = response.body || {};
96
109
  this._state = "configuring";
97
- await this.waitForInitialized(10000);
110
+ // Note: per DAP spec, the "initialized" event signals the adapter is
111
+ // ready for configuration (setBreakpoints, etc.). Different adapters
112
+ // emit it at different times: delve emits it immediately after the
113
+ // initialize response, but debugpy only emits it after `launch` is
114
+ // received. The orchestrator (server.ts) is responsible for waiting
115
+ // for it AFTER firing the launch/attach request — see waitForInitialized.
98
116
  return this.capabilities;
99
117
  } catch (err) {
100
118
  this._state = "terminated";
@@ -199,6 +217,29 @@ export class DebugSession {
199
217
  await this.client.sendRequest("pause", { threadId: tid });
200
218
  }
201
219
 
220
+ async waitForTerminated(timeout = DEFAULT_TIMEOUT): Promise<void> {
221
+ if (this._state === "terminated") return;
222
+ return new Promise<void>((resolve, reject) => {
223
+ let settled = false;
224
+ const wrapped = () => {
225
+ if (settled) return;
226
+ settled = true;
227
+ clearTimeout(timer);
228
+ const idx = this.terminatedResolvers.indexOf(wrapped);
229
+ if (idx !== -1) this.terminatedResolvers.splice(idx, 1);
230
+ resolve();
231
+ };
232
+ const timer = setTimeout(() => {
233
+ if (settled) return;
234
+ settled = true;
235
+ const idx = this.terminatedResolvers.indexOf(wrapped);
236
+ if (idx !== -1) this.terminatedResolvers.splice(idx, 1);
237
+ reject(new Error("timeout"));
238
+ }, timeout);
239
+ this.terminatedResolvers.push(wrapped);
240
+ });
241
+ }
242
+
202
243
  async waitForStopped(timeout = DEFAULT_TIMEOUT): Promise<DebugProtocol.StoppedEvent> {
203
244
  if (this._state === "stopped") {
204
245
  return {
@@ -251,17 +292,9 @@ export class DebugSession {
251
292
  async getVariables(
252
293
  variablesReference: number,
253
294
  depth = 1
254
- ): Promise<
255
- Array<{
256
- name: string;
257
- value: string;
258
- type?: string;
259
- hasChildren: boolean;
260
- variablesReference: number;
261
- }>
262
- > {
295
+ ): Promise<Variable[]> {
263
296
  const response = await this.client.sendRequest("variables", { variablesReference });
264
- const vars = ((response.body?.variables as DebugProtocol.Variable[]) || []).map((v) => ({
297
+ const vars: Variable[] = ((response.body?.variables as DebugProtocol.Variable[]) || []).map((v) => ({
265
298
  name: v.name,
266
299
  value: v.value,
267
300
  type: v.type,
@@ -271,8 +304,7 @@ export class DebugSession {
271
304
  if (depth > 1) {
272
305
  for (const v of vars) {
273
306
  if (v.hasChildren) {
274
- const children = await this.getVariables(v.variablesReference, depth - 1);
275
- v.value = `${v.value} { ${children.map((c) => `${c.name}: ${c.value}`).join(", ")} }`;
307
+ v.children = await this.getVariables(v.variablesReference, depth - 1);
276
308
  }
277
309
  }
278
310
  }
@@ -345,16 +377,28 @@ export class DebugSession {
345
377
  }
346
378
 
347
379
  addOutput(category: string, text: string): void {
348
- this.outputBuffer.push({ category, text, timestamp: new Date().toISOString() });
380
+ this.outputBuffer.push({
381
+ seq: this.nextOutputSeq++,
382
+ category,
383
+ text,
384
+ timestamp: new Date().toISOString(),
385
+ });
349
386
  }
350
387
 
351
388
  getOutput(
352
389
  category?: string,
353
- clear = false
354
- ): { output: OutputEntry[]; truncated: boolean } {
355
- const entries = category
356
- ? this.outputBuffer.getFiltered((e) => e.category === category)
357
- : this.outputBuffer.getAll();
390
+ clear = false,
391
+ since?: number
392
+ ): { output: OutputEntry[]; truncated: boolean; nextSince: number } {
393
+ const matches = (e: OutputEntry): boolean => {
394
+ if (category && e.category !== category) return false;
395
+ if (since !== undefined && e.seq <= since) return false;
396
+ return true;
397
+ };
398
+ const entries =
399
+ category !== undefined || since !== undefined
400
+ ? this.outputBuffer.getFiltered(matches)
401
+ : this.outputBuffer.getAll();
358
402
  const truncated = this.outputBuffer.truncated;
359
403
  if (clear) {
360
404
  if (category) {
@@ -363,7 +407,9 @@ export class DebugSession {
363
407
  this.outputBuffer.clear();
364
408
  }
365
409
  }
366
- return { output: entries, truncated };
410
+ const lastInResult = entries.length > 0 ? entries[entries.length - 1].seq : undefined;
411
+ const nextSince = lastInResult ?? since ?? this.nextOutputSeq - 1;
412
+ return { output: entries, truncated, nextSince };
367
413
  }
368
414
 
369
415
  async disconnect(terminateDebuggee?: boolean): Promise<void> {
@@ -429,6 +475,7 @@ export class DebugSession {
429
475
  case "terminated":
430
476
  case "exited":
431
477
  this._state = "terminated";
478
+ this.resolveTerminatedWaiters();
432
479
  break;
433
480
  }
434
481
  }
@@ -52,6 +52,8 @@ function send(msg) {
52
52
 
53
53
  let seq = 1;
54
54
  let currentLine = 1;
55
+ let breakpointCount = 0;
56
+ let stopOnEntry = false;
55
57
 
56
58
  function handleMessage(msg) {
57
59
  if (msg.type !== "request") return;
@@ -62,6 +64,7 @@ function handleMessage(msg) {
62
64
  send({ seq: seq++, type: "event", event: "initialized", body: {} });
63
65
  break;
64
66
  case "setBreakpoints":
67
+ breakpointCount += (msg.arguments.breakpoints || []).length;
65
68
  send({ seq: seq++, type: "response", request_seq: msg.seq, command: "setBreakpoints", success: true,
66
69
  body: { breakpoints: (msg.arguments.breakpoints || []).map((bp, i) => ({ id: i+1, verified: true, line: bp.line })) } });
67
70
  break;
@@ -70,12 +73,24 @@ function handleMessage(msg) {
70
73
  break;
71
74
  case "configurationDone":
72
75
  send({ seq: seq++, type: "response", request_seq: msg.seq, command: "configurationDone", success: true, body: {} });
76
+ // Real-adapter behavior: after configurationDone, if launch was non-stopOnEntry
77
+ // and breakpoints were set, the program runs and hits the first breakpoint.
78
+ if (!stopOnEntry && breakpointCount > 0) {
79
+ setTimeout(() => { send({ seq: seq++, type: "event", event: "stopped", body: { reason: "breakpoint", threadId: 1, allThreadsStopped: true } }); }, 50);
80
+ } else if (!stopOnEntry && breakpointCount === 0) {
81
+ // No breakpoints, no stop-on-entry: program runs to completion and terminates.
82
+ setTimeout(() => {
83
+ send({ seq: seq++, type: "event", event: "output", body: { category: "stdout", output: "Program completed\n" } });
84
+ send({ seq: seq++, type: "event", event: "terminated", body: {} });
85
+ }, 50);
86
+ }
73
87
  break;
74
88
  case "launch":
75
89
  currentLine = 1;
90
+ stopOnEntry = !!msg.arguments.stopOnEntry;
76
91
  send({ seq: seq++, type: "response", request_seq: msg.seq, command: "launch", success: true, body: {} });
77
92
  send({ seq: seq++, type: "event", event: "output", body: { category: "stdout", output: "Program started\n" } });
78
- if (msg.arguments.stopOnEntry) {
93
+ if (stopOnEntry) {
79
94
  setTimeout(() => { send({ seq: seq++, type: "event", event: "stopped", body: { reason: "entry", threadId: 1, allThreadsStopped: true } }); }, 50);
80
95
  }
81
96
  break;
@@ -105,6 +120,13 @@ function handleMessage(msg) {
105
120
  { name: "name", value: "'hello'", type: "str", variablesReference: 0 },
106
121
  { name: "data", value: "[1, 2, 3]", type: "list", variablesReference: 300 },
107
122
  ] } });
123
+ } else if (msg.arguments.variablesReference === 300) {
124
+ send({ seq: seq++, type: "response", request_seq: msg.seq, command: "variables", success: true,
125
+ body: { variables: [
126
+ { name: "0", value: "1", type: "int", variablesReference: 0 },
127
+ { name: "1", value: "2", type: "int", variablesReference: 0 },
128
+ { name: "2", value: "3", type: "int", variablesReference: 0 },
129
+ ] } });
108
130
  } else {
109
131
  send({ seq: seq++, type: "response", request_seq: msg.seq, command: "variables", success: true, body: { variables: [] } });
110
132
  }