dap-mcp-server 0.1.11 → 0.1.16
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 +25 -7
- package/SKILL.md +37 -186
- package/build/adapters/builtin.js +27 -3
- package/build/adapters/builtin.js.map +1 -1
- package/build/adapters/detect.d.ts +12 -0
- package/build/adapters/detect.js +91 -0
- package/build/adapters/detect.js.map +1 -1
- package/build/adapters/registry.js +35 -12
- package/build/adapters/registry.js.map +1 -1
- package/build/dap/dap-client.d.ts +71 -4
- package/build/dap/dap-client.js +324 -53
- package/build/dap/dap-client.js.map +1 -1
- package/build/dap/types.d.ts +17 -0
- package/build/diagnostics/doctor.d.ts +64 -0
- package/build/diagnostics/doctor.js +209 -0
- package/build/diagnostics/doctor.js.map +1 -0
- package/build/server.js +337 -83
- package/build/server.js.map +1 -1
- package/build/session/inspect.d.ts +6 -2
- package/build/session/inspect.js +38 -9
- package/build/session/inspect.js.map +1 -1
- package/build/session/run.js +10 -9
- package/build/session/run.js.map +1 -1
- package/build/session/session.d.ts +16 -0
- package/build/session/session.js +73 -10
- package/build/session/session.js.map +1 -1
- package/integrations/claude/dap-debugging/.claude-plugin/plugin.json +9 -0
- package/integrations/claude/dap-debugging/.mcp.json +8 -0
- package/integrations/claude/dap-debugging/skills/dap-debugging/SKILL.md +49 -0
- package/package.json +8 -3
- package/.forgejo/workflows/publish.yml +0 -63
- package/CLAUDE.md +0 -71
- package/TODO.md +0 -32
- package/docs/plans/2026-03-05-dap-wrapper-mcp-design.md +0 -326
- package/docs/plans/2026-03-05-dap-wrapper-mcp-implementation.md +0 -2815
- package/src/adapters/builtin.ts +0 -50
- package/src/adapters/detect.test.ts +0 -36
- package/src/adapters/detect.ts +0 -36
- package/src/adapters/registry.test.ts +0 -73
- package/src/adapters/registry.ts +0 -86
- package/src/dap/dap-client.test.ts +0 -87
- package/src/dap/dap-client.ts +0 -238
- package/src/dap/protocol.test.ts +0 -82
- package/src/dap/protocol.ts +0 -78
- package/src/dap/ring-buffer.test.ts +0 -82
- package/src/dap/ring-buffer.ts +0 -64
- package/src/dap/types.ts +0 -98
- package/src/index.ts +0 -30
- package/src/integration/debug-lifecycle.test.ts +0 -131
- package/src/server.ts +0 -1062
- package/src/session/inspect.test.ts +0 -126
- package/src/session/inspect.ts +0 -147
- package/src/session/run.test.ts +0 -74
- package/src/session/run.ts +0 -136
- package/src/session/session-manager.test.ts +0 -74
- package/src/session/session-manager.ts +0 -67
- package/src/session/session.test.ts +0 -126
- package/src/session/session.ts +0 -474
- package/src/test-fixtures/crash-adapter.js +0 -23
- package/src/test-fixtures/mock-adapter.js +0 -164
- package/src/test-fixtures/no-stop-adapter.js +0 -77
- package/src/test-fixtures/silent-adapter.js +0 -24
- package/tsconfig.json +0 -16
package/build/server.js
CHANGED
|
@@ -30,7 +30,8 @@ import { SessionManager } from "./session/session-manager.js";
|
|
|
30
30
|
import { DebugSession } from "./session/session.js";
|
|
31
31
|
import { runDebugInspect } from "./session/inspect.js";
|
|
32
32
|
import { runDebugRun } from "./session/run.js";
|
|
33
|
-
import {
|
|
33
|
+
import { inspectAdapterAvailability } from "./adapters/detect.js";
|
|
34
|
+
import { resolveProjectAdapter, runDebugDoctor } from "./diagnostics/doctor.js";
|
|
34
35
|
function readPackageVersion() {
|
|
35
36
|
try {
|
|
36
37
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -47,7 +48,7 @@ export function createServer() {
|
|
|
47
48
|
const registry = new AdapterRegistry(globalDir, projectDir);
|
|
48
49
|
const sessions = new SessionManager();
|
|
49
50
|
// Load persisted adapter configs (best-effort, don't block server start)
|
|
50
|
-
registry.load().catch((err) => {
|
|
51
|
+
const registryReady = registry.load().catch((err) => {
|
|
51
52
|
console.error("Warning: failed to load adapter configs:", err);
|
|
52
53
|
});
|
|
53
54
|
const server = new McpServer({
|
|
@@ -55,57 +56,227 @@ export function createServer() {
|
|
|
55
56
|
version: readPackageVersion(),
|
|
56
57
|
}, {
|
|
57
58
|
instructions: [
|
|
58
|
-
"
|
|
59
|
-
"",
|
|
60
|
-
"
|
|
61
|
-
"",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
" with `logMessage`) for printf-style debugging without modifying source.",
|
|
66
|
-
"- **`debug_launch` + step/evaluate** — Interactive multi-step session when you need to explore.",
|
|
67
|
-
"",
|
|
68
|
-
"## Interactive Debugging Workflow",
|
|
69
|
-
"",
|
|
70
|
-
"1. `list_adapters` — discover available adapters; the response includes a summary and install hints.",
|
|
71
|
-
"2. `debug_launch` (or `debug_attach`) — start a session. With breakpoints provided, `stopOnEntry` defaults",
|
|
72
|
-
" to false so the program runs to the first breakpoint. Returns sessionId + initial stopped state.",
|
|
73
|
-
"3. Inspect state with `get_state` — threads + call stack + scopes + variables in one call.",
|
|
74
|
-
"4. `step` — continue, next, stepIn, stepOut, pause. Blocks until stopped; returns new state directly.",
|
|
75
|
-
"5. `evaluate` — expressions in the context of a stopped frame.",
|
|
76
|
-
"6. `set_breakpoints` / `set_exception_breakpoints` — modify breakpoints mid-session. Use `logMessage`",
|
|
77
|
-
" on a breakpoint to make it a logpoint (emits formatted text without pausing).",
|
|
78
|
-
"7. `list_breakpoints` — see what's currently set.",
|
|
79
|
-
"8. `get_output` — retrieve stdout/stderr/console output (10K-line ring buffer).",
|
|
80
|
-
"9. `debug_disconnect` — end. `debug_restart` replays the launch config with preserved breakpoints.",
|
|
81
|
-
"",
|
|
82
|
-
"## Key Conventions",
|
|
83
|
-
"",
|
|
84
|
-
"- **Auto-session resolution**: tools accept an optional `sessionId`; with one active session, omit it.",
|
|
85
|
-
"- **Synchronous stepping**: `step` returns the new stopped state. Don't follow it with `get_state`.",
|
|
86
|
-
"- **Variable depth**: `get_state` accepts `variableDepth` (default 1). With `> 1`, container variables",
|
|
87
|
-
" expose their entries as a structured `children` array.",
|
|
88
|
-
"- **stopOnEntry default**: false when `debug_launch` is given breakpoints, true otherwise.",
|
|
89
|
-
"- **Adapter registration**: `configure_adapter` persists to `~/.dap-mcp/adapters.json` (global) or",
|
|
90
|
-
" `./.dap-mcp/adapters.json` (project); project overrides global.",
|
|
91
|
-
"- **Error handling**: tool errors return `isError: true`. The session usually remains usable.",
|
|
59
|
+
"Local source-code debugger using the Debug Adapter Protocol.",
|
|
60
|
+
"Use it when a test fails, a program crashes, runtime state is unclear, or code behavior cannot be explained statically.",
|
|
61
|
+
"Start with debug_doctor to detect the project and verify adapter readiness.",
|
|
62
|
+
"Prefer debug_at for a one-call breakpoint inspection: it selects the adapter, launches, waits, evaluates expressions, and cleans up.",
|
|
63
|
+
"Use debug_run when output or termination is enough. Use debug_launch only for interactive stepping or multiple inspection points.",
|
|
64
|
+
"For interactive sessions: debug_launch -> get_state/evaluate -> step -> debug_disconnect. A launch with breakpoints waits by default and reports stopped, terminated, or timeout explicitly.",
|
|
65
|
+
"Omit sessionId when exactly one session is active. Adapter-specific DAP fields belong in launchConfig. Tool errors use isError=true.",
|
|
92
66
|
].join("\n"),
|
|
93
67
|
});
|
|
68
|
+
const doctorOutputSchema = z.object({
|
|
69
|
+
cwd: z.string(),
|
|
70
|
+
ready: z.boolean(),
|
|
71
|
+
recommendedAdapter: z.string().optional(),
|
|
72
|
+
detected: z.object({
|
|
73
|
+
languages: z.array(z.string()),
|
|
74
|
+
testFrameworks: z.array(z.string()),
|
|
75
|
+
packageManager: z.string().optional(),
|
|
76
|
+
pythonPath: z.string().optional(),
|
|
77
|
+
launchJson: z.string().optional(),
|
|
78
|
+
}),
|
|
79
|
+
adapters: z.array(z.object({
|
|
80
|
+
type: z.string(),
|
|
81
|
+
source: z.enum(["builtin", "global", "project"]),
|
|
82
|
+
selected: z.boolean(),
|
|
83
|
+
available: z.boolean(),
|
|
84
|
+
command: z.string(),
|
|
85
|
+
reason: z.string().optional(),
|
|
86
|
+
installHint: z.string().optional(),
|
|
87
|
+
runtime: z.string().optional(),
|
|
88
|
+
program: z.string(),
|
|
89
|
+
transport: z.enum(["stdio", "tcp"]),
|
|
90
|
+
})),
|
|
91
|
+
nextSteps: z.array(z.string()),
|
|
92
|
+
});
|
|
93
|
+
const breakpointResultSchema = z.object({
|
|
94
|
+
id: z.number().optional(),
|
|
95
|
+
verified: z.boolean(),
|
|
96
|
+
line: z.number(),
|
|
97
|
+
message: z.string().optional(),
|
|
98
|
+
});
|
|
99
|
+
const debugAtOutputSchema = z.object({
|
|
100
|
+
adapter: z.string(),
|
|
101
|
+
hit: z.boolean(),
|
|
102
|
+
reason: z.string().optional(),
|
|
103
|
+
location: z
|
|
104
|
+
.object({
|
|
105
|
+
file: z.string(),
|
|
106
|
+
line: z.number(),
|
|
107
|
+
functionName: z.string().optional(),
|
|
108
|
+
column: z.number().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
111
|
+
values: z
|
|
112
|
+
.record(z.string(), z.object({
|
|
113
|
+
result: z.string().optional(),
|
|
114
|
+
type: z.string().optional(),
|
|
115
|
+
error: z.string().optional(),
|
|
116
|
+
}))
|
|
117
|
+
.optional(),
|
|
118
|
+
output: z.array(z.string()).optional(),
|
|
119
|
+
status: z.string().optional(),
|
|
120
|
+
timedOut: z.boolean().optional(),
|
|
121
|
+
breakpoint: breakpointResultSchema.optional(),
|
|
122
|
+
});
|
|
123
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
124
|
+
// High-level LLM entry points
|
|
125
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
server.registerTool("debug_doctor", {
|
|
127
|
+
title: "Check debugger readiness",
|
|
128
|
+
description: "Inspect a local project, detect its language/tooling, select the likely debug adapter, verify the complete adapter command, and return concrete remediation. Use this before debugging or when an adapter fails to start.",
|
|
129
|
+
inputSchema: z.object({
|
|
130
|
+
cwd: z.string().optional().describe("Project directory (default: server working directory)"),
|
|
131
|
+
adapter: z.string().optional().describe("Adapter to validate instead of auto-detecting one"),
|
|
132
|
+
}),
|
|
133
|
+
outputSchema: doctorOutputSchema,
|
|
134
|
+
annotations: {
|
|
135
|
+
readOnlyHint: true,
|
|
136
|
+
destructiveHint: false,
|
|
137
|
+
idempotentHint: true,
|
|
138
|
+
openWorldHint: false,
|
|
139
|
+
},
|
|
140
|
+
}, async (options) => {
|
|
141
|
+
try {
|
|
142
|
+
await registryReady;
|
|
143
|
+
const result = await runDebugDoctor(registry, options);
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
146
|
+
structuredContent: { ...result },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: `Error: ${String(err)}` }],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
server.registerTool("debug_at", {
|
|
157
|
+
title: "Inspect runtime state at a line",
|
|
158
|
+
description: "Primary one-shot debugging tool for bugs and failing behavior. Auto-selects a ready adapter, launches the program, stops at file:line, evaluates expressions, reports breakpoint/termination/timeout details, and cleans up.",
|
|
159
|
+
inputSchema: z.object({
|
|
160
|
+
cwd: z.string().optional().describe("Project and debuggee working directory"),
|
|
161
|
+
adapter: z.string().optional().describe("Adapter type; auto-detected from file/project when omitted"),
|
|
162
|
+
program: z
|
|
163
|
+
.string()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe("Launch target; defaults to file for Python/Node and cwd for Go"),
|
|
166
|
+
args: z.array(z.string()).optional().describe("Program arguments"),
|
|
167
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
|
|
168
|
+
file: z.string().describe("Source file containing the breakpoint"),
|
|
169
|
+
line: z.number().int().positive().describe("One-based breakpoint line"),
|
|
170
|
+
condition: z.string().optional().describe("Conditional breakpoint expression"),
|
|
171
|
+
hitCondition: z.string().optional().describe("Adapter-specific hit count condition"),
|
|
172
|
+
expressions: z.array(z.string()).optional().describe("Expressions to evaluate in the stopped frame"),
|
|
173
|
+
timeout: z.number().positive().optional().describe("Breakpoint wait timeout in milliseconds"),
|
|
174
|
+
launchConfig: z
|
|
175
|
+
.record(z.string(), z.unknown())
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("Adapter-specific DAP launch fields"),
|
|
178
|
+
}),
|
|
179
|
+
outputSchema: debugAtOutputSchema,
|
|
180
|
+
annotations: {
|
|
181
|
+
readOnlyHint: false,
|
|
182
|
+
destructiveHint: true,
|
|
183
|
+
idempotentHint: false,
|
|
184
|
+
openWorldHint: false,
|
|
185
|
+
},
|
|
186
|
+
}, async (options) => {
|
|
187
|
+
try {
|
|
188
|
+
await registryReady;
|
|
189
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
190
|
+
const sourceFile = path.resolve(cwd, options.file);
|
|
191
|
+
const resolved = await resolveProjectAdapter(registry, {
|
|
192
|
+
cwd,
|
|
193
|
+
adapter: options.adapter,
|
|
194
|
+
sourceFile,
|
|
195
|
+
});
|
|
196
|
+
const adapter = resolved.result.recommendedAdapter;
|
|
197
|
+
if (!adapter || !resolved.config || !resolved.result.ready) {
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `Debugger is not ready:\n${JSON.stringify(resolved.result, null, 2)}`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
isError: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const hasLaunchTarget = options.launchConfig !== undefined &&
|
|
209
|
+
["program", "module", "code"].some((key) => options.launchConfig?.[key] !== undefined);
|
|
210
|
+
if (!options.program && !hasLaunchTarget && adapter === "cppdbg") {
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: "Error: C/C++ debugging requires the compiled executable in program or launchConfig.program.",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
isError: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const program = options.program ??
|
|
222
|
+
(hasLaunchTarget ? undefined : adapter === "go" ? cwd : sourceFile);
|
|
223
|
+
const result = await runDebugInspect(registry, sessions, {
|
|
224
|
+
adapter,
|
|
225
|
+
adapterConfig: resolved.config,
|
|
226
|
+
program,
|
|
227
|
+
args: options.args,
|
|
228
|
+
cwd,
|
|
229
|
+
env: options.env,
|
|
230
|
+
at: {
|
|
231
|
+
file: sourceFile,
|
|
232
|
+
line: options.line,
|
|
233
|
+
condition: options.condition,
|
|
234
|
+
hitCondition: options.hitCondition,
|
|
235
|
+
},
|
|
236
|
+
expressions: options.expressions,
|
|
237
|
+
timeout: options.timeout,
|
|
238
|
+
launchConfig: options.launchConfig,
|
|
239
|
+
});
|
|
240
|
+
const structuredContent = { adapter, ...result };
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{ type: "text", text: JSON.stringify(structuredContent, null, 2) },
|
|
244
|
+
],
|
|
245
|
+
structuredContent,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: "text", text: `Error: ${String(err)}` }],
|
|
251
|
+
isError: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
94
255
|
// ────────────────────────────────────────────────────────────────────────────
|
|
95
256
|
// 1. list_adapters
|
|
96
257
|
// ────────────────────────────────────────────────────────────────────────────
|
|
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.", {},
|
|
258
|
+
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.", {}, {
|
|
259
|
+
readOnlyHint: true,
|
|
260
|
+
destructiveHint: false,
|
|
261
|
+
idempotentHint: true,
|
|
262
|
+
openWorldHint: false,
|
|
263
|
+
}, async () => {
|
|
98
264
|
try {
|
|
265
|
+
await registryReady;
|
|
99
266
|
const adapters = registry.listAll();
|
|
100
267
|
const results = await Promise.all(adapters.map(async (a) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
return { ...a, available };
|
|
268
|
+
const availability = await inspectAdapterAvailability(a);
|
|
269
|
+
return { ...a, ...availability };
|
|
104
270
|
}));
|
|
105
271
|
const available = results.filter((r) => r.available);
|
|
106
272
|
const missing = results
|
|
107
273
|
.filter((r) => !r.available)
|
|
108
|
-
.map((r) => ({
|
|
274
|
+
.map((r) => ({
|
|
275
|
+
type: r.type,
|
|
276
|
+
command: r.command,
|
|
277
|
+
reason: r.reason,
|
|
278
|
+
installHint: r.installHint,
|
|
279
|
+
}));
|
|
109
280
|
const summary = missing.length === 0
|
|
110
281
|
? `${available.length}/${results.length} adapters available`
|
|
111
282
|
: `${available.length}/${results.length} adapters available; missing: ${missing.map((m) => m.type).join(", ")}`;
|
|
@@ -135,10 +306,27 @@ export function createServer() {
|
|
|
135
306
|
args: z.array(z.string()).optional().describe("Additional arguments to pass to the adapter"),
|
|
136
307
|
launchDefaults: z.record(z.string(), z.unknown()).optional().describe("Default launch configuration properties"),
|
|
137
308
|
installHint: z.string().optional().describe("Hint shown when adapter is not installed"),
|
|
309
|
+
transport: z.enum(["stdio", "tcp"]).optional().describe("DAP transport used by the adapter"),
|
|
310
|
+
portMatcher: z.string().optional().describe("Regex whose first capture group is the TCP port"),
|
|
138
311
|
scope: z.enum(["global", "project"]).optional().describe("Where to persist the adapter config (default: project)"),
|
|
139
|
-
},
|
|
312
|
+
}, {
|
|
313
|
+
readOnlyHint: false,
|
|
314
|
+
destructiveHint: true,
|
|
315
|
+
idempotentHint: true,
|
|
316
|
+
openWorldHint: false,
|
|
317
|
+
}, async ({ type, program, runtime, args, launchDefaults, installHint, transport, portMatcher, scope }) => {
|
|
140
318
|
try {
|
|
141
|
-
|
|
319
|
+
await registryReady;
|
|
320
|
+
const config = {
|
|
321
|
+
type,
|
|
322
|
+
program,
|
|
323
|
+
runtime,
|
|
324
|
+
args,
|
|
325
|
+
launchDefaults,
|
|
326
|
+
installHint,
|
|
327
|
+
transport,
|
|
328
|
+
portMatcher,
|
|
329
|
+
};
|
|
142
330
|
const saveScope = scope ?? "project";
|
|
143
331
|
registry.register(config, saveScope);
|
|
144
332
|
await registry.save(saveScope);
|
|
@@ -161,12 +349,16 @@ export function createServer() {
|
|
|
161
349
|
// ────────────────────────────────────────────────────────────────────────────
|
|
162
350
|
// 3. debug_launch
|
|
163
351
|
// ────────────────────────────────────────────────────────────────────────────
|
|
164
|
-
server.tool("debug_launch", "Launch
|
|
352
|
+
server.tool("debug_launch", "Launch an interactive debug session. With a breakpoint or stopOnEntry, waits by default and reports whether execution stopped, terminated, or timed out.", {
|
|
165
353
|
adapter: z.string().describe("Adapter type to use (e.g. 'python', 'node')"),
|
|
166
|
-
program: z.string().describe("Path to the program to debug"),
|
|
354
|
+
program: z.string().optional().describe("Path to the program to debug; may instead be supplied through launchConfig"),
|
|
167
355
|
args: z.array(z.string()).optional().describe("Program arguments"),
|
|
168
356
|
cwd: z.string().optional().describe("Working directory for the program"),
|
|
169
357
|
env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
|
|
358
|
+
launchConfig: z
|
|
359
|
+
.record(z.string(), z.unknown())
|
|
360
|
+
.optional()
|
|
361
|
+
.describe("Adapter-specific DAP launch properties; explicit common fields take precedence"),
|
|
170
362
|
stopOnEntry: z
|
|
171
363
|
.boolean()
|
|
172
364
|
.optional()
|
|
@@ -185,6 +377,11 @@ export function createServer() {
|
|
|
185
377
|
.array(z.string())
|
|
186
378
|
.optional()
|
|
187
379
|
.describe("Exception filter IDs (e.g. ['uncaughtExceptions'])"),
|
|
380
|
+
waitForStop: z
|
|
381
|
+
.boolean()
|
|
382
|
+
.optional()
|
|
383
|
+
.describe("Wait for stopped/terminated outcome; defaults to true when a stop is expected"),
|
|
384
|
+
timeout: z.number().positive().optional().describe("Wait timeout in milliseconds (default: 30000)"),
|
|
188
385
|
adapterConfig: z
|
|
189
386
|
.object({
|
|
190
387
|
type: z.string(),
|
|
@@ -193,14 +390,19 @@ export function createServer() {
|
|
|
193
390
|
args: z.array(z.string()).optional(),
|
|
194
391
|
launchDefaults: z.record(z.string(), z.unknown()).optional(),
|
|
195
392
|
installHint: z.string().optional(),
|
|
393
|
+
transport: z.enum(["stdio", "tcp"]).optional(),
|
|
394
|
+
portMatcher: z.string().optional(),
|
|
196
395
|
})
|
|
197
396
|
.optional()
|
|
198
397
|
.describe("Override adapter config inline instead of looking up from registry"),
|
|
199
|
-
}, async ({ adapter, program, args, cwd, env, stopOnEntry, breakpoints, exceptionBreakpoints, adapterConfig, }) => {
|
|
200
|
-
// Default:
|
|
201
|
-
const
|
|
398
|
+
}, async ({ adapter, program, args, cwd, env, launchConfig, stopOnEntry, breakpoints, exceptionBreakpoints, waitForStop, timeout, adapterConfig, }) => {
|
|
399
|
+
// Default: run to a configured breakpoint/exception; otherwise stop at entry.
|
|
400
|
+
const expectsConfiguredStop = (breakpoints !== undefined && breakpoints.length > 0) ||
|
|
401
|
+
(exceptionBreakpoints !== undefined && exceptionBreakpoints.length > 0);
|
|
402
|
+
const shouldStop = stopOnEntry ?? !expectsConfiguredStop;
|
|
202
403
|
let session;
|
|
203
404
|
try {
|
|
405
|
+
await registryReady;
|
|
204
406
|
const config = adapterConfig ??
|
|
205
407
|
registry.get(adapter);
|
|
206
408
|
if (!config) {
|
|
@@ -222,19 +424,38 @@ export function createServer() {
|
|
|
222
424
|
// Build launch request
|
|
223
425
|
const launchArgs = {
|
|
224
426
|
...config.launchDefaults,
|
|
225
|
-
|
|
427
|
+
...launchConfig,
|
|
226
428
|
stopOnEntry: shouldStop,
|
|
227
429
|
};
|
|
430
|
+
if (program !== undefined)
|
|
431
|
+
launchArgs.program = program;
|
|
228
432
|
if (args !== undefined)
|
|
229
433
|
launchArgs.args = args;
|
|
230
434
|
if (cwd !== undefined)
|
|
231
435
|
launchArgs.cwd = cwd;
|
|
232
436
|
if (env !== undefined)
|
|
233
437
|
launchArgs.env = env;
|
|
438
|
+
// Adapter-specific arg normalization. delve's `cwd` is the debuggee's
|
|
439
|
+
// working directory; the *build* (go build) runs from delve's own
|
|
440
|
+
// process cwd, controlled by `dlvCwd`. Mirror cwd → dlvCwd so a
|
|
441
|
+
// user-supplied cwd is found by `go build` looking for go.mod.
|
|
442
|
+
if (config.type === "go" && cwd !== undefined && launchArgs.dlvCwd === undefined) {
|
|
443
|
+
launchArgs.dlvCwd = cwd;
|
|
444
|
+
}
|
|
234
445
|
// Send launch first (don't await — real adapters like debugpy block
|
|
235
446
|
// the launch response until configurationDone arrives)
|
|
236
447
|
const launchPromise = session.launch(launchArgs);
|
|
448
|
+
// Suppress unhandled rejection: the real `await launchPromise` below
|
|
449
|
+
// will still throw if launch fails. Without this, an early launch
|
|
450
|
+
// rejection (e.g. adapter rejects the request) crashes the MCP server.
|
|
451
|
+
launchPromise.catch(() => { });
|
|
452
|
+
// Wait for the adapter to emit the "initialized" event before sending
|
|
453
|
+
// configuration requests. Some adapters (delve) emit it right after the
|
|
454
|
+
// initialize response; others (debugpy) only emit it after receiving
|
|
455
|
+
// launch — that's why we fire launch first and wait for initialized here.
|
|
456
|
+
await session.waitForInitialized(10000);
|
|
237
457
|
// Set breakpoints grouped by file
|
|
458
|
+
const breakpointResults = [];
|
|
238
459
|
if (breakpoints && breakpoints.length > 0) {
|
|
239
460
|
const byFile = new Map();
|
|
240
461
|
for (const bp of breakpoints) {
|
|
@@ -243,12 +464,13 @@ export function createServer() {
|
|
|
243
464
|
byFile.set(bp.file, existing);
|
|
244
465
|
}
|
|
245
466
|
for (const [file, bps] of byFile) {
|
|
246
|
-
await session.setBreakpoints(file, bps.map((b) => ({
|
|
467
|
+
const results = await session.setBreakpoints(file, bps.map((b) => ({
|
|
247
468
|
line: b.line,
|
|
248
469
|
condition: b.condition,
|
|
249
470
|
hitCondition: b.hitCondition,
|
|
250
471
|
logMessage: b.logMessage,
|
|
251
472
|
})));
|
|
473
|
+
breakpointResults.push({ file, breakpoints: results });
|
|
252
474
|
}
|
|
253
475
|
}
|
|
254
476
|
// Set exception breakpoints
|
|
@@ -259,14 +481,23 @@ export function createServer() {
|
|
|
259
481
|
await session.configurationDone();
|
|
260
482
|
// Now await the launch response
|
|
261
483
|
await launchPromise;
|
|
484
|
+
const shouldWait = waitForStop ?? (shouldStop || expectsConfiguredStop);
|
|
262
485
|
let stateData;
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
486
|
+
if (shouldWait) {
|
|
487
|
+
const startedAt = Date.now();
|
|
488
|
+
const outcome = await session.waitForStopOrTermination(timeout ?? 30000);
|
|
489
|
+
if (outcome.status === "stopped") {
|
|
266
490
|
stateData = (await session.collectState());
|
|
267
491
|
}
|
|
268
|
-
|
|
269
|
-
|
|
492
|
+
else if (outcome.status === "terminated") {
|
|
493
|
+
const { output } = session.getOutput();
|
|
494
|
+
stateData = {
|
|
495
|
+
status: "terminated",
|
|
496
|
+
output: output.map((entry) => entry.text),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
stateData = { status: session.state, timedOut: true, elapsedMs: Date.now() - startedAt };
|
|
270
501
|
}
|
|
271
502
|
}
|
|
272
503
|
else {
|
|
@@ -276,7 +507,7 @@ export function createServer() {
|
|
|
276
507
|
content: [
|
|
277
508
|
{
|
|
278
509
|
type: "text",
|
|
279
|
-
text: JSON.stringify({ sessionId: id, ...stateData }, null, 2),
|
|
510
|
+
text: JSON.stringify({ sessionId: id, breakpoints: breakpointResults, ...stateData }, null, 2),
|
|
280
511
|
},
|
|
281
512
|
],
|
|
282
513
|
};
|
|
@@ -309,12 +540,15 @@ export function createServer() {
|
|
|
309
540
|
args: z.array(z.string()).optional(),
|
|
310
541
|
launchDefaults: z.record(z.string(), z.unknown()).optional(),
|
|
311
542
|
installHint: z.string().optional(),
|
|
543
|
+
transport: z.enum(["stdio", "tcp"]).optional(),
|
|
544
|
+
portMatcher: z.string().optional(),
|
|
312
545
|
})
|
|
313
546
|
.optional()
|
|
314
547
|
.describe("Override adapter config inline"),
|
|
315
548
|
}, async ({ adapter, port, host, processId, attachConfig, adapterConfig }) => {
|
|
316
549
|
let session;
|
|
317
550
|
try {
|
|
551
|
+
await registryReady;
|
|
318
552
|
const config = adapterConfig ?? registry.get(adapter);
|
|
319
553
|
if (!config) {
|
|
320
554
|
return {
|
|
@@ -344,6 +578,10 @@ export function createServer() {
|
|
|
344
578
|
attachArgs.processId = processId;
|
|
345
579
|
// Send attach first (don't await — real adapters block until configurationDone)
|
|
346
580
|
const attachPromise = session.attach(attachArgs);
|
|
581
|
+
// Suppress unhandled rejection: the real `await attachPromise` below
|
|
582
|
+
// will still throw if attach fails.
|
|
583
|
+
attachPromise.catch(() => { });
|
|
584
|
+
await session.waitForInitialized(10000);
|
|
347
585
|
await session.configurationDone();
|
|
348
586
|
await attachPromise;
|
|
349
587
|
return {
|
|
@@ -435,6 +673,11 @@ export function createServer() {
|
|
|
435
673
|
.number()
|
|
436
674
|
.optional()
|
|
437
675
|
.describe("If running, wait up to this many milliseconds for the program to stop (default: 0 = don't wait)"),
|
|
676
|
+
}, {
|
|
677
|
+
readOnlyHint: true,
|
|
678
|
+
destructiveHint: false,
|
|
679
|
+
idempotentHint: true,
|
|
680
|
+
openWorldHint: false,
|
|
438
681
|
}, async ({ sessionId, threadId, depth, variableDepth, timeout }) => {
|
|
439
682
|
try {
|
|
440
683
|
const session = sessions.resolve(sessionId);
|
|
@@ -446,16 +689,13 @@ export function createServer() {
|
|
|
446
689
|
};
|
|
447
690
|
}
|
|
448
691
|
if (waitMs > 0 && session.state === "running") {
|
|
449
|
-
|
|
450
|
-
|
|
692
|
+
const outcome = await session.waitForStopOrTermination(waitMs);
|
|
693
|
+
if (outcome.status === "stopped") {
|
|
451
694
|
const state = await session.collectState(threadId, depth ?? 10, variableDepth ?? 1);
|
|
452
695
|
return {
|
|
453
696
|
content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
|
|
454
697
|
};
|
|
455
698
|
}
|
|
456
|
-
catch {
|
|
457
|
-
// Timed out — fall through to return running status
|
|
458
|
-
}
|
|
459
699
|
}
|
|
460
700
|
const { output } = session.getOutput();
|
|
461
701
|
return {
|
|
@@ -556,13 +796,10 @@ export function createServer() {
|
|
|
556
796
|
await session.pause(threadId);
|
|
557
797
|
break;
|
|
558
798
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
catch (err) {
|
|
563
|
-
// Timeout or terminated
|
|
799
|
+
const outcome = await session.waitForStopOrTermination(waitMs);
|
|
800
|
+
if (outcome.status !== "stopped") {
|
|
564
801
|
const elapsed = Date.now() - startTime;
|
|
565
|
-
if (
|
|
802
|
+
if (outcome.status === "terminated") {
|
|
566
803
|
const { output } = session.getOutput();
|
|
567
804
|
return {
|
|
568
805
|
content: [
|
|
@@ -582,17 +819,6 @@ export function createServer() {
|
|
|
582
819
|
],
|
|
583
820
|
};
|
|
584
821
|
}
|
|
585
|
-
if (session.state === "terminated") {
|
|
586
|
-
const { output } = session.getOutput();
|
|
587
|
-
return {
|
|
588
|
-
content: [
|
|
589
|
-
{
|
|
590
|
-
type: "text",
|
|
591
|
-
text: JSON.stringify({ status: "terminated", output: output.map((e) => e.text) }, null, 2),
|
|
592
|
-
},
|
|
593
|
-
],
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
822
|
const state = await session.collectState(threadId);
|
|
597
823
|
return {
|
|
598
824
|
content: [{ type: "text", text: JSON.stringify(state, null, 2) }],
|
|
@@ -619,6 +845,11 @@ export function createServer() {
|
|
|
619
845
|
.number()
|
|
620
846
|
.optional()
|
|
621
847
|
.describe("Return only entries with seq > since. Pass the `nextSince` value from a prior call to fetch incremental output."),
|
|
848
|
+
}, {
|
|
849
|
+
readOnlyHint: false,
|
|
850
|
+
destructiveHint: true,
|
|
851
|
+
idempotentHint: false,
|
|
852
|
+
openWorldHint: false,
|
|
622
853
|
}, async ({ sessionId, category, clear, since }) => {
|
|
623
854
|
try {
|
|
624
855
|
const session = sessions.resolve(sessionId);
|
|
@@ -711,6 +942,10 @@ export function createServer() {
|
|
|
711
942
|
await newSession.initialize();
|
|
712
943
|
// Send launch first (don't await — real adapters block until configurationDone)
|
|
713
944
|
const launchPromise = newSession.launch(launchConfig);
|
|
945
|
+
// Suppress unhandled rejection: the real `await launchPromise` below
|
|
946
|
+
// will still throw if launch fails.
|
|
947
|
+
launchPromise.catch(() => { });
|
|
948
|
+
await newSession.waitForInitialized(10000);
|
|
714
949
|
// Re-apply saved breakpoints
|
|
715
950
|
for (const [file, bps] of savedBreakpoints) {
|
|
716
951
|
await newSession.setBreakpoints(file, bps);
|
|
@@ -718,14 +953,18 @@ export function createServer() {
|
|
|
718
953
|
await newSession.configurationDone();
|
|
719
954
|
await launchPromise;
|
|
720
955
|
const stopOnEntry = launchConfig.stopOnEntry !== false;
|
|
956
|
+
const shouldWait = stopOnEntry || savedBreakpoints.size > 0;
|
|
721
957
|
let stateData;
|
|
722
|
-
if (
|
|
723
|
-
|
|
724
|
-
|
|
958
|
+
if (shouldWait) {
|
|
959
|
+
const outcome = await newSession.waitForStopOrTermination(30000);
|
|
960
|
+
if (outcome.status === "stopped") {
|
|
725
961
|
stateData = (await newSession.collectState());
|
|
726
962
|
}
|
|
727
|
-
|
|
728
|
-
stateData = {
|
|
963
|
+
else {
|
|
964
|
+
stateData = {
|
|
965
|
+
status: outcome.status === "timeout" ? newSession.state : "terminated",
|
|
966
|
+
timedOut: outcome.status === "timeout",
|
|
967
|
+
};
|
|
729
968
|
}
|
|
730
969
|
}
|
|
731
970
|
else {
|
|
@@ -776,6 +1015,10 @@ export function createServer() {
|
|
|
776
1015
|
.number()
|
|
777
1016
|
.optional()
|
|
778
1017
|
.describe("Milliseconds to wait for the breakpoint to be hit (default: 30000)"),
|
|
1018
|
+
launchConfig: z
|
|
1019
|
+
.record(z.string(), z.unknown())
|
|
1020
|
+
.optional()
|
|
1021
|
+
.describe("Adapter-specific DAP launch fields"),
|
|
779
1022
|
adapterConfig: z
|
|
780
1023
|
.object({
|
|
781
1024
|
type: z.string(),
|
|
@@ -784,11 +1027,14 @@ export function createServer() {
|
|
|
784
1027
|
args: z.array(z.string()).optional(),
|
|
785
1028
|
launchDefaults: z.record(z.string(), z.unknown()).optional(),
|
|
786
1029
|
installHint: z.string().optional(),
|
|
1030
|
+
transport: z.enum(["stdio", "tcp"]).optional(),
|
|
1031
|
+
portMatcher: z.string().optional(),
|
|
787
1032
|
})
|
|
788
1033
|
.optional()
|
|
789
1034
|
.describe("Override adapter config inline"),
|
|
790
1035
|
}, async (opts) => {
|
|
791
1036
|
try {
|
|
1037
|
+
await registryReady;
|
|
792
1038
|
const result = await runDebugInspect(registry, sessions, opts);
|
|
793
1039
|
return {
|
|
794
1040
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -806,6 +1052,11 @@ export function createServer() {
|
|
|
806
1052
|
// ────────────────────────────────────────────────────────────────────────────
|
|
807
1053
|
server.tool("list_breakpoints", "List the breakpoints currently registered with a session, grouped by source file. Includes regular breakpoints, conditional breakpoints, and logpoints.", {
|
|
808
1054
|
sessionId: z.string().optional().describe("Session ID (omit if only one session is active)"),
|
|
1055
|
+
}, {
|
|
1056
|
+
readOnlyHint: true,
|
|
1057
|
+
destructiveHint: false,
|
|
1058
|
+
idempotentHint: true,
|
|
1059
|
+
openWorldHint: false,
|
|
809
1060
|
}, async ({ sessionId }) => {
|
|
810
1061
|
try {
|
|
811
1062
|
const session = sessions.resolve(sessionId);
|
|
@@ -860,11 +1111,14 @@ export function createServer() {
|
|
|
860
1111
|
args: z.array(z.string()).optional(),
|
|
861
1112
|
launchDefaults: z.record(z.string(), z.unknown()).optional(),
|
|
862
1113
|
installHint: z.string().optional(),
|
|
1114
|
+
transport: z.enum(["stdio", "tcp"]).optional(),
|
|
1115
|
+
portMatcher: z.string().optional(),
|
|
863
1116
|
})
|
|
864
1117
|
.optional()
|
|
865
1118
|
.describe("Override adapter config inline"),
|
|
866
1119
|
}, async (opts) => {
|
|
867
1120
|
try {
|
|
1121
|
+
await registryReady;
|
|
868
1122
|
const result = await runDebugRun(registry, sessions, opts);
|
|
869
1123
|
return {
|
|
870
1124
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|