Skip to content

Commit 00aa404

Browse files
committed
Feature: Add ability to get unbound breakpoints
1 parent ac8b64f commit 00aa404

5 files changed

Lines changed: 137 additions & 5 deletions

File tree

.vscode/launch.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Run Extension",
6+
"type": "extensionHost",
7+
"request": "launch",
8+
"runtimeExecutable": "${execPath}",
9+
"args": [
10+
"--extensionDevelopmentPath=${workspaceFolder}"
11+
]
12+
}
13+
]
14+
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to DebugMCP will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
66

7+
## [Unreleased]
8+
9+
### Added
10+
- **`get_unbound_breakpoints` tool** — returns all breakpoints that the debugger could not bind to an executable line (`verified === false` in the DAP protocol). Useful for diagnosing silent `add_breakpoint` failures. Reports the debugger’s own reason string when available. Without an active debug session all pending breakpoints are listed with a reminder to call `start_debugging` first.
11+
- **`add_breakpoint` now reports verification status** — after setting a breakpoint the tool waits briefly and queries the active debug session via `getDebugProtocolBreakpoint`. The response is now a JSON object with `file`, `line`, `verified`, and an optional `hint` explaining why the breakpoint could not be bound. Addresses the silent-failure issue described in [#18](https://github.com/microsoft/DebugMCP/issues/18).
12+
713
## [1.0.8] - 2025-03-14
814

915
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C
5757
| **remove_breakpoint** | Remove a breakpoint from a specific line | `fileFullPath` (required)<br>`line` (required) |
5858
| **clear_all_breakpoints** | Remove all breakpoints at once | None |
5959
| **list_breakpoints** | List all active breakpoints | None |
60+
| **get_unbound_breakpoints** | Returns all breakpoints that are unverified (set but not resolved to an executable line). Most useful after `start_debugging`. | None |
6061
| **get_variables_values** | Get variables and their values at current execution point | `scope` (optional: 'local', 'global', 'all') |
6162
| **evaluate_expression** | Evaluate an expression in debug context | `expression` (required) |
6263

src/debugMCPServer.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ export class DebugMCPServer {
183183
return { content: [{ type: 'text' as const, text: result }] };
184184
});
185185

186+
// Get unbound breakpoints tool
187+
this.mcpServer!.registerTool('get_unbound_breakpoints', {
188+
description: 'Returns all breakpoints that are currently unverified (unbound) — i.e., set but not resolved to an executable line by the debugger. ' +
189+
'Most useful after start_debugging has been called. ' +
190+
'Use this to diagnose why a breakpoint is not being hit, or to confirm that add_breakpoint succeeded.',
191+
}, async () => {
192+
const result = await this.debuggingHandler.handleGetUnboundBreakpoints();
193+
return { content: [{ type: 'text' as const, text: result }] };
194+
});
195+
186196
// Get variables tool
187197
this.mcpServer!.registerTool('get_variables_values', {
188198
description: 'Inspect all variable values at the current execution point. This is your window into program state - see what data looks like at runtime, verify assumptions, identify unexpected values, and understand why code behaves as it does.',
@@ -204,6 +214,7 @@ export class DebugMCPServer {
204214
const result = await this.debuggingHandler.handleEvaluateExpression(args);
205215
return { content: [{ type: 'text' as const, text: result }] };
206216
});
217+
207218
}
208219

209220
/**

src/debuggingHandler.ts

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface IDebuggingHandler {
2121
handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string>;
2222
handleClearAllBreakpoints(): Promise<string>;
2323
handleListBreakpoints(): Promise<string>;
24+
handleGetUnboundBreakpoints(): Promise<string>;
2425
handleGetVariables(args: { scope?: 'local' | 'global' | 'all' }): Promise<string>;
2526
handleEvaluateExpression(args: { expression: string }): Promise<string>;
2627
}
@@ -260,12 +261,44 @@ export class DebuggingHandler implements IDebuggingHandler {
260261
for (const lineNumber of matchingLineNumbers) {
261262
await this.executor.addBreakpoint(uri, lineNumber);
262263
}
263-
264-
if (matchingLineNumbers.length === 1) {
265-
return `Breakpoint added at ${fileFullPath}:${matchingLineNumbers[0]}`;
264+
265+
// Wait briefly for VS Code to verify the breakpoints against the active debug session
266+
await new Promise(resolve => setTimeout(resolve, 500));
267+
268+
const session = vscode.debug.activeDebugSession;
269+
const allBreakpoints = this.executor.getBreakpoints();
270+
271+
const results = await Promise.all(matchingLineNumbers.map(async (lineNumber) => {
272+
const bp = allBreakpoints.find(b => {
273+
if (b instanceof vscode.SourceBreakpoint) {
274+
return b.location.uri.toString() === uri.toString() &&
275+
b.location.range.start.line === lineNumber - 1;
276+
}
277+
return false;
278+
}) as vscode.SourceBreakpoint | undefined;
279+
280+
let verified = false;
281+
if (bp && session) {
282+
const dapBp = await session.getDebugProtocolBreakpoint(bp);
283+
verified = (dapBp as any)?.verified === true;
284+
}
285+
286+
return {
287+
file: fileFullPath,
288+
line: lineNumber,
289+
verified,
290+
...(verified ? {} : {
291+
hint: session
292+
? 'Breakpoint was set but not verified. The line may not be executable — check that lineContent matches an actual code statement.'
293+
: 'Breakpoint was set but not yet verified. No debug session is active — verification occurs after start_debugging is called.'
294+
})
295+
};
296+
}));
297+
298+
if (results.length === 1) {
299+
return JSON.stringify(results[0], null, 2);
266300
} else {
267-
const linesList = matchingLineNumbers.join(', ');
268-
return `Breakpoints added at ${matchingLineNumbers.length} locations in ${fileFullPath}: lines ${linesList}`;
301+
return JSON.stringify({ breakpoints: results }, null, 2);
269302
}
270303
} catch (error) {
271304
throw new Error(`Error adding breakpoint: ${error}`);
@@ -330,6 +363,73 @@ export class DebuggingHandler implements IDebuggingHandler {
330363
}
331364
}
332365

366+
/**
367+
* Return all breakpoints that are not yet verified (unbound)
368+
*/
369+
public async handleGetUnboundBreakpoints(): Promise<string> {
370+
try {
371+
const breakpoints = this.executor.getBreakpoints();
372+
const sourceBreakpoints = breakpoints.filter(
373+
(bp): bp is vscode.SourceBreakpoint => bp instanceof vscode.SourceBreakpoint
374+
);
375+
376+
if (sourceBreakpoints.length === 0) {
377+
return 'No breakpoints are currently set.';
378+
}
379+
380+
const session = vscode.debug.activeDebugSession;
381+
382+
if (!session) {
383+
return 'No active debug session. Breakpoints can only be verified while a debug session is running — call start_debugging first.';
384+
}
385+
386+
// With an active session, query the DAP protocol breakpoint for each source breakpoint.
387+
// The DAP Breakpoint object carries: verified, message (reason), source (resolved location), line, column.
388+
const diagnostics: string[] = [];
389+
for (const bp of sourceBreakpoints) {
390+
const dapBp = await session.getDebugProtocolBreakpoint(bp) as any;
391+
const verified: boolean = dapBp?.verified === true;
392+
if (!verified) {
393+
const requestedFile = bp.location.uri.fsPath;
394+
const requestedLine = bp.location.range.start.line + 1;
395+
const idx = diagnostics.length + 1;
396+
397+
let entry = `${idx}. Requested: ${requestedFile}:${requestedLine}`;
398+
399+
// DAP message is the debugger's own explanation (same source as Debug Doctor)
400+
const message: string | undefined = dapBp?.message;
401+
if (message) {
402+
entry += `\n Reason: ${message}`;
403+
} else {
404+
entry += `\n Reason: Could not be verified at this location`;
405+
}
406+
407+
// If the debugger resolved the breakpoint to a different source file,
408+
// report that location — this surfaces sourcemap/path-mismatch issues
409+
// the same way Debug Doctor does.
410+
const resolvedPath: string | undefined = dapBp?.source?.path;
411+
const resolvedLine: number | undefined = dapBp?.line;
412+
if (resolvedPath && resolvedPath !== requestedFile) {
413+
entry += `\n Resolved to: ${resolvedPath}${resolvedLine !== undefined ? `:${resolvedLine}` : ''}`;
414+
entry += `\n Hint: The debugger found a different source file. This is often a sourcemap or build-output path mismatch.`;
415+
} else if (!resolvedPath) {
416+
entry += `\n Hint: The debugger could not find a corresponding source location. Check that lineContent matches an actual executable statement and that any required build step has run.`;
417+
}
418+
419+
diagnostics.push(entry);
420+
}
421+
}
422+
423+
if (diagnostics.length === 0) {
424+
return 'All breakpoints are verified (bound to executable lines).';
425+
}
426+
427+
return `Unbound (unverified) breakpoints — ${diagnostics.length} of ${sourceBreakpoints.length} could not be resolved:\n\n${diagnostics.join('\n\n')}`;
428+
} catch (error) {
429+
throw new Error(`Error getting unbound breakpoints: ${error}`);
430+
}
431+
}
432+
333433
/**
334434
* Get variables from current debug context
335435
*/

0 commit comments

Comments
 (0)