From 21b307cad0640fa7906d756fa8bc1b2070132686 Mon Sep 17 00:00:00 2001 From: ozzafar <48795672+ozzafar@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:57:18 +0200 Subject: [PATCH 1/4] migrate to StreamableHTTPServerTransport and improve state --- docs/architecture/debugMCPServer.md | 20 +++---- src/debugMCPServer.ts | 78 ++++++++++++++++---------- src/debugState.ts | 21 +++++++ src/debuggingExecutor.ts | 36 ++++++++---- src/debuggingHandler.ts | 68 +++++++++++++--------- src/utils/agentConfigurationManager.ts | 4 +- 6 files changed, 148 insertions(+), 79 deletions(-) diff --git a/docs/architecture/debugMCPServer.md b/docs/architecture/debugMCPServer.md index 370b632..6288486 100644 --- a/docs/architecture/debugMCPServer.md +++ b/docs/architecture/debugMCPServer.md @@ -6,7 +6,7 @@ The MCP server component that exposes VS Code debugging capabilities to AI agent ## Motivation -AI coding agents need a standardized way to control debuggers programmatically. MCP provides this standard, and `DebugMCPServer` implements it using the official `@modelcontextprotocol/sdk` with Server-Sent Events (SSE) transport over an express HTTP server for real-time bidirectional communication. +AI coding agents need a standardized way to control debuggers programmatically. MCP provides this standard, and `DebugMCPServer` implements it using the official `@modelcontextprotocol/sdk` with Streamable HTTP transport over an express HTTP server. ## Responsibility @@ -14,17 +14,17 @@ AI coding agents need a standardized way to control debuggers programmatically. - Register debugging tools that AI agents can invoke - Register documentation resources for agent guidance - Delegate all debugging operations to `DebuggingHandler` -- Manage SSE transport connections via `SSEServerTransport` on configurable port (default: 3001) +- Manage Streamable HTTP transport via `StreamableHTTPServerTransport` on configurable port (default: 3001) ## Architecture Position ``` AI Agent (MCP Client) │ - ▼ SSE Connection (GET /sse + POST /messages) + ▼ HTTP POST /mcp ┌───────────────────┐ │ DebugMCPServer │ ◄── You are here -│ (express + SSE) │ +│ (express + HTTP) │ └───────────────────┘ │ ▼ Delegates to @@ -38,15 +38,14 @@ AI Agent (MCP Client) ### Tools vs Resources - **Tools**: Actions the AI can perform (start debugging, step over, etc.) -- **Resources**: Documentation the AI can read for guidance +- **Resources**: Documentation the AI can read for guidance (note: some clients like GitHub Copilot don't support resources, so the `get_debug_instructions` tool is also provided) -### SSE Transport +### Streamable HTTP Transport -Uses HTTP with Server-Sent Events for persistent connections. The express server exposes two endpoints: -- `GET /sse` — Establishes the SSE stream and creates a session -- `POST /messages?sessionId=...` — Receives JSON-RPC messages from the client +Uses stateless HTTP POST requests for MCP communication. The express server exposes: +- `POST /mcp` — Handles all MCP protocol messages (JSON-RPC over HTTP) -Each client connection creates an `SSEServerTransport` instance that is connected to the `McpServer`. The server tracks active transports by session ID and cleans them up on disconnect. +Each request creates a new `StreamableHTTPServerTransport` instance in stateless mode, which is cleaned up when the response closes. This approach is simpler than session-based transports and works well with standard HTTP clients. ## Key Code Locations @@ -59,6 +58,7 @@ Each client connection creates an `SSEServerTransport` instance that is connecte | Tool | Description | |------|-------------| +| `get_debug_instructions` | Get debugging guide (for clients that don't support resources) | | `start_debugging` | Start a debug session | | `stop_debugging` | Stop current session | | `step_over/into/out` | Stepping commands | diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index f44f641..f3a3bbb 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -13,7 +13,7 @@ import { } from '.'; import { logger } from './utils/logger'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; /** * Main MCP server class that exposes debugging functionality as tools and resources. @@ -25,7 +25,7 @@ export class DebugMCPServer { private port: number; private initialized: boolean = false; private debuggingHandler: IDebuggingHandler; - private transports: Map = new Map(); + private transports: Map = new Map(); constructor(port: number, timeoutInSeconds: number) { // Initialize the debugging components with dependency injection @@ -57,6 +57,16 @@ export class DebugMCPServer { * Setup MCP tools that delegate to the debugging handler */ private setupTools() { + // Get debug instructions tool (for clients that don't support MCP resources like GitHub Copilot) + this.mcpServer!.registerTool('get_debug_instructions', { + description: 'Get the debugging guide with step-by-step instructions for effective debugging. ' + + 'Returns comprehensive guidance including breakpoint strategies, root cause analysis framework, ' + + 'and best practices. Call this before starting a debug session.', + }, async () => { + const content = await this.loadMarkdownFile('agent-resources/debug_instructions.md'); + return { content: [{ type: 'text' as const, text: content }] }; + }); + // Start debugging tool this.mcpServer!.registerTool('start_debugging', { description: 'IMPORTANT DEBUGGING TOOL - Start a debug session for a code file' + @@ -67,11 +77,15 @@ export class DebugMCPServer { '\n• Functions return incorrect results' + '\n• Code behaves differently than expected' + '\n• User reports "it doesn\'t work"' + - '\n\n⚠️ CRITICAL: Before using this tool, first read debugmcp://docs/debug_instructions resource!', + '\n\n⚠️ CRITICAL: Before using this tool, first call get_debug_instructions or read debugmcp://docs/debug_instructions resource!', inputSchema: { fileFullPath: z.string().describe('Full path to the source code file to debug'), workingDirectory: z.string().describe('Working directory for the debug session'), - testName: z.string().optional().describe('Name of the specific test name to debug.'), + testName: z.string().optional().describe( + 'Name of a specific test name to debug. ' + + 'Only provide this when debugging a single test method. ' + + 'Leave empty to debug the entire file or test class.' + ), }, }, async (args: { fileFullPath: string; workingDirectory: string; testName?: string }) => { const result = await this.debuggingHandler.handleStartDebugging(args); @@ -308,29 +322,39 @@ export class DebugMCPServer { const express = expressModule.default; const app = express(); - // SSE endpoint — clients connect here to establish the MCP session - app.get('/sse', async (req: any, res: any) => { - logger.info('New SSE connection established'); - const transport = new SSEServerTransport('/messages', res); - this.transports.set(transport.sessionId, transport); + // Parse JSON body for incoming requests + app.use(express.json()); + + // Streamable HTTP endpoint — handles MCP protocol messages + app.post('/mcp', async (req: any, res: any) => { + logger.info('New MCP request received'); + + // Create a new transport for each request (stateless mode) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // Stateless mode - no session management + }); - transport.onclose = () => { - this.transports.delete(transport.sessionId); - logger.info(`SSE transport closed: ${transport.sessionId}`); - }; + // Clean up transport when response closes + res.on('close', () => { + transport.close(); + logger.info('MCP transport closed'); + }); + // Connect the MCP server to this transport await this.mcpServer!.connect(transport); + + // Handle the incoming request + await transport.handleRequest(req, res, req.body); }); - // Message endpoint — clients POST JSON-RPC messages here - app.post('/messages', async (req: any, res: any) => { - const sessionId = req.query.sessionId as string; - const transport = this.transports.get(sessionId); - if (!transport) { - res.status(404).json({ error: 'Session not found' }); - return; - } - await transport.handlePostMessage(req, res); + // Legacy SSE endpoint for backward compatibility + // Redirects to the new /mcp endpoint with appropriate headers + app.get('/sse', async (req: any, res: any) => { + res.status(410).json({ + error: 'SSE endpoint deprecated', + message: 'Please use POST /mcp endpoint instead', + newEndpoint: '/mcp' + }); }); // Start HTTP server @@ -353,14 +377,8 @@ export class DebugMCPServer { * Stop the MCP server */ async stop() { - // Close all active transports - for (const [sessionId, transport] of this.transports) { - try { - await transport.close(); - } catch (error) { - logger.error(`Error closing transport ${sessionId}`, error); - } - } + // Note: With stateless StreamableHTTPServerTransport, transports are closed per-request + // No need to track and close them manually this.transports.clear(); // Close the HTTP server diff --git a/src/debugState.ts b/src/debugState.ts index eed9160..012d449 100644 --- a/src/debugState.ts +++ b/src/debugState.ts @@ -1,5 +1,15 @@ // Copyright (c) Microsoft Corporation. +/** + * Represents a single stack frame in the call stack + */ +export interface StackFrame { + name: string; + source?: string; + line?: number; + column?: number; +} + /** * Represents the current state of a debugging session */ @@ -13,6 +23,7 @@ export class DebugState { public frameId: number | null; public threadId: number | null; public frameName: string | null; + public stackTrace: StackFrame[]; // TODO breakpoints constructor() { @@ -25,6 +36,7 @@ export class DebugState { this.frameId = null; this.threadId = null; this.frameName = null; + this.stackTrace = []; } /** @@ -40,6 +52,7 @@ export class DebugState { this.frameId = null; this.threadId = null; this.frameName = null; + this.stackTrace = []; } /** @@ -91,6 +104,13 @@ export class DebugState { this.frameName = frameName; } + /** + * Update stack trace + */ + public updateStackTrace(stackTrace: StackFrame[]): void { + this.stackTrace = [...stackTrace]; + } + /** * Check if frame name is available */ @@ -112,6 +132,7 @@ export class DebugState { cloned.frameId = this.frameId; cloned.threadId = this.threadId; cloned.frameName = this.frameName; + cloned.stackTrace = [...this.stackTrace]; return cloned; } } diff --git a/src/debuggingExecutor.ts b/src/debuggingExecutor.ts index 4274abb..571de3e 100644 --- a/src/debuggingExecutor.ts +++ b/src/debuggingExecutor.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. import * as vscode from 'vscode'; -import { DebugState } from './debugState'; +import { DebugState, StackFrame } from './debugState'; /** * Interface for debugging execution operations @@ -181,12 +181,16 @@ export class DebuggingExecutor implements IDebuggingExecutor { const currentLine = activeEditor.selection.active.line + 1; // 1-based line number const currentLineContent = activeEditor.document.lineAt(activeEditor.selection.active.line).text.trim(); - // Get next lines + // Get next non-empty lines const nextLines = []; - for (let i = 1; i <= numNextLines; i++) { - if (activeEditor.selection.active.line + i < activeEditor.document.lineCount) { - nextLines.push(activeEditor.document.lineAt(activeEditor.selection.active.line + i).text.trim()); + let lineOffset = 1; + while (nextLines.length < numNextLines && + activeEditor.selection.active.line + lineOffset < activeEditor.document.lineCount) { + const lineText = activeEditor.document.lineAt(activeEditor.selection.active.line + lineOffset).text.trim(); + if (lineText.length > 0) { + nextLines.push(lineText); } + lineOffset++; } state.updateLocation( @@ -207,25 +211,37 @@ export class DebuggingExecutor implements IDebuggingExecutor { } /** - * Extract frame name from the current stack frame + * Extract frame name and stack trace from the current debug session */ private async extractFrameName(session: vscode.DebugSession, frameId: number, state: DebugState): Promise { try { - // Get stack trace to extract frame name + // Get full stack trace (up to 50 frames) const stackTraceResponse = await session.customRequest('stackTrace', { threadId: state.threadId, startFrame: 0, - levels: 1 + levels: 50 }); if (stackTraceResponse?.stackFrames && stackTraceResponse.stackFrames.length > 0) { + // Extract frame name from current frame const currentFrame = stackTraceResponse.stackFrames[0]; state.updateFrameName(currentFrame.name || null); + + // Build stack trace array + const stackTrace: StackFrame[] = stackTraceResponse.stackFrames.map((frame: any) => ({ + name: frame.name || 'unknown', + source: frame.source?.path || frame.source?.name || undefined, + line: frame.line || undefined, + column: frame.column || undefined, + })); + + state.updateStackTrace(stackTrace); } } catch (error) { - console.log('Unable to extract frame name:', error); - // Set empty frame name on error + console.log('Unable to extract stack info:', error); + // Set empty values on error state.updateFrameName(null); + state.updateStackTrace([]); } } diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts index d38444a..0cb5c0b 100644 --- a/src/debuggingHandler.ts +++ b/src/debuggingHandler.ts @@ -422,38 +422,52 @@ export class DebuggingHandler implements IDebuggingHandler { } /** - * Format debug state as a readable string + * Format debug state as a JSON string for structured output */ private formatDebugState(state: DebugState): string { - if (!state.sessionActive) { - return 'Debug session is not active'; - } - - let output = 'Debug State:\n==========\n\n'; - - if (state.hasFrameName()) { - output += `Frame: ${state.frameName}\n`; - } - - if (state.hasLocationInfo()) { - output += `File: ${state.fileName}\n`; - output += `Line: ${state.currentLine}\n`; + const stateObject: { + sessionActive: boolean; + stackTrace?: string[]; + breakpoints?: string[]; + fileFullPath?: string | null; + fileName?: string | null; + currentLine?: number | null; + currentLineContent?: string | null; + nextLines?: string[]; + frameId?: number | null; + threadId?: number | null; + frameName?: string | null; + } = { + sessionActive: state.sessionActive, + }; + + if (state.sessionActive) { + // Compact stack trace: "functionName:line" format + stateObject.stackTrace = state.stackTrace.map(frame => + `${frame.name}:${frame.line || '?'}` + ); - output += `${state.currentLine}: ${state.currentLineContent}\n`; + // Compact breakpoints list: "fileName:line" format + const breakpoints = this.executor.getBreakpoints(); + stateObject.breakpoints = breakpoints + .filter((bp): bp is vscode.SourceBreakpoint => bp instanceof vscode.SourceBreakpoint) + .map(bp => { + const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop() || 'unknown'; + const line = bp.location.range.start.line + 1; + return `${fileName}:${line}`; + }); - // Show next few lines for context - // if (state.nextLines && state.nextLines.length > 0) { - // output += '\nNext lines:\n'; - // state.nextLines.forEach((line, index) => { - // const lineNumber = (state.currentLine || 0) + index + 1; - // output += ` ${lineNumber}: ${line}\n`; - // }); - // } - } else { - output += 'No location information available. The session might have stopped or ended\n'; + stateObject.fileFullPath = state.fileFullPath; + stateObject.fileName = state.fileName; + stateObject.currentLine = state.currentLine; + stateObject.currentLineContent = state.currentLineContent; + stateObject.nextLines = state.nextLines; + stateObject.frameId = state.frameId; + stateObject.threadId = state.threadId; + stateObject.frameName = state.frameName; } - - return output; + + return JSON.stringify(stateObject, null, 2); } /** diff --git a/src/utils/agentConfigurationManager.ts b/src/utils/agentConfigurationManager.ts index c8dec3b..c48bc80 100644 --- a/src/utils/agentConfigurationManager.ts +++ b/src/utils/agentConfigurationManager.ts @@ -156,8 +156,8 @@ export class AgentConfigurationManager { autoApprove: [], disabled: false, timeout: this.timeoutInSeconds, - type: "sse", - url: `http://localhost:${this.serverPort}/sse` + type: "streamableHttp", + url: `http://localhost:${this.serverPort}/mcp` }; } From 31fbba9e19c5826fa9dfc85167e6e315e8880d88 Mon Sep 17 00:00:00 2001 From: ozzafar <48795672+ozzafar@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:25:04 +0200 Subject: [PATCH 2/4] sse backward compatibility --- src/extension.ts | 7 +++ src/utils/agentConfigurationManager.ts | 70 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 4b801a6..70b9c34 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,6 +24,13 @@ export async function activate(context: vscode.ExtensionContext) { // Initialize Agent Configuration Manager agentConfigManager = new AgentConfigurationManager(context, timeoutInSeconds, serverPort); + // Migrate existing SSE configurations to streamableHttp (for backward compatibility) + try { + await agentConfigManager.migrateExistingConfigurations(); + } catch (error) { + logger.error('Error migrating existing configurations', error); + } + // Initialize MCP Server try { logger.info('Starting MCP server initialization...'); diff --git a/src/utils/agentConfigurationManager.ts b/src/utils/agentConfigurationManager.ts index c48bc80..7c0de61 100644 --- a/src/utils/agentConfigurationManager.ts +++ b/src/utils/agentConfigurationManager.ts @@ -161,6 +161,76 @@ export class AgentConfigurationManager { }; } + /** + * Migrate existing SSE configurations to streamableHttp + * This should be called on extension activation to ensure backward compatibility + */ + public async migrateExistingConfigurations(): Promise { + const agents = await this.getSupportedAgents(); + let migrationCount = 0; + + for (const agent of agents) { + try { + if (!fs.existsSync(agent.configPath)) { + continue; + } + + const configContent = await fs.promises.readFile(agent.configPath, 'utf8'); + let config: any; + + try { + config = JSON.parse(configContent); + } catch { + continue; // Skip if config can't be parsed + } + + const fieldName = agent.mcpServerFieldName; + const debugmcpConfig = config[fieldName]?.debugmcp; + + if (!debugmcpConfig) { + continue; // DebugMCP not configured for this agent + } + + // Check if it's using the old SSE configuration + const needsMigration = + debugmcpConfig.type === 'sse' || + debugmcpConfig.type === 'http' || + (debugmcpConfig.url && debugmcpConfig.url.endsWith('/sse')); + + if (needsMigration) { + console.log(`Migrating DebugMCP configuration for ${agent.displayName} from SSE to streamableHttp`); + + // Update to new configuration + config[fieldName].debugmcp = this.getDebugMCPConfig(); + + // Preserve any custom autoApprove settings + if (debugmcpConfig.autoApprove && Array.isArray(debugmcpConfig.autoApprove)) { + config[fieldName].debugmcp.autoApprove = debugmcpConfig.autoApprove; + } + + // Write the migrated config + await fs.promises.writeFile( + agent.configPath, + JSON.stringify(config, null, 2), + 'utf8' + ); + + migrationCount++; + console.log(`Successfully migrated ${agent.displayName} configuration`); + } + } catch (error) { + console.error(`Error migrating config for ${agent.name}:`, error); + // Continue with other agents even if one fails + } + } + + if (migrationCount > 0) { + vscode.window.showInformationMessage( + `DebugMCP: Migrated ${migrationCount} agent configuration(s) to use the new transport protocol.` + ); + } + } + /** * Add DebugMCP server configuration to the specified agent's config */ From f1c7f68ad50d92cad15b990e3bbfc6912357f88f Mon Sep 17 00:00:00 2001 From: ozzafar <48795672+ozzafar@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:43:05 +0200 Subject: [PATCH 3/4] update readme and version --- README.md | 69 +++++++++++++++++++++++++---------------------- package-lock.json | 4 +-- package.json | 2 +- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 84b869d..f0deec9 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,24 @@ DebugMCP bridges the gap between professional debugging and AI-assisted developm ### 🔧 Tools -| Tool | Description | Parameters | Example Usage | -|------|-------------|------------|---------------| -| **start_debugging** | Start a debug session for a source code file | `filePath` (required)
`workingDirectory` (optional)
`configurationName` (optional) | Start debugging a Python script | -| **stop_debugging** | Stop the current debug session | None | End the current debugging session | -| **step_over** | Execute the next line (step over function calls) | None | Move to next line without entering functions | -| **step_into** | Step into function calls | None | Enter function calls to debug them | -| **step_out** | Step out of the current function | None | Exit current function and return to caller | -| **continue_execution** | Continue until next breakpoint | None | Resume execution until hitting a breakpoint | -| **restart_debugging** | Restart the current debug session | None | Restart debugging from the beginning | -| **add_breakpoint** | Add a breakpoint at a specific line | `filePath` (required)
`line` (required) | Set breakpoint at line 25 of main.py | -| **remove_breakpoint** | Remove a breakpoint from a specific line | `filePath` (required)
`line` (required) | Remove breakpoint from line 25 | -| **list_breakpoints** | List all active breakpoints | None | Show all currently set breakpoints | -| **get_debug_status** | Get current debug session status | None | Check if debugging session is active | -| **get_variables** | Get variables and their values | `scope` (optional: 'local', 'global', 'all') | Inspect local variables | -| **evaluate_expression** | Evaluate an expression in debug context | `expression` (required) | Evaluate `user.name` or `len(items)` | +| Tool | Description | Parameters | +|------|-------------|------------| +| **get_debug_instructions** | Get the debugging guide with best practices and workflow instructions | None | +| **start_debugging** | Start a debug session for a source code file | `fileFullPath` (required)
`workingDirectory` (required)
`testName` (optional) | +| **stop_debugging** | Stop the current debug session | None | +| **step_over** | Execute the next line (step over function calls) | None | +| **step_into** | Step into function calls | None | +| **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) | +| **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 | +| **get_variables_values** | Get variables and their values at current execution point | `scope` (optional: 'local', 'global', 'all') | +| **evaluate_expression** | Evaluate an expression in debug context | `expression` (required) | + +> **Note:** The `get_debug_instructions` tool is particularly useful for AI clients like GitHub Copilot that don't support MCP resources. It provides the same debugging guide content that is also available as an MCP resource. ### 🎯 Debugging Best Practices @@ -120,14 +123,16 @@ The extension runs an MCP server automatically. It will pop up a message to auto ### Manual MCP Server Registration (Optional) +> **🔄 Auto-Migration**: If you previously configured DebugMCP with SSE transport, the extension will automatically migrate your configuration to the new Streamable HTTP transport on activation. + #### Cline Add to your Cline settings or `cline_mcp_settings.json`: ```json { "mcpServers": { "debugmcp": { - "transport": "sse", - "url": "http://localhost:3001/sse", + "type": "streamableHttp", + "url": "http://localhost:3001/mcp", "description": "DebugMCP - AI-powered debugging assistant" } } @@ -135,30 +140,30 @@ Add to your Cline settings or `cline_mcp_settings.json`: ``` #### GitHub Copilot -Add to your Copilot workspace settings (`.vscode/settings.json`): +Add to your VS Code settings (`settings.json`): ```json { - "github.copilot.mcp.servers": { - "debugmcp": { - "type": "sse", - "url": "http://localhost:3001/sse", - "description": "DebugMCP - Multi-language debugging support" + "mcp": { + "servers": { + "debugmcp": { + "type": "http", + "url": "http://localhost:3001/mcp", + "description": "DebugMCP - Multi-language debugging support" + } } } } ``` -#### Roo Code -Add to Roo's MCP settings: +#### Cursor +Add to Cursor's MCP settings: ```json { - "mcp": { - "servers": { - "debugmcp": { - "type": "sse", - "url": "http://localhost:3001/sse", - "description": "DebugMCP - Debugging tools for AI assistants" - } + "mcpServers": { + "debugmcp": { + "type": "streamableHttp", + "url": "http://localhost:3001/mcp", + "description": "DebugMCP - Debugging tools for AI assistants" } } } diff --git a/package-lock.json b/package-lock.json index 4e56587..86b841b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "debugmcpextension", - "version": "1.0.6", + "version": "1.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "debugmcpextension", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", diff --git a/package.json b/package.json index 51f92b1..971386b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "debugmcpextension", "displayName": "DebugMCP", "description": "A VSCode extension that provides comprehensive multi-language debugging capabilities and automatically exposes itself as an MCP (Model Context Protocol) server for seamless integration with AI assistants.", - "version": "1.0.6", + "version": "1.0.7", "publisher": "ozzafar", "author": { "name": "Oz Zafar", From 150612f1499aadc8ac79960ce8a843509bae8787 Mon Sep 17 00:00:00 2001 From: ozzafar <48795672+ozzafar@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:22:56 +0200 Subject: [PATCH 4/4] support configname + debug state tosting --- README.md | 2 +- src/debugMCPServer.ts | 6 +++- src/debugState.ts | 66 ++++++++++++++++++++++++++++++++++-- src/debuggingExecutor.ts | 12 +++++++ src/debuggingHandler.ts | 72 +++++----------------------------------- 5 files changed, 91 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index f0deec9..11dd867 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ DebugMCP bridges the gap between professional debugging and AI-assisted developm | Tool | Description | Parameters | |------|-------------|------------| | **get_debug_instructions** | Get the debugging guide with best practices and workflow instructions | None | -| **start_debugging** | Start a debug session for a source code file | `fileFullPath` (required)
`workingDirectory` (required)
`testName` (optional) | +| **start_debugging** | Start a debug session for a source code file | `fileFullPath` (required)
`workingDirectory` (required)
`testName` (optional)
`configurationName` (optional) | | **stop_debugging** | Stop the current debug session | None | | **step_over** | Execute the next line (step over function calls) | None | | **step_into** | Step into function calls | None | diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index f3a3bbb..9e2369c 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -86,8 +86,12 @@ export class DebugMCPServer { 'Only provide this when debugging a single test method. ' + 'Leave empty to debug the entire file or test class.' ), + configurationName: z.string().optional().describe( + 'Name of a specific debug configuration from launch.json to use. ' + + 'Leave empty to be prompted to select a configuration interactively.' + ), }, - }, async (args: { fileFullPath: string; workingDirectory: string; testName?: string }) => { + }, async (args: { fileFullPath: string; workingDirectory: string; testName?: string; configurationName?: string }) => { const result = await this.debuggingHandler.handleStartDebugging(args); return { content: [{ type: 'text' as const, text: result }] }; }); diff --git a/src/debugState.ts b/src/debugState.ts index 012d449..6fe8c3a 100644 --- a/src/debugState.ts +++ b/src/debugState.ts @@ -24,7 +24,8 @@ export class DebugState { public threadId: number | null; public frameName: string | null; public stackTrace: StackFrame[]; - // TODO breakpoints + public configurationName: string | null; + public breakpoints: string[]; constructor() { this.sessionActive = false; @@ -37,6 +38,8 @@ export class DebugState { this.threadId = null; this.frameName = null; this.stackTrace = []; + this.configurationName = null; + this.breakpoints = []; } /** @@ -53,6 +56,8 @@ export class DebugState { this.threadId = null; this.frameName = null; this.stackTrace = []; + this.configurationName = null; + this.breakpoints = []; } /** @@ -119,8 +124,19 @@ export class DebugState { } /** - * Clone the current state + * Update the configuration name */ + public updateConfigurationName(configurationName: string | null): void { + this.configurationName = configurationName; + } + + /** + * Update breakpoints list (formatted as "fileName:line" strings) + */ + public updateBreakpoints(breakpoints: string[]): void { + this.breakpoints = [...breakpoints]; + } + public clone(): DebugState { const cloned = new DebugState(); cloned.sessionActive = this.sessionActive; @@ -133,6 +149,52 @@ export class DebugState { cloned.threadId = this.threadId; cloned.frameName = this.frameName; cloned.stackTrace = [...this.stackTrace]; + cloned.configurationName = this.configurationName; + cloned.breakpoints = [...this.breakpoints]; return cloned; } + + /** + * Format debug state as a JSON string for structured output + */ + public toString(): string { + const stateObject: { + sessionActive: boolean; + configurationName?: string | null; + stackTrace?: string[]; + breakpoints?: string[]; + fileFullPath?: string | null; + fileName?: string | null; + currentLine?: number | null; + currentLineContent?: string | null; + nextLines?: string[]; + frameId?: number | null; + threadId?: number | null; + frameName?: string | null; + } = { + sessionActive: this.sessionActive, + }; + + if (this.sessionActive) { + stateObject.configurationName = this.configurationName; + + // Compact stack trace: "functionName:line" format + stateObject.stackTrace = this.stackTrace.map(frame => + `${frame.name}:${frame.line || '?'}` + ); + + stateObject.breakpoints = this.breakpoints; + + stateObject.fileFullPath = this.fileFullPath; + stateObject.fileName = this.fileName; + stateObject.currentLine = this.currentLine; + stateObject.currentLineContent = this.currentLineContent; + stateObject.nextLines = this.nextLines; + stateObject.frameId = this.frameId; + stateObject.threadId = this.threadId; + stateObject.frameName = this.frameName; + } + + return JSON.stringify(stateObject, null, 2); + } } diff --git a/src/debuggingExecutor.ts b/src/debuggingExecutor.ts index 571de3e..47d1526 100644 --- a/src/debuggingExecutor.ts +++ b/src/debuggingExecutor.ts @@ -166,6 +166,7 @@ export class DebuggingExecutor implements IDebuggingExecutor { const activeSession = vscode.debug.activeDebugSession; if (activeSession) { state.sessionActive = true; + state.updateConfigurationName(activeSession.configuration.name ?? null); const activeStackItem = vscode.debug.activeStackItem; if (activeStackItem && 'frameId' in activeStackItem) { @@ -207,6 +208,17 @@ export class DebuggingExecutor implements IDebuggingExecutor { console.log('Unable to get debug state:', error); } + // Populate breakpoints as compact "fileName:line" strings + const breakpoints = vscode.debug.breakpoints; + const formattedBreakpoints = breakpoints + .filter((bp): bp is vscode.SourceBreakpoint => bp instanceof vscode.SourceBreakpoint) + .map(bp => { + const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop() || 'unknown'; + const line = bp.location.range.start.line + 1; + return `${fileName}:${line}`; + }); + state.updateBreakpoints(formattedBreakpoints); + return state; } diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts index 0cb5c0b..8dedcb8 100644 --- a/src/debuggingHandler.ts +++ b/src/debuggingHandler.ts @@ -10,7 +10,7 @@ import { logger } from './utils/logger'; * Interface for debugging handler operations */ export interface IDebuggingHandler { - handleStartDebugging(args: { fileFullPath: string; workingDirectory: string; testName?: string }): Promise; + handleStartDebugging(args: { fileFullPath: string; workingDirectory: string; testName?: string; configurationName?: string }): Promise; handleStopDebugging(): Promise; handleStepOver(): Promise; handleStepInto(): Promise; @@ -48,11 +48,12 @@ export class DebuggingHandler implements IDebuggingHandler { fileFullPath: string; workingDirectory: string; testName?: string; + configurationName?: string; }): Promise { - const { fileFullPath, workingDirectory, testName } = args; + const { fileFullPath, workingDirectory, testName, configurationName } = args; try { - let selectedConfigName = await this.configManager.promptForConfiguration(workingDirectory); + let selectedConfigName = configurationName ?? await this.configManager.promptForConfiguration(workingDirectory); // Get debug configuration from launch.json or create default const debugConfig = await this.configManager.getDebugConfig( @@ -75,7 +76,7 @@ export class DebuggingHandler implements IDebuggingHandler { const configInfo = selectedConfigName ? ` using configuration '${selectedConfigName}'` : ' with default configuration'; const testInfo = testName ? ` (test: ${testName})` : ''; const currentState = await this.executor.getCurrentDebugState(this.numNextLines); - return `Debug session started successfully for: ${fileFullPath}${configInfo}${testInfo}. Current state: ${this.formatDebugState(currentState)}`; + return `Debug session started successfully for: ${fileFullPath}${configInfo}${testInfo}. Current state: ${currentState.toString()}`; } else { throw new Error('Failed to start debug session. Make sure the appropriate language extension is installed.'); } @@ -137,8 +138,7 @@ export class DebuggingHandler implements IDebuggingHandler { // Wait for debugger state to change const afterState = await this.waitForStateChange(beforeState); - // Format the debug state as a string - return this.formatDebugState(afterState); + return afterState.toString(); } catch (error) { throw new Error(`Error executing step over: ${error}`); } @@ -161,8 +161,7 @@ export class DebuggingHandler implements IDebuggingHandler { // Wait for debugger state to change const afterState = await this.waitForStateChange(beforeState); - // Format the debug state as a string - return this.formatDebugState(afterState); + return afterState.toString(); } catch (error) { throw new Error(`Error executing step into: ${error}`); } @@ -185,8 +184,7 @@ export class DebuggingHandler implements IDebuggingHandler { // Wait for debugger state to change const afterState = await this.waitForStateChange(beforeState); - // Format the debug state as a string - return this.formatDebugState(afterState); + return afterState.toString(); } catch (error) { throw new Error(`Error executing step out: ${error}`); } @@ -209,10 +207,7 @@ export class DebuggingHandler implements IDebuggingHandler { // Wait for debugger state to change const afterState = await this.waitForStateChange(beforeState); - let result = this.formatDebugState(afterState); - - - return result; + return afterState.toString(); } catch (error) { throw new Error(`Error executing continue: ${error}`); } @@ -421,55 +416,6 @@ export class DebuggingHandler implements IDebuggingHandler { } } - /** - * Format debug state as a JSON string for structured output - */ - private formatDebugState(state: DebugState): string { - const stateObject: { - sessionActive: boolean; - stackTrace?: string[]; - breakpoints?: string[]; - fileFullPath?: string | null; - fileName?: string | null; - currentLine?: number | null; - currentLineContent?: string | null; - nextLines?: string[]; - frameId?: number | null; - threadId?: number | null; - frameName?: string | null; - } = { - sessionActive: state.sessionActive, - }; - - if (state.sessionActive) { - // Compact stack trace: "functionName:line" format - stateObject.stackTrace = state.stackTrace.map(frame => - `${frame.name}:${frame.line || '?'}` - ); - - // Compact breakpoints list: "fileName:line" format - const breakpoints = this.executor.getBreakpoints(); - stateObject.breakpoints = breakpoints - .filter((bp): bp is vscode.SourceBreakpoint => bp instanceof vscode.SourceBreakpoint) - .map(bp => { - const fileName = bp.location.uri.fsPath.split(/[/\\]/).pop() || 'unknown'; - const line = bp.location.range.start.line + 1; - return `${fileName}:${line}`; - }); - - stateObject.fileFullPath = state.fileFullPath; - stateObject.fileName = state.fileName; - stateObject.currentLine = state.currentLine; - stateObject.currentLineContent = state.currentLineContent; - stateObject.nextLines = state.nextLines; - stateObject.frameId = state.frameId; - stateObject.threadId = state.threadId; - stateObject.frameName = state.frameName; - } - - return JSON.stringify(stateObject, null, 2); - } - /** * Get current debug state */