Skip to content

Commit 6f02fa0

Browse files
authored
Merge pull request #109 from microsoft/ozzafar/issue-15-logpoints
Fix #15: add logpoints via a new add_logpoint tool
2 parents 37529e2 + 46814e8 commit 6f02fa0

10 files changed

Lines changed: 92 additions & 9 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe
44

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

1010
> **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.
@@ -61,6 +61,7 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C
6161
| **pause_execution** | Interrupt a freely-running program and stop at its current location (no breakpoint needed) | None |
6262
| **restart_debugging** | Restart the current debug session | None |
6363
| **add_breakpoint** | Add a breakpoint at a specific line (optionally conditional) | `fileFullPath` (required)<br>`line` (required, 1-based)<br>`condition` (optional) |
64+
| **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) |
6465
| **remove_breakpoint** | Remove a breakpoint from a specific line | `fileFullPath` (required)<br>`line` (required) |
6566
| **clear_all_breakpoints** | Remove all breakpoints at once | None |
6667
| **list_breakpoints** | List all active breakpoints | None |

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "debugmcpextension",
33
"displayName": "DebugMCP — Agentic Debugging for VS Code, Cursor & More",
44
"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.",
5-
"version": "2.2.3",
5+
"version": "2.2.4",
66
"publisher": "ozzafar",
77
"author": {
88
"name": "Oz Zafar",

skills/debug-live/SKILL.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description: Drive an interactive VS Code debugger to investigate bugs, failing
44
license: MIT
55
allowed-tools:
66
- add_breakpoint
7+
- add_logpoint
78
- remove_breakpoint
89
- clear_all_breakpoints
910
- list_breakpoints
@@ -169,6 +170,10 @@ Before ending the debug session, confirm you can answer:
169170
you isolate the issue, add tighter breakpoints around the problematic region.
170171
- **Use line numbers.** `add_breakpoint` takes a 1-based `line`; re-check the line after
171172
edits since numbers shift when code changes.
173+
- **Prefer logpoints for loops/hot paths.** When you want to observe how a value evolves
174+
across many iterations without stopping, use `add_logpoint` with `{expression}`
175+
interpolation (e.g. `iter {i}: total={total}`) instead of repeatedly continuing from a
176+
breakpoint. Logpoints also avoid distorting timing-sensitive code.
172177
- **Don't overuse breakpoints.** A handful of well-placed pauses beats dozens of noisy
173178
ones. After each session, `clear_all_breakpoints` to start fresh.
174179
- **For test debugging,** pass `testName` to `start_debugging`. The server routes through

src/controlServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export class ControlServer {
9797
return this.handler.handleRestart();
9898
case 'handleAddBreakpoint':
9999
return this.handler.handleAddBreakpoint(args);
100+
case 'handleAddLogpoint':
101+
return this.handler.handleAddLogpoint(args);
100102
case 'handleRemoveBreakpoint':
101103
return this.handler.handleRemoveBreakpoint(args);
102104
case 'handleClearAllBreakpoints':

src/debugMCPServer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,18 @@ export class DebugMCPServer {
270270
}, async (args: { fileFullPath: string; line: number; condition?: string }) =>
271271
this.runTool('add_breakpoint', () => debuggingHandler.handleAddBreakpoint(args)));
272272

273+
// Add logpoint tool
274+
server.registerTool('add_logpoint', {
275+
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.',
276+
inputSchema: {
277+
fileFullPath: z.string().describe('Full path to the file'),
278+
line: z.number().int().describe('Line number (1-based) where the logpoint should be set'),
279+
logMessage: z.string().describe('Message to log when the line is reached. Wrap expressions in {curly braces} to interpolate runtime values.'),
280+
condition: z.string().optional().describe('Optional condition expression. When provided, the message is only logged if this expression evaluates to true.'),
281+
},
282+
}, async (args: { fileFullPath: string; line: number; logMessage: string; condition?: string }) =>
283+
this.runTool('add_logpoint', () => debuggingHandler.handleAddLogpoint(args)));
284+
273285
// Remove breakpoint tool
274286
server.registerTool('remove_breakpoint', {
275287
description: 'Remove a breakpoint that is no longer needed.',

src/debuggingExecutor.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface IDebuggingExecutor {
3333
continue(): Promise<void>;
3434
pause(): Promise<void>;
3535
restart(): Promise<void>;
36-
addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise<void>;
36+
addBreakpoint(uri: vscode.Uri, line: number, condition?: string, logMessage?: string): Promise<void>;
3737
removeBreakpoint(uri: vscode.Uri, line: number): Promise<void>;
3838
getCurrentDebugState(numNextLines: number): Promise<DebugState>;
3939
getVariables(frameId: number, scope?: 'local' | 'global' | 'all'): Promise<any>;
@@ -326,14 +326,17 @@ export class DebuggingExecutor implements IDebuggingExecutor {
326326
/**
327327
* Add a breakpoint at specified location. An optional condition makes it a
328328
* conditional breakpoint that only pauses execution when the expression
329-
* evaluates to true.
329+
* evaluates to true. An optional logMessage makes it a logpoint that logs
330+
* the message (with {expressions} interpolated) instead of pausing.
330331
*/
331-
public async addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise<void> {
332+
public async addBreakpoint(uri: vscode.Uri, line: number, condition?: string, logMessage?: string): Promise<void> {
332333
try {
333334
const breakpoint = new vscode.SourceBreakpoint(
334335
new vscode.Location(uri, new vscode.Position(line - 1, 0)),
335336
true,
336-
condition
337+
condition,
338+
undefined,
339+
logMessage
337340
);
338341
vscode.debug.addBreakpoints([breakpoint]);
339342
} catch (error) {

src/debuggingHandler.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface IDebuggingHandler {
1919
handlePause(): Promise<string>;
2020
handleRestart(): Promise<string>;
2121
handleAddBreakpoint(args: { fileFullPath: string; line: number; condition?: string }): Promise<string>;
22+
handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise<string>;
2223
handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string>;
2324
handleClearAllBreakpoints(): Promise<string>;
2425
handleListBreakpoints(): Promise<string>;
@@ -325,6 +326,39 @@ export class DebuggingHandler implements IDebuggingHandler {
325326
}
326327
}
327328

329+
/**
330+
* Add a logpoint: a breakpoint that logs a message (with {expressions}
331+
* interpolated by the debug adapter) instead of pausing execution. An
332+
* optional condition only logs when the expression is true.
333+
*/
334+
public async handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise<string> {
335+
const { fileFullPath, line, logMessage, condition } = args;
336+
337+
try {
338+
if (!Number.isInteger(line) || line < 1) {
339+
throw new Error(`Invalid line number: ${line}. Provide a 1-based line number.`);
340+
}
341+
if (!logMessage) {
342+
throw new Error('A non-empty logMessage is required for a logpoint.');
343+
}
344+
345+
// Validate the line exists so we fail clearly instead of setting an
346+
// unbound logpoint past the end of the file.
347+
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(fileFullPath));
348+
if (line > document.lineCount) {
349+
throw new Error(`Line ${line} is out of range: ${fileFullPath} has ${document.lineCount} lines.`);
350+
}
351+
352+
const uri = vscode.Uri.file(fileFullPath);
353+
await this.executor.addBreakpoint(uri, line, condition, logMessage);
354+
355+
const conditionInfo = condition ? ` (condition: ${condition})` : '';
356+
return `Logpoint added at ${fileFullPath}:${line}${conditionInfo}`;
357+
} catch (error) {
358+
throw new Error(`Error adding logpoint: ${error}`);
359+
}
360+
}
361+
328362
/**
329363
* Remove a breakpoint from specified location
330364
*/
@@ -372,7 +406,9 @@ export class DebuggingHandler implements IDebuggingHandler {
372406
const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop();
373407
const line = bp.location.range.start.line + 1;
374408
const conditionInfo = bp.condition ? ` (condition: ${bp.condition})` : '';
375-
breakpointList += `${index + 1}. ${fileName}:${line}${conditionInfo}\n`;
409+
const kind = bp.logMessage ? `Logpoint` : `Breakpoint`;
410+
const logInfo = bp.logMessage ? ` (log: ${bp.logMessage})` : '';
411+
breakpointList += `${index + 1}. ${kind} ${fileName}:${line}${conditionInfo}${logInfo}\n`;
376412
} else if (bp instanceof vscode.FunctionBreakpoint) {
377413
breakpointList += `${index + 1}. Function: ${bp.functionName}\n`;
378414
}

src/routingDebuggingHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ export class RoutingDebuggingHandler implements IDebuggingHandler {
204204
return this.forward('handleAddBreakpoint', args, args.fileFullPath);
205205
}
206206

207+
public handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise<string> {
208+
return this.forward('handleAddLogpoint', args, args.fileFullPath);
209+
}
210+
207211
public handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string> {
208212
return this.forward('handleRemoveBreakpoint', args, args.fileFullPath);
209213
}

src/test/routing.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class RecordingHandler implements IDebuggingHandler {
2828
handlePause() { return this.record('pause', {}); }
2929
handleRestart() { return this.record('restart', {}); }
3030
handleAddBreakpoint(args: any) { return this.record('addBp', args); }
31+
handleAddLogpoint(args: any) { return this.record('addLp', args); }
3132
handleRemoveBreakpoint(args: any) { return this.record('removeBp', args); }
3233
handleClearAllBreakpoints() { return this.record('clearBp', {}); }
3334
handleListBreakpoints() { return this.record('listBp', {}); }
@@ -125,6 +126,25 @@ suite('Multi-window routing', () => {
125126
assert.strictEqual(handlerB.calls.length, 0);
126127
});
127128

129+
test('add_logpoint routes by fileFullPath before any start_debugging', async () => {
130+
const repoA = path.join(dir, 'repoA');
131+
const repoB = path.join(dir, 'repoB');
132+
const handlerA = new RecordingHandler('A');
133+
const handlerB = new RecordingHandler('B');
134+
await startWindow('a.json', [repoA], handlerA);
135+
await startWindow('b.json', [repoB], handlerB);
136+
137+
const routing = new RoutingDebuggingHandler(new WorkspaceRegistry(process.pid, dir));
138+
const result = await routing.handleAddLogpoint({
139+
fileFullPath: path.join(repoA, 'src', 'y.py'),
140+
line: 1,
141+
logMessage: 'i={i}'
142+
});
143+
144+
assert.strictEqual(result, 'A:addLp');
145+
assert.strictEqual(handlerB.calls.length, 0);
146+
});
147+
128148
test('throws a helpful error when no window owns the path', async () => {
129149
const repoA = path.join(dir, 'repoA');
130150
const repoB = path.join(dir, 'repoB');

0 commit comments

Comments
 (0)