Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![VS Code](https://img.shields.io/badge/VS%20Code-1.104.0+-blue.svg)](https://code.visualstudio.com/)
[![Version](https://img.shields.io/badge/version-2.2.3-green.svg)](https://github.com/microsoft/DebugMCP)
[![Version](https://img.shields.io/badge/version-2.2.4-green.svg)](https://github.com/microsoft/DebugMCP)
[![VS Marketplace](https://img.shields.io/badge/VS%20Marketplace-Install-blue.svg)](https://marketplace.visualstudio.com/items?itemName=ozzafar.debugmcpextension)

> ⭐ **If you find DebugMCP useful, please [star the repo on GitHub](https://github.com/microsoft/DebugMCP)!** It helps others discover the project and motivates continued development.
Expand Down Expand Up @@ -61,6 +61,7 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C
| **pause_execution** | Interrupt a freely-running program and stop at its current location (no breakpoint needed) | None |
| **restart_debugging** | Restart the current debug session | None |
| **add_breakpoint** | Add a breakpoint at a specific line (optionally conditional) | `fileFullPath` (required)<br>`line` (required, 1-based)<br>`condition` (optional) |
| **add_logpoint** | Add a logpoint that logs a message (instead of pausing) when a line is reached | `fileFullPath` (required)<br>`line` (required, 1-based)<br>`logMessage` (required, `{expr}` interpolated)<br>`condition` (optional) |
| **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 |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "debugmcpextension",
"displayName": "DebugMCP — Agentic Debugging for VS Code, Cursor & More",
"description": "Your AI agent debugs for you — right inside VS Code, Cursor & other VS Code-based editors. Let Copilot, Cline, Cursor, Codex & any MCP agent set breakpoints, step through code, and inspect variables live instead of guessing from logs.",
"version": "2.2.3",
"version": "2.2.4",
"publisher": "ozzafar",
"author": {
"name": "Oz Zafar",
Expand Down
5 changes: 5 additions & 0 deletions skills/debug-live/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: Drive an interactive VS Code debugger to investigate bugs, failing
license: MIT
allowed-tools:
- add_breakpoint
- add_logpoint
- remove_breakpoint
- clear_all_breakpoints
- list_breakpoints
Expand Down Expand Up @@ -169,6 +170,10 @@ Before ending the debug session, confirm you can answer:
you isolate the issue, add tighter breakpoints around the problematic region.
- **Use line numbers.** `add_breakpoint` takes a 1-based `line`; re-check the line after
edits since numbers shift when code changes.
- **Prefer logpoints for loops/hot paths.** When you want to observe how a value evolves
across many iterations without stopping, use `add_logpoint` with `{expression}`
interpolation (e.g. `iter {i}: total={total}`) instead of repeatedly continuing from a
breakpoint. Logpoints also avoid distorting timing-sensitive code.
- **Don't overuse breakpoints.** A handful of well-placed pauses beats dozens of noisy
ones. After each session, `clear_all_breakpoints` to start fresh.
- **For test debugging,** pass `testName` to `start_debugging`. The server routes through
Expand Down
2 changes: 2 additions & 0 deletions src/controlServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export class ControlServer {
return this.handler.handleRestart();
case 'handleAddBreakpoint':
return this.handler.handleAddBreakpoint(args);
case 'handleAddLogpoint':
return this.handler.handleAddLogpoint(args);
case 'handleRemoveBreakpoint':
return this.handler.handleRemoveBreakpoint(args);
case 'handleClearAllBreakpoints':
Expand Down
12 changes: 12 additions & 0 deletions src/debugMCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@ export class DebugMCPServer {
}, async (args: { fileFullPath: string; line: number; condition?: string }) =>
this.runTool('add_breakpoint', () => debuggingHandler.handleAddBreakpoint(args)));

// Add logpoint tool
server.registerTool('add_logpoint', {
description: 'Add a logpoint: a breakpoint that logs a message instead of pausing execution. Ideal for tracing values across many iterations or hot paths without stopping, or where a hard pause would distort timing. Embed expressions in curly braces to interpolate runtime values, e.g. "user id={user.id}, count={items.length}". Provide an optional condition to only log when it evaluates to true.',
inputSchema: {
fileFullPath: z.string().describe('Full path to the file'),
line: z.number().int().describe('Line number (1-based) where the logpoint should be set'),
logMessage: z.string().describe('Message to log when the line is reached. Wrap expressions in {curly braces} to interpolate runtime values.'),
condition: z.string().optional().describe('Optional condition expression. When provided, the message is only logged if this expression evaluates to true.'),
},
}, async (args: { fileFullPath: string; line: number; logMessage: string; condition?: string }) =>
this.runTool('add_logpoint', () => debuggingHandler.handleAddLogpoint(args)));

// Remove breakpoint tool
server.registerTool('remove_breakpoint', {
description: 'Remove a breakpoint that is no longer needed.',
Expand Down
11 changes: 7 additions & 4 deletions src/debuggingExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface IDebuggingExecutor {
continue(): Promise<void>;
pause(): Promise<void>;
restart(): Promise<void>;
addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise<void>;
addBreakpoint(uri: vscode.Uri, line: number, condition?: string, logMessage?: string): Promise<void>;
removeBreakpoint(uri: vscode.Uri, line: number): Promise<void>;
getCurrentDebugState(numNextLines: number): Promise<DebugState>;
getVariables(frameId: number, scope?: 'local' | 'global' | 'all'): Promise<any>;
Expand Down Expand Up @@ -326,14 +326,17 @@ export class DebuggingExecutor implements IDebuggingExecutor {
/**
* Add a breakpoint at specified location. An optional condition makes it a
* conditional breakpoint that only pauses execution when the expression
* evaluates to true.
* evaluates to true. An optional logMessage makes it a logpoint that logs
* the message (with {expressions} interpolated) instead of pausing.
*/
public async addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise<void> {
public async addBreakpoint(uri: vscode.Uri, line: number, condition?: string, logMessage?: string): Promise<void> {
try {
const breakpoint = new vscode.SourceBreakpoint(
new vscode.Location(uri, new vscode.Position(line - 1, 0)),
true,
condition
condition,
undefined,
logMessage
);
vscode.debug.addBreakpoints([breakpoint]);
} catch (error) {
Expand Down
38 changes: 37 additions & 1 deletion src/debuggingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface IDebuggingHandler {
handlePause(): Promise<string>;
handleRestart(): Promise<string>;
handleAddBreakpoint(args: { fileFullPath: string; line: number; condition?: string }): Promise<string>;
handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise<string>;
handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string>;
handleClearAllBreakpoints(): Promise<string>;
handleListBreakpoints(): Promise<string>;
Expand Down Expand Up @@ -325,6 +326,39 @@ export class DebuggingHandler implements IDebuggingHandler {
}
}

/**
* Add a logpoint: a breakpoint that logs a message (with {expressions}
* interpolated by the debug adapter) instead of pausing execution. An
* optional condition only logs when the expression is true.
*/
public async handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise<string> {
const { fileFullPath, line, logMessage, condition } = args;

try {
if (!Number.isInteger(line) || line < 1) {
throw new Error(`Invalid line number: ${line}. Provide a 1-based line number.`);
}
if (!logMessage) {
throw new Error('A non-empty logMessage is required for a logpoint.');
}

// Validate the line exists so we fail clearly instead of setting an
// unbound logpoint past the end of the file.
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(fileFullPath));
if (line > document.lineCount) {
throw new Error(`Line ${line} is out of range: ${fileFullPath} has ${document.lineCount} lines.`);
}

const uri = vscode.Uri.file(fileFullPath);
await this.executor.addBreakpoint(uri, line, condition, logMessage);

const conditionInfo = condition ? ` (condition: ${condition})` : '';
return `Logpoint added at ${fileFullPath}:${line}${conditionInfo}`;
} catch (error) {
throw new Error(`Error adding logpoint: ${error}`);
}
}

/**
* Remove a breakpoint from specified location
*/
Expand Down Expand Up @@ -372,7 +406,9 @@ export class DebuggingHandler implements IDebuggingHandler {
const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop();
const line = bp.location.range.start.line + 1;
const conditionInfo = bp.condition ? ` (condition: ${bp.condition})` : '';
breakpointList += `${index + 1}. ${fileName}:${line}${conditionInfo}\n`;
const kind = bp.logMessage ? `Logpoint` : `Breakpoint`;
const logInfo = bp.logMessage ? ` (log: ${bp.logMessage})` : '';
breakpointList += `${index + 1}. ${kind} ${fileName}:${line}${conditionInfo}${logInfo}\n`;
} else if (bp instanceof vscode.FunctionBreakpoint) {
breakpointList += `${index + 1}. Function: ${bp.functionName}\n`;
}
Expand Down
4 changes: 4 additions & 0 deletions src/routingDebuggingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ export class RoutingDebuggingHandler implements IDebuggingHandler {
return this.forward('handleAddBreakpoint', args, args.fileFullPath);
}

public handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise<string> {
return this.forward('handleAddLogpoint', args, args.fileFullPath);
}

public handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string> {
return this.forward('handleRemoveBreakpoint', args, args.fileFullPath);
}
Expand Down
20 changes: 20 additions & 0 deletions src/test/routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class RecordingHandler implements IDebuggingHandler {
handlePause() { return this.record('pause', {}); }
handleRestart() { return this.record('restart', {}); }
handleAddBreakpoint(args: any) { return this.record('addBp', args); }
handleAddLogpoint(args: any) { return this.record('addLp', args); }
handleRemoveBreakpoint(args: any) { return this.record('removeBp', args); }
handleClearAllBreakpoints() { return this.record('clearBp', {}); }
handleListBreakpoints() { return this.record('listBp', {}); }
Expand Down Expand Up @@ -125,6 +126,25 @@ suite('Multi-window routing', () => {
assert.strictEqual(handlerB.calls.length, 0);
});

test('add_logpoint routes by fileFullPath before any start_debugging', async () => {
const repoA = path.join(dir, 'repoA');
const repoB = path.join(dir, 'repoB');
const handlerA = new RecordingHandler('A');
const handlerB = new RecordingHandler('B');
await startWindow('a.json', [repoA], handlerA);
await startWindow('b.json', [repoB], handlerB);

const routing = new RoutingDebuggingHandler(new WorkspaceRegistry(process.pid, dir));
const result = await routing.handleAddLogpoint({
fileFullPath: path.join(repoA, 'src', 'y.py'),
line: 1,
logMessage: 'i={i}'
});

assert.strictEqual(result, 'A:addLp');
assert.strictEqual(handlerB.calls.length, 0);
});

test('throws a helpful error when no window owns the path', async () => {
const repoA = path.join(dir, 'repoA');
const repoB = path.join(dir, 'repoB');
Expand Down
Loading