Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)<br>`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) |

Expand Down
11 changes: 11 additions & 0 deletions src/debugMCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand All @@ -204,6 +214,7 @@ export class DebugMCPServer {
const result = await this.debuggingHandler.handleEvaluateExpression(args);
return { content: [{ type: 'text' as const, text: result }] };
});

}

/**
Expand Down
110 changes: 105 additions & 5 deletions src/debuggingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface IDebuggingHandler {
handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string>;
handleClearAllBreakpoints(): Promise<string>;
handleListBreakpoints(): Promise<string>;
handleGetUnboundBreakpoints(): Promise<string>;
handleGetVariables(args: { scope?: 'local' | 'global' | 'all' }): Promise<string>;
handleEvaluateExpression(args: { expression: string }): Promise<string>;
}
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -330,6 +363,73 @@ export class DebuggingHandler implements IDebuggingHandler {
}
}

/**
* Return all breakpoints that are not yet verified (unbound)
*/
public async handleGetUnboundBreakpoints(): Promise<string> {
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
*/
Expand Down