diff --git a/CHANGELOG.md b/CHANGELOG.md
index d824033..c55fbfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to DebugMCP will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
+## [Unreleased]
+
+### Added
+- **`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.
+- **`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).
+
## [1.0.8] - 2025-03-14
### Added
diff --git a/README.md b/README.md
index a16ed39..85e89e2 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C
| **remove_breakpoint** | Remove a breakpoint from a specific line | `fileFullPath` (required)
`line` (required) |
| **clear_all_breakpoints** | Remove all breakpoints at once | None |
| **list_breakpoints** | List all active breakpoints | None |
+| **get_unbound_breakpoints** | Returns all breakpoints that are unverified (set but not resolved to an executable line). Most useful after `start_debugging`. | None |
| **get_variables_values** | Get variables and their values at current execution point | `scope` (optional: 'local', 'global', 'all') |
| **evaluate_expression** | Evaluate an expression in debug context | `expression` (required) |
diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts
index 40e33f8..e42642c 100644
--- a/src/debugMCPServer.ts
+++ b/src/debugMCPServer.ts
@@ -183,6 +183,16 @@ export class DebugMCPServer {
return { content: [{ type: 'text' as const, text: result }] };
});
+ // Get unbound breakpoints tool
+ this.mcpServer!.registerTool('get_unbound_breakpoints', {
+ description: 'Returns all breakpoints that are currently unverified (unbound) — i.e., set but not resolved to an executable line by the debugger. ' +
+ 'Most useful after start_debugging has been called. ' +
+ 'Use this to diagnose why a breakpoint is not being hit, or to confirm that add_breakpoint succeeded.',
+ }, async () => {
+ const result = await this.debuggingHandler.handleGetUnboundBreakpoints();
+ return { content: [{ type: 'text' as const, text: result }] };
+ });
+
// Get variables tool
this.mcpServer!.registerTool('get_variables_values', {
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 {
const result = await this.debuggingHandler.handleEvaluateExpression(args);
return { content: [{ type: 'text' as const, text: result }] };
});
+
}
/**
diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts
index dd87bde..ccf1123 100644
--- a/src/debuggingHandler.ts
+++ b/src/debuggingHandler.ts
@@ -21,6 +21,7 @@ export interface IDebuggingHandler {
handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise;
handleClearAllBreakpoints(): Promise;
handleListBreakpoints(): Promise;
+ handleGetUnboundBreakpoints(): Promise;
handleGetVariables(args: { scope?: 'local' | 'global' | 'all' }): Promise;
handleEvaluateExpression(args: { expression: string }): Promise;
}
@@ -260,12 +261,44 @@ export class DebuggingHandler implements IDebuggingHandler {
for (const lineNumber of matchingLineNumbers) {
await this.executor.addBreakpoint(uri, lineNumber);
}
-
- if (matchingLineNumbers.length === 1) {
- return `Breakpoint added at ${fileFullPath}:${matchingLineNumbers[0]}`;
+
+ // Wait briefly for VS Code to verify the breakpoints against the active debug session
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ const session = vscode.debug.activeDebugSession;
+ const allBreakpoints = this.executor.getBreakpoints();
+
+ const results = await Promise.all(matchingLineNumbers.map(async (lineNumber) => {
+ const bp = allBreakpoints.find(b => {
+ if (b instanceof vscode.SourceBreakpoint) {
+ return b.location.uri.toString() === uri.toString() &&
+ b.location.range.start.line === lineNumber - 1;
+ }
+ return false;
+ }) as vscode.SourceBreakpoint | undefined;
+
+ let verified = false;
+ if (bp && session) {
+ const dapBp = await session.getDebugProtocolBreakpoint(bp);
+ verified = (dapBp as any)?.verified === true;
+ }
+
+ return {
+ file: fileFullPath,
+ line: lineNumber,
+ verified,
+ ...(verified ? {} : {
+ hint: session
+ ? 'Breakpoint was set but not verified. The line may not be executable — check that lineContent matches an actual code statement.'
+ : 'Breakpoint was set but not yet verified. No debug session is active — verification occurs after start_debugging is called.'
+ })
+ };
+ }));
+
+ if (results.length === 1) {
+ return JSON.stringify(results[0], null, 2);
} else {
- const linesList = matchingLineNumbers.join(', ');
- return `Breakpoints added at ${matchingLineNumbers.length} locations in ${fileFullPath}: lines ${linesList}`;
+ return JSON.stringify({ breakpoints: results }, null, 2);
}
} catch (error) {
throw new Error(`Error adding breakpoint: ${error}`);
@@ -330,6 +363,73 @@ export class DebuggingHandler implements IDebuggingHandler {
}
}
+ /**
+ * Return all breakpoints that are not yet verified (unbound)
+ */
+ public async handleGetUnboundBreakpoints(): Promise {
+ try {
+ const breakpoints = this.executor.getBreakpoints();
+ const sourceBreakpoints = breakpoints.filter(
+ (bp): bp is vscode.SourceBreakpoint => bp instanceof vscode.SourceBreakpoint
+ );
+
+ if (sourceBreakpoints.length === 0) {
+ return 'No breakpoints are currently set.';
+ }
+
+ const session = vscode.debug.activeDebugSession;
+
+ if (!session) {
+ return 'No active debug session. Breakpoints can only be verified while a debug session is running — call start_debugging first.';
+ }
+
+ // With an active session, query the DAP protocol breakpoint for each source breakpoint.
+ // The DAP Breakpoint object carries: verified, message (reason), source (resolved location), line, column.
+ const diagnostics: string[] = [];
+ for (const bp of sourceBreakpoints) {
+ const dapBp = await session.getDebugProtocolBreakpoint(bp) as any;
+ const verified: boolean = dapBp?.verified === true;
+ if (!verified) {
+ const requestedFile = bp.location.uri.fsPath;
+ const requestedLine = bp.location.range.start.line + 1;
+ const idx = diagnostics.length + 1;
+
+ let entry = `${idx}. Requested: ${requestedFile}:${requestedLine}`;
+
+ // DAP message is the debugger's own explanation (same source as Debug Doctor)
+ const message: string | undefined = dapBp?.message;
+ if (message) {
+ entry += `\n Reason: ${message}`;
+ } else {
+ entry += `\n Reason: Could not be verified at this location`;
+ }
+
+ // If the debugger resolved the breakpoint to a different source file,
+ // report that location — this surfaces sourcemap/path-mismatch issues
+ // the same way Debug Doctor does.
+ const resolvedPath: string | undefined = dapBp?.source?.path;
+ const resolvedLine: number | undefined = dapBp?.line;
+ if (resolvedPath && resolvedPath !== requestedFile) {
+ entry += `\n Resolved to: ${resolvedPath}${resolvedLine !== undefined ? `:${resolvedLine}` : ''}`;
+ entry += `\n Hint: The debugger found a different source file. This is often a sourcemap or build-output path mismatch.`;
+ } else if (!resolvedPath) {
+ 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.`;
+ }
+
+ diagnostics.push(entry);
+ }
+ }
+
+ if (diagnostics.length === 0) {
+ return 'All breakpoints are verified (bound to executable lines).';
+ }
+
+ return `Unbound (unverified) breakpoints — ${diagnostics.length} of ${sourceBreakpoints.length} could not be resolved:\n\n${diagnostics.join('\n\n')}`;
+ } catch (error) {
+ throw new Error(`Error getting unbound breakpoints: ${error}`);
+ }
+ }
+
/**
* Get variables from current debug context
*/