diff --git a/README.md b/README.md index 84b869d..11dd867 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)
`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 | +| **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/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/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", diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index f44f641..9e2369c 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,13 +77,21 @@ 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.' + ), + 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 }] }; }); @@ -308,29 +326,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 +381,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..6fe8c3a 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,7 +23,9 @@ export class DebugState { public frameId: number | null; public threadId: number | null; public frameName: string | null; - // TODO breakpoints + public stackTrace: StackFrame[]; + public configurationName: string | null; + public breakpoints: string[]; constructor() { this.sessionActive = false; @@ -25,6 +37,9 @@ export class DebugState { this.frameId = null; this.threadId = null; this.frameName = null; + this.stackTrace = []; + this.configurationName = null; + this.breakpoints = []; } /** @@ -40,6 +55,9 @@ export class DebugState { this.frameId = null; this.threadId = null; this.frameName = null; + this.stackTrace = []; + this.configurationName = null; + this.breakpoints = []; } /** @@ -91,6 +109,13 @@ export class DebugState { this.frameName = frameName; } + /** + * Update stack trace + */ + public updateStackTrace(stackTrace: StackFrame[]): void { + this.stackTrace = [...stackTrace]; + } + /** * Check if frame name is available */ @@ -99,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; @@ -112,6 +148,53 @@ export class DebugState { cloned.frameId = this.frameId; 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 4274abb..47d1526 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 @@ -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) { @@ -181,12 +182,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( @@ -203,29 +208,52 @@ 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; } /** - * 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..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,41 +416,6 @@ export class DebuggingHandler implements IDebuggingHandler { } } - /** - * Format debug state as a readable string - */ - 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`; - - output += `${state.currentLine}: ${state.currentLineContent}\n`; - - // 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'; - } - - return output; - } - /** * Get current debug state */ 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 c8dec3b..7c0de61 100644 --- a/src/utils/agentConfigurationManager.ts +++ b/src/utils/agentConfigurationManager.ts @@ -156,11 +156,81 @@ 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` }; } + /** + * 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 */