diff --git a/CHANGELOG.md b/CHANGELOG.md index 8afb0f6..b942314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ 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/). +## [2.1.0] - 2026-06-23 + +### Added +- **Conditional breakpoints** — `add_breakpoint` now accepts an optional `condition` expression so execution only pauses when the condition evaluates to true (e.g. `i == 5`, `user.id === null`). Conditions are surfaced in `list_breakpoints` and the debug state. + +### Changed +- **Renamed the companion Agent Skill from `really-debug` to `debug-live`.** It now installs at `skills/debug-live/` (e.g. `~/.copilot/skills/debug-live/`) and is invoked with `/debug-live`. Previous `really-debug` (and `debug`) installs are cleaned up automatically on registration. + +### Fixed +- **`continue`/step no longer hangs when the program runs to completion.** Detection now settles on termination of the specific session being debugged (by identity) instead of waiting for the global active session to clear, which previously hung until the timeout when a parent session (e.g. the JS debug terminal) outlived the program. + ## [1.2.0] - 2026-06-04 ### Added diff --git a/README.md b/README.md index 65ebfa2..e4084cc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Let AI agents debug your code inside VS Code - set breakpoints, step through exe ## ✨ What's New in 2.0.0 -- **`/really-debug` Agent Skill** — DebugMCP now ships a companion [Agent Skill](./skills/really-debug/SKILL.md) that is auto-installed into each configured harness's personal skills directory (e.g. `~/.copilot/skills/really-debug/`). Invoke it with `/really-debug` in supporting agents to load the systematic debugging workflow and trigger DebugMCP tools with the right context. +- **`/debug-live` Agent Skill** — DebugMCP now ships a companion [Agent Skill](./skills/debug-live/SKILL.md) that is auto-installed into each configured harness's personal skills directory (e.g. `~/.copilot/skills/debug-live/`). Invoke it with `/debug-live` in supporting agents to load the systematic debugging workflow and trigger DebugMCP tools with the right context. - **Robust debugging via the VS Code Testing API** — `start_debugging` with a `testName` now uses the VS Code Testing API to discover and launch the test, replacing the previous best-effort path. This works reliably across language test runners that integrate with the Testing API (pytest, Jest/Vitest, Java, .NET, Go, etc.) and produces consistent breakpoint hits inside individual test cases. ## 🚀 Quick Install @@ -57,7 +57,7 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C | **step_out** | Step out of the current function | None | | **continue_execution** | Continue until next breakpoint | None | | **restart_debugging** | Restart the current debug session | None | -| **add_breakpoint** | Add a breakpoint at a specific line | `fileFullPath` (required)
`lineContent` (required) | +| **add_breakpoint** | Add a breakpoint at a specific line (optionally conditional) | `fileFullPath` (required)
`lineContent` (required)
`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 | @@ -67,7 +67,7 @@ DebugMCP is an MCP server that gives AI coding agents full control over the VS C > **Note:** The MCP server intentionally exposes **tools only** — no procedural > instructions, no documentation resources. Workflow guidance (when to debug, how to > structure a root-cause investigation, language-specific quirks) lives in the companion -> [DebugMCP Agent Skill](./skills/really-debug/SKILL.md) so it can be loaded into an +> [DebugMCP Agent Skill](./skills/debug-live/SKILL.md) so it can be loaded into an > agent's prompt context independently of the MCP capability surface. ### 🎯 Debugging Best Practices diff --git a/docs/architecture/debugMCPServer.md b/docs/architecture/debugMCPServer.md index 4c47c0e..3e7e97a 100644 --- a/docs/architecture/debugMCPServer.md +++ b/docs/architecture/debugMCPServer.md @@ -39,7 +39,7 @@ AI Agent (MCP Client) `DebugMCPServer` exposes **tools only**. Procedural workflow guidance (when to debug, how to structure a root-cause investigation, language-specific quirks) lives in the -companion Agent Skill at `skills/really-debug/SKILL.md`, not in tool descriptions or MCP +companion Agent Skill at `skills/debug-live/SKILL.md`, not in tool descriptions or MCP resources. This separation matches modern agent ecosystems where MCP servers provide *capabilities* and skills provide *procedural knowledge* an agent loads as context. @@ -58,7 +58,7 @@ Each request creates a new stateless `StreamableHTTPServerTransport` instance th - Class definition: `src/debugMCPServer.ts` - Tool registration: `setupTools()` method (uses `McpServer.registerTool()`) - Server startup: `start()` method (creates express app with `/mcp` route) -- Agent Skill (companion, not part of the MCP surface): `skills/really-debug/SKILL.md` +- Agent Skill (companion, not part of the MCP surface): `skills/debug-live/SKILL.md` ## Exposed Tools diff --git a/package-lock.json b/package-lock.json index 566877d..bb2bf63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "debugmcpextension", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "debugmcpextension", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", diff --git a/package.json b/package.json index 6d4675e..067a4a5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "debugmcpextension", "displayName": "DebugMCP", "description": "Let AI agents debug your code inside VS Code — breakpoints, step-through execution, variable inspection, and expression evaluation. Automatically exposes itself as an MCP (Model Context Protocol) server for seamless integration with AI assistants.", - "version": "2.0.1", + "version": "2.1.0", "publisher": "ozzafar", "author": { "name": "Oz Zafar", @@ -79,9 +79,17 @@ "description": "Port number for the DebugMCP server" }, "debugmcp.bindHost": { - "type": ["string", "array"], - "items": { "type": "string" }, - "default": ["127.0.0.1", "::1"], + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, + "default": [ + "127.0.0.1", + "::1" + ], "markdownDescription": "Network interface(s) the DebugMCP HTTP server binds to. **Defaults to `[\"127.0.0.1\", \"::1\"]` (IPv4 + IPv6 loopback only).** Accepts a single string or an array of strings. ⚠️ **Security warning:** changing this to `0.0.0.0` or a LAN address exposes the unauthenticated MCP debugger — including arbitrary code execution via `evaluate_expression` and `start_debugging` — to every host on the network. Only change this if you fully understand the risk." } } diff --git a/skills/really-debug/README.md b/skills/debug-live/README.md similarity index 100% rename from skills/really-debug/README.md rename to skills/debug-live/README.md diff --git a/skills/really-debug/SKILL.md b/skills/debug-live/SKILL.md similarity index 99% rename from skills/really-debug/SKILL.md rename to skills/debug-live/SKILL.md index 1ea9e5e..660d6d9 100644 --- a/skills/really-debug/SKILL.md +++ b/skills/debug-live/SKILL.md @@ -1,5 +1,5 @@ --- -name: really-debug +name: debug-live description: Drive an interactive VS Code debugger to investigate bugs, failing tests, wrong/null variable values, unexpected runtime behavior, and other "it doesn't work" reports. Use this skill whenever speculation about runtime behavior would be cheaper to *verify* by stepping through the code than to reason about. Pairs with the DebugMCP MCP server, which exposes the underlying breakpoint / step / inspect tools. license: MIT allowed-tools: diff --git a/skills/really-debug/references/troubleshooting/cpp.md b/skills/debug-live/references/troubleshooting/cpp.md similarity index 100% rename from skills/really-debug/references/troubleshooting/cpp.md rename to skills/debug-live/references/troubleshooting/cpp.md diff --git a/skills/really-debug/references/troubleshooting/csharp.md b/skills/debug-live/references/troubleshooting/csharp.md similarity index 100% rename from skills/really-debug/references/troubleshooting/csharp.md rename to skills/debug-live/references/troubleshooting/csharp.md diff --git a/skills/really-debug/references/troubleshooting/go.md b/skills/debug-live/references/troubleshooting/go.md similarity index 100% rename from skills/really-debug/references/troubleshooting/go.md rename to skills/debug-live/references/troubleshooting/go.md diff --git a/skills/really-debug/references/troubleshooting/java.md b/skills/debug-live/references/troubleshooting/java.md similarity index 100% rename from skills/really-debug/references/troubleshooting/java.md rename to skills/debug-live/references/troubleshooting/java.md diff --git a/skills/really-debug/references/troubleshooting/javascript.md b/skills/debug-live/references/troubleshooting/javascript.md similarity index 100% rename from skills/really-debug/references/troubleshooting/javascript.md rename to skills/debug-live/references/troubleshooting/javascript.md diff --git a/skills/really-debug/references/troubleshooting/python.md b/skills/debug-live/references/troubleshooting/python.md similarity index 100% rename from skills/really-debug/references/troubleshooting/python.md rename to skills/debug-live/references/troubleshooting/python.md diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index d356b05..30f8c07 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -20,7 +20,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; * * Tools only. Procedural workflow guidance (when to debug, how to perform * root-cause analysis, language-specific quirks) lives in the companion - * Agent Skill at `skills/really-debug/` — the MCP surface itself only describes + * Agent Skill at `skills/debug-live/` — the MCP surface itself only describes * what each tool does, not how to use them together. */ /** @@ -155,7 +155,7 @@ export class DebugMCPServer { * Tool descriptions are intentionally terse and behavioral. Procedural * guidance (when to use which tool, how to perform root-cause analysis, * language-specific quirks) lives in the companion Agent Skill at - * `skills/really-debug/SKILL.md`. + * `skills/debug-live/SKILL.md`. */ private setupTools(server: McpServer) { // Start debugging tool @@ -231,12 +231,13 @@ export class DebugMCPServer { // Add breakpoint tool server.registerTool('add_breakpoint', { - description: 'Set a breakpoint to pause execution at a critical line of code. Essential for debugging: pause before potential errors, examine state at decision points, or verify code paths. Breakpoints let you inspect variables and control flow at exact moments.', + description: 'Set a breakpoint to pause execution at a critical line of code. Essential for debugging: pause before potential errors, examine state at decision points, or verify code paths. Breakpoints let you inspect variables and control flow at exact moments. Provide an optional condition to create a conditional breakpoint that only pauses when the expression evaluates to true (e.g. "i == 5" or "user.id === null").', inputSchema: { fileFullPath: z.string().describe('Full path to the file'), lineContent: z.string().describe('Line content'), + 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; lineContent: string }) => { + }, async (args: { fileFullPath: string; lineContent: string; condition?: string }) => { const result = await this.debuggingHandler.handleAddBreakpoint(args); return { content: [{ type: 'text' as const, text: result }] }; }); diff --git a/src/debuggingExecutor.ts b/src/debuggingExecutor.ts index 2d82244..1c7d169 100644 --- a/src/debuggingExecutor.ts +++ b/src/debuggingExecutor.ts @@ -31,7 +31,7 @@ export interface IDebuggingExecutor { stepOut(): Promise; continue(): Promise; restart(): Promise; - addBreakpoint(uri: vscode.Uri, line: number): Promise; + addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise; removeBreakpoint(uri: vscode.Uri, line: number): Promise; getCurrentDebugState(numNextLines: number): Promise; getVariables(frameId: number, scope?: 'local' | 'global' | 'all'): Promise; @@ -286,12 +286,16 @@ export class DebuggingExecutor implements IDebuggingExecutor { } /** - * Add a breakpoint at specified location + * Add a breakpoint at specified location. An optional condition makes it a + * conditional breakpoint that only pauses execution when the expression + * evaluates to true. */ - public async addBreakpoint(uri: vscode.Uri, line: number): Promise { + public async addBreakpoint(uri: vscode.Uri, line: number, condition?: string): Promise { try { const breakpoint = new vscode.SourceBreakpoint( - new vscode.Location(uri, new vscode.Position(line - 1, 0)) + new vscode.Location(uri, new vscode.Position(line - 1, 0)), + true, + condition ); vscode.debug.addBreakpoints([breakpoint]); } catch (error) { @@ -361,7 +365,8 @@ export class DebuggingExecutor implements IDebuggingExecutor { .map(bp => { const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop() || 'unknown'; const line = bp.location.range.start.line + 1; - return `${fileName}:${line}`; + const base = `${fileName}:${line}`; + return bp.condition ? `${base} [when: ${bp.condition}]` : base; }); state.updateBreakpoints(formattedBreakpoints); diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts index c8e7998..b04a785 100644 --- a/src/debuggingHandler.ts +++ b/src/debuggingHandler.ts @@ -17,7 +17,7 @@ export interface IDebuggingHandler { handleStepOut(): Promise; handleContinue(): Promise; handleRestart(): Promise; - handleAddBreakpoint(args: { fileFullPath: string; lineContent: string }): Promise; + handleAddBreakpoint(args: { fileFullPath: string; lineContent: string; condition?: string }): Promise; handleRemoveBreakpoint(args: { fileFullPath: string; line: number }): Promise; handleClearAllBreakpoints(): Promise; handleListBreakpoints(): Promise; @@ -271,10 +271,11 @@ export class DebuggingHandler implements IDebuggingHandler { } /** - * Add a breakpoint at specified location + * 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; lineContent: string }): Promise { - const { fileFullPath, lineContent } = args; + public async handleAddBreakpoint(args: { fileFullPath: string; lineContent: string; condition?: string }): Promise { + const { fileFullPath, lineContent, condition } = args; try { // Find the line number containing the line content @@ -297,14 +298,15 @@ export class DebuggingHandler implements IDebuggingHandler { // Add breakpoints to all matching lines for (const lineNumber of matchingLineNumbers) { - await this.executor.addBreakpoint(uri, lineNumber); + await this.executor.addBreakpoint(uri, lineNumber, condition); } + const conditionInfo = condition ? ` (condition: ${condition})` : ''; if (matchingLineNumbers.length === 1) { - return `Breakpoint added at ${fileFullPath}:${matchingLineNumbers[0]}`; + return `Breakpoint added at ${fileFullPath}:${matchingLineNumbers[0]}${conditionInfo}`; } else { const linesList = matchingLineNumbers.join(', '); - return `Breakpoints added at ${matchingLineNumbers.length} locations in ${fileFullPath}: lines ${linesList}`; + return `Breakpoints added at ${matchingLineNumbers.length} locations in ${fileFullPath}: lines ${linesList}${conditionInfo}`; } } catch (error) { throw new Error(`Error adding breakpoint: ${error}`); @@ -357,7 +359,8 @@ export class DebuggingHandler implements IDebuggingHandler { if (bp instanceof vscode.SourceBreakpoint) { const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop(); const line = bp.location.range.start.line + 1; - breakpointList += `${index + 1}. ${fileName}:${line}\n`; + const conditionInfo = bp.condition ? ` (condition: ${bp.condition})` : ''; + breakpointList += `${index + 1}. ${fileName}:${line}${conditionInfo}\n`; } else if (bp instanceof vscode.FunctionBreakpoint) { breakpointList += `${index + 1}. Function: ${bp.functionName}\n`; } @@ -487,6 +490,8 @@ export class DebuggingHandler implements IDebuggingHandler { private async waitForStateChange(beforeState: DebugState): Promise { const timeoutMs = this.timeoutInSeconds * 1000; const subscriptions: vscode.Disposable[] = []; + const operatingSession = this.executor.getActiveSession(); + let operatingSessionTerminated = false; try { await new Promise(resolve => { @@ -517,9 +522,12 @@ export class DebuggingHandler implements IDebuggingHandler { }) ); subscriptions.push( - vscode.debug.onDidTerminateDebugSession(() => { + vscode.debug.onDidTerminateDebugSession(session => { // continue/step that runs the program to completion. - if (!vscode.debug.activeDebugSession) { + if (operatingSession && session.id === operatingSession.id) { + operatingSessionTerminated = true; + settle(); + } else if (!vscode.debug.activeDebugSession) { settle(); } }) @@ -537,7 +545,14 @@ export class DebuggingHandler implements IDebuggingHandler { subscriptions.forEach(d => d.dispose()); } - return await this.executor.getCurrentDebugState(this.numNextLines); + const afterState = await this.executor.getCurrentDebugState(this.numNextLines); + // The operating session ended (program ran to completion). A lingering + // parent session (e.g. the JS debug terminal) can leave a different + // session reported as active, so reflect termination explicitly here. + if (operatingSessionTerminated) { + afterState.sessionActive = false; + } + return afterState; } /** diff --git a/src/utils/agentConfigurationManager.ts b/src/utils/agentConfigurationManager.ts index 0dd84e2..7534265 100644 --- a/src/utils/agentConfigurationManager.ts +++ b/src/utils/agentConfigurationManager.ts @@ -203,7 +203,7 @@ export class AgentConfigurationManager { * Path to the debugmcp skill bundled with the extension. */ private getBundledSkillPath(): string { - return path.join(this.context.extensionPath, 'skills', 'really-debug'); + return path.join(this.context.extensionPath, 'skills', 'debug-live'); } /** @@ -220,23 +220,29 @@ export class AgentConfigurationManager { } const skillsDir = this.getSkillsDirForAgent(agent); - const destination = path.join(skillsDir, 'really-debug'); + const destination = path.join(skillsDir, 'debug-live'); try { await fs.promises.mkdir(skillsDir, { recursive: true }); await fs.promises.cp(bundledSkillPath, destination, { recursive: true, force: true }); console.log(`Successfully registered debugmcp skill for ${agent.name} at ${destination}`); - // Back-compat cleanup: earlier 1.2.0 builds installed the skill at - // `/debug`. Remove the stale copy if it's still around so - // users don't end up with two competing entries. - const legacyDestination = path.join(skillsDir, 'debug'); - if (fs.existsSync(legacyDestination)) { - try { - await fs.promises.rm(legacyDestination, { recursive: true, force: true }); - console.log(`Removed legacy debugmcp skill at ${legacyDestination}`); - } catch (cleanupError) { - console.warn(`Failed to remove legacy debugmcp skill at ${legacyDestination}:`, cleanupError); + // Back-compat cleanup: earlier builds installed the skill under + // different directory names (`debug` in 1.2.0, later `really-debug`). + // Remove any stale copies so users don't end up with competing + // entries alongside the current `debug-live` skill. + const legacyDestinations = [ + path.join(skillsDir, 'debug'), + path.join(skillsDir, 'really-debug'), + ]; + for (const legacyDestination of legacyDestinations) { + if (fs.existsSync(legacyDestination)) { + try { + await fs.promises.rm(legacyDestination, { recursive: true, force: true }); + console.log(`Removed legacy debugmcp skill at ${legacyDestination}`); + } catch (cleanupError) { + console.warn(`Failed to remove legacy debugmcp skill at ${legacyDestination}:`, cleanupError); + } } }