From 46814e8151d842ef19934020296ea45705c53711 Mon Sep 17 00:00:00 2001
From: ozzafar <48795672+ozzafar@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:52:07 +0300
Subject: [PATCH] Fix #15: add logpoints via a new add_logpoint tool
Add an `add_logpoint` MCP tool for setting logpoints: breakpoints that log
a message (with {expression} interpolation) when a line is reached instead
of pausing execution. Useful for tracing values across loops/hot paths and
for timing-sensitive code where a hard pause would distort behavior.
Logpoints are SourceBreakpoints carrying a logMessage, so remove_breakpoint,
list_breakpoints, and clear_all_breakpoints handle them automatically;
list_breakpoints now labels entries Breakpoint/Logpoint and shows the
message. Wired through the multi-window routing/control-server path and
covered by a routing test. Bumped version to 2.2.4 and updated the README
tools table and debug-live skill.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ea24b92-40ad-4539-aa13-61aa6ff8751f
---
README.md | 3 ++-
package-lock.json | 4 ++--
package.json | 2 +-
skills/debug-live/SKILL.md | 5 +++++
src/controlServer.ts | 2 ++
src/debugMCPServer.ts | 12 +++++++++++
src/debuggingExecutor.ts | 11 ++++++----
src/debuggingHandler.ts | 38 +++++++++++++++++++++++++++++++++-
src/routingDebuggingHandler.ts | 4 ++++
src/test/routing.test.ts | 20 ++++++++++++++++++
10 files changed, 92 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index 310fca9..99f985d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe
[](https://opensource.org/licenses/MIT)
[](https://code.visualstudio.com/)
-[](https://github.com/microsoft/DebugMCP)
+[](https://github.com/microsoft/DebugMCP)
[](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.
@@ -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)
`line` (required, 1-based)
`condition` (optional) |
+| **add_logpoint** | Add a logpoint that logs a message (instead of pausing) when a line is reached | `fileFullPath` (required)
`line` (required, 1-based)
`logMessage` (required, `{expr}` interpolated)
`condition` (optional) |
| **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 |
diff --git a/package-lock.json b/package-lock.json
index feb66e0..0f89920 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "debugmcpextension",
- "version": "2.2.3",
+ "version": "2.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "debugmcpextension",
- "version": "2.2.3",
+ "version": "2.2.4",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
diff --git a/package.json b/package.json
index fc6b537..fcdb404 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/skills/debug-live/SKILL.md b/skills/debug-live/SKILL.md
index 5431e45..b752b5c 100644
--- a/skills/debug-live/SKILL.md
+++ b/skills/debug-live/SKILL.md
@@ -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
@@ -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
diff --git a/src/controlServer.ts b/src/controlServer.ts
index 6a21512..215938f 100644
--- a/src/controlServer.ts
+++ b/src/controlServer.ts
@@ -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':
diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts
index 7f4156f..4de9287 100644
--- a/src/debugMCPServer.ts
+++ b/src/debugMCPServer.ts
@@ -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.',
diff --git a/src/debuggingExecutor.ts b/src/debuggingExecutor.ts
index 023088d..15821c0 100644
--- a/src/debuggingExecutor.ts
+++ b/src/debuggingExecutor.ts
@@ -33,7 +33,7 @@ export interface IDebuggingExecutor {
continue(): Promise;
pause(): Promise;
restart(): Promise;
- addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise;
+ addBreakpoint(uri: vscode.Uri, line: number, condition?: string, logMessage?: string): Promise;
removeBreakpoint(uri: vscode.Uri, line: number): Promise;
getCurrentDebugState(numNextLines: number): Promise;
getVariables(frameId: number, scope?: 'local' | 'global' | 'all'): Promise;
@@ -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 {
+ public async addBreakpoint(uri: vscode.Uri, line: number, condition?: string, logMessage?: string): Promise {
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) {
diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts
index d068386..08551b1 100644
--- a/src/debuggingHandler.ts
+++ b/src/debuggingHandler.ts
@@ -19,6 +19,7 @@ export interface IDebuggingHandler {
handlePause(): Promise;
handleRestart(): Promise;
handleAddBreakpoint(args: { fileFullPath: string; line: number; condition?: string }): Promise;
+ handleAddLogpoint(args: { fileFullPath: string; line: number; logMessage: string; condition?: string }): Promise;
handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise;
handleClearAllBreakpoints(): Promise;
handleListBreakpoints(): Promise;
@@ -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 {
+ 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
*/
@@ -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`;
}
diff --git a/src/routingDebuggingHandler.ts b/src/routingDebuggingHandler.ts
index f6489e9..507fb44 100644
--- a/src/routingDebuggingHandler.ts
+++ b/src/routingDebuggingHandler.ts
@@ -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 {
+ return this.forward('handleAddLogpoint', args, args.fileFullPath);
+ }
+
public handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise {
return this.forward('handleRemoveBreakpoint', args, args.fileFullPath);
}
diff --git a/src/test/routing.test.ts b/src/test/routing.test.ts
index 2fe6334..2f34343 100644
--- a/src/test/routing.test.ts
+++ b/src/test/routing.test.ts
@@ -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', {}); }
@@ -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');