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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
## [Unreleased]

### Added
- **Claude Code auto-registration** - Claude Code is now offered in the agent selection popup and configured via `~/.claude.json`'s user-scope `mcpServers` field. Claude Desktop connects via its Custom Connector UI instead of a static config file; the README's manual configuration section covers both.
- Breakpoint, logpoint, and removal tools now support VS Code virtual-document URIs, including Business Central sources. An optional selects the correct workspace when multiple editor windows are open.
- **Claude Code auto-registration** - Claude Code is now offered in the agent selection popup and configured via 's user-scope field. Claude Desktop connects via its Custom Connector UI instead of a static config file; the README's manual configuration section covers both.

## [2.3.5] - 2026-09-09

Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C
| **continue_execution** | Continue until next breakpoint | None |
| **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) |
| **add_breakpoint** | Add a breakpoint at a specific line (optionally conditional) | `fileFullPath` (required; path or virtual URI)<br>`workingDirectory` (optional; identifies the window for virtual URIs)<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; path or virtual URI)<br>`workingDirectory` (optional; identifies the window for virtual URIs)<br>`line` (required, 1-based)<br>`logMessage` (required, `{expr}` interpolated)<br>`condition` (optional) |
| **remove_breakpoint** | Remove a breakpoint from a specific line | `fileFullPath` (required; path or virtual URI)<br>`workingDirectory` (optional; identifies the window for virtual URIs)<br>`line` (required) |
| **clear_all_breakpoints** | Remove all breakpoints at once | None |
| **list_breakpoints** | List all active breakpoints | None |
| **list_variable_names** | List names and types of variables in scope, without reading any values | `scope` (optional: 'local', 'global', 'all') |
Expand Down Expand Up @@ -231,6 +231,16 @@ DebugMCP supports debugging for the following languages with their respective VS
| **PHP** | [PHP Debug](https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug) | `.php` | ✅ Fully Supported |
| **Ruby** | [Ruby](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp) | `.rb` | ✅ Fully Supported |
| **C#/.NET** | [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) | `.cs`, `.csproj` | ✅ Fully Supported |
| **AL (Business Central)** | [AL Language](https://marketplace.visualstudio.com/items?itemName=ms-dynamics-smb.al) | `.al`, virtual `.dal` | ✅ Supported with an AL launch configuration |

### Virtual source documents

Breakpoint tools accept native filesystem paths and VS Code virtual-document URIs. This
supports generated or downloaded sources that a language extension exposes without a local
file, including Business Central dependency objects such as
`al-preview://AlLang/.../Table/18/Customer.dal`. When multiple editor windows are open,
pass `workingDirectory` with the virtual URI so DebugMCP routes the operation to the correct
workspace.

## Configuration

Expand Down Expand Up @@ -593,3 +603,4 @@ If DebugMCP has helped you debug faster, please consider giving it a star on Git
## License

MIT License - See [LICENSE](LICENSE.txt) for details
This extension was created by **Oz Zafar**, **Ori Bar-Ilan** and **Karin Brisker**.
6 changes: 4 additions & 2 deletions docs/architecture/debugMCPServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ extension. To avoid debugging the wrong workspace when several windows are open:
operation to that window's `ControlServer`. The target is cached per session so hint-less
follow-ups (step/continue/inspect) reach the same window. If the router window closes,
a worker window takes over the port on retry.
- Breakpoint tools also accept virtual source URIs. A sole registered window is unambiguous;
with multiple windows, callers provide the optional `workingDirectory` routing hint.

`DebugMCPServer` builds one handler **per MCP session** via a handler factory, which is
what lets concurrent agent sessions drive debuggers in different repos simultaneously.
Expand Down Expand Up @@ -133,7 +135,7 @@ error wins:
| `continue_execution` | Continue to next breakpoint |
| `pause_execution` | Interrupt a running program (no breakpoint needed) |
| `restart_debugging` | Restart session |
| `add/remove_breakpoint` | Breakpoint management |
| `add/remove_breakpoint` | Breakpoint management for local paths and virtual source URIs |
| `clear_all_breakpoints` | Remove all breakpoints |
| `list_breakpoints` | List active breakpoints |
| `get_variables_values` | Read the values of specifically named variables |
Expand All @@ -143,4 +145,4 @@ error wins:
## Configuration

- `debugmcp.serverPort`: Port number (default: 3001)
- `debugmcp.timeoutInSeconds`: Operation timeout (default: 180)
- `debugmcp.timeoutInSeconds`: Operation timeout (default: 180)
8 changes: 8 additions & 0 deletions docs/architecture/debuggingHandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Debugging is inherently asynchronous - when you step over a line, the debugger t
## Responsibility

- Orchestrate debugging operations (start, stop, step, breakpoints)
- Preserve language-extension virtual source URIs when opening documents and setting breakpoints
- Detect when debugger state has meaningfully changed after commands
- Format debug state into human/AI-readable responses
- Recursively format explicitly requested structs and arrays
Expand Down Expand Up @@ -64,6 +65,13 @@ A state change is considered meaningful when any of these change:
- Frame name (function/method)
- Frame ID

### Virtual source documents

Breakpoint and logpoint locations may be native paths or VS Code virtual-document URIs.
`src/utils/sourceUri.ts` keeps custom schemes intact instead of converting them into malformed
`file:` URIs. This is required for language-extension sources such as Business Central `.dal`
documents served through the `al-preview:` scheme.

### Root Cause Analysis

When debugging stops, the handler prompts AI agents to consider whether they found the root cause or just a symptom, encouraging deeper investigation.
Expand Down
17 changes: 10 additions & 7 deletions src/debugMCPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,33 +258,36 @@ export class DebugMCPServer {
server.registerTool('add_breakpoint', {
description: 'Set a breakpoint to pause execution at a critical line of code. Breakpoints let you inspect variables and control flow at exact moments.',
inputSchema: {
fileFullPath: z.string().describe('Full path to the file'),
fileFullPath: z.string().describe('Full path or VS Code virtual-document URI of the source file'),
workingDirectory: z.string().optional().describe('Workspace directory used to select the correct VS Code window. Required for a virtual-document URI when multiple windows are open.'),
line: z.number().int().describe('Line number (1-based) where the breakpoint should be set'),
condition: z.string().optional().describe('Optional condition expression. When provided, execution only pauses if this expression evaluates to true at the breakpoint location.'),
},
}, async (args: { fileFullPath: string; line: number; condition?: string }) =>
}, async (args: { fileFullPath: string; workingDirectory?: 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}".',
inputSchema: {
fileFullPath: z.string().describe('Full path to the file'),
fileFullPath: z.string().describe('Full path or VS Code virtual-document URI of the source file'),
workingDirectory: z.string().optional().describe('Workspace directory used to select the correct VS Code window. Required for a virtual-document URI when multiple windows are open.'),
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 }) =>
}, async (args: { fileFullPath: string; workingDirectory?: 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.',
inputSchema: {
fileFullPath: z.string().describe('Full path to the file'),
fileFullPath: z.string().describe('Full path or VS Code virtual-document URI of the source file'),
workingDirectory: z.string().optional().describe('Workspace directory used to select the correct VS Code window. Required for a virtual-document URI when multiple windows are open.'),
line: z.number().describe('Line number (1-based)'),
},
}, async (args: { fileFullPath: string; line: number }) =>
}, async (args: { fileFullPath: string; workingDirectory?: string; line: number }) =>
this.runTool('remove_breakpoint', () => debuggingHandler.handleRemoveBreakpoint(args)));

// Clear all breakpoints tool
Expand Down Expand Up @@ -653,4 +656,4 @@ export class DebugMCPServer {
isInitialized(): boolean {
return this.initialized;
}
}
}
25 changes: 22 additions & 3 deletions src/debuggingExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import * as fs from 'node:fs';
import { isSourceUri } from './utils/sourceUri';

function toSourceUri(source: string): vscode.Uri {
return isSourceUri(source) ? vscode.Uri.parse(source, true) : vscode.Uri.file(source);
}
// Copyright (c) Microsoft Corporation.

import * as vscode from 'vscode';
Expand Down Expand Up @@ -51,6 +57,7 @@ export interface IDebuggingExecutor {
getActiveSession(): DebugSessionInfo | undefined;
getActiveFrameId?(): number | undefined;
waitForDebugSessionReady(timeoutMs: number, signal?: AbortSignal): Promise<'stopped' | 'terminated' | 'timeout' | 'no-session' | 'attached'>;
getFileLineCount?(fileFullPath: string): Promise<number>;
dispose?(): Promise<void> | void;
}

Expand Down Expand Up @@ -85,6 +92,16 @@ export class DebuggingExecutor implements IDebuggingExecutor {
/**
* Start a debugging session
*/
public async getFileLineCount(fileFullPath: string): Promise<number> {
if (isSourceUri(fileFullPath)) {
const uri = toSourceUri(fileFullPath);
const document = await vscode.workspace.openTextDocument(uri);
return document.lineCount;
}
const content = await fs.promises.readFile(fileFullPath, 'utf8');
return content.length === 0 ? 0 : content.split(/\r?\n/).length;
}

public async startDebugging(
workingDirectory: string,
config: string | DebugConfiguration
Expand Down Expand Up @@ -350,7 +367,7 @@ export class DebuggingExecutor implements IDebuggingExecutor {
*/
public async addBreakpoint(fileFullPath: string, line: number, condition?: string, logMessage?: string): Promise<void> {
try {
const uri = vscode.Uri.file(fileFullPath);
const uri = toSourceUri(fileFullPath);
const breakpoint = new vscode.SourceBreakpoint(
new vscode.Location(uri, new vscode.Position(line - 1, 0)),
true,
Expand All @@ -369,7 +386,7 @@ export class DebuggingExecutor implements IDebuggingExecutor {
*/
public async removeBreakpoint(fileFullPath: string, line: number): Promise<void> {
try {
const uri = vscode.Uri.file(fileFullPath);
const uri = toSourceUri(fileFullPath);
const breakpoints = vscode.debug.breakpoints.filter(bp => {
if (bp instanceof vscode.SourceBreakpoint) {
return bp.location.uri.toString() === uri.toString() &&
Expand Down Expand Up @@ -746,7 +763,9 @@ export class DebuggingExecutor implements IDebuggingExecutor {
.filter((breakpoint): breakpoint is vscode.SourceBreakpoint =>
breakpoint instanceof vscode.SourceBreakpoint)
.map(breakpoint => ({
fileFullPath: breakpoint.location.uri.fsPath,
fileFullPath: isSourceUri(breakpoint.location.uri.toString())
? breakpoint.location.uri.toString()
: breakpoint.location.uri.fsPath,
line: breakpoint.location.range.start.line + 1,
condition: breakpoint.condition,
logMessage: breakpoint.logMessage
Expand Down
17 changes: 11 additions & 6 deletions src/debuggingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DebugConfigurationManager, IDebugConfigurationManager } from './utils/d
import { DebugState } from './debugState';
import { IDebuggingExecutor } from './debuggingExecutor';
import { logger } from './utils/logger';
import { isSourceUri } from './utils/sourceUri';
import {
isSensitiveExpression,
isSensitiveName,
Expand All @@ -26,9 +27,9 @@ export interface IDebuggingHandler {
handleContinue(): Promise<string>;
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>;
handleAddBreakpoint(args: { fileFullPath: string; workingDirectory?: string; line: number; condition?: string }): Promise<string>;
handleAddLogpoint(args: { fileFullPath: string; workingDirectory?: string; line: number; logMessage: string; condition?: string }): Promise<string>;
handleRemoveBreakpoint(args: { fileFullPath: string; workingDirectory?: string; line: number }): Promise<string>;
handleClearAllBreakpoints(): Promise<string>;
handleListBreakpoints(): Promise<string>;
handleGetVariables(args: { variableNames: string[]; scope?: 'local' | 'global' | 'all' }): Promise<string>;
Expand Down Expand Up @@ -394,7 +395,7 @@ export class DebuggingHandler implements IDebuggingHandler {
* Add a breakpoint at specified location. An optional condition makes it a
* conditional breakpoint that only pauses when the expression is true.
*/
public async handleAddBreakpoint(args: { fileFullPath: string; line: number; condition?: string }): Promise<string> {
public async handleAddBreakpoint(args: { fileFullPath: string; workingDirectory?: string; line: number; condition?: string }): Promise<string> {
const { fileFullPath, line, condition } = args;

try {
Expand Down Expand Up @@ -446,7 +447,7 @@ export class DebuggingHandler implements IDebuggingHandler {
* 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> {
public async handleAddLogpoint(args: { fileFullPath: string; workingDirectory?: string; line: number; logMessage: string; condition?: string }): Promise<string> {
const { fileFullPath, line, logMessage, condition } = args;

try {
Expand Down Expand Up @@ -476,10 +477,11 @@ export class DebuggingHandler implements IDebuggingHandler {
/**
* Remove a breakpoint from specified location
*/
public async handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise<string> {
public async handleRemoveBreakpoint(args: { fileFullPath: string; workingDirectory?: string; line: number }): Promise<string> {
const { fileFullPath, line } = args;

try {

// Check if breakpoint exists at this location
const breakpoints = this.executor.getBreakpoints();
const existingBreakpoint = breakpoints.find(bp =>
Expand Down Expand Up @@ -1018,6 +1020,9 @@ export class DebuggingHandler implements IDebuggingHandler {
}

private async getFileLineCount(fileFullPath: string): Promise<number> {
if (this.executor.getFileLineCount) {
return await this.executor.getFileLineCount(fileFullPath);
}
const content = await fs.promises.readFile(fileFullPath, 'utf8');
return content.length === 0 ? 0 : content.split(/\r?\n/).length;
}
Expand Down
Loading