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 */