From db82d6045a85da67f8273a18046b80f78f5ce37a Mon Sep 17 00:00:00 2001 From: sharmahardik Date: Tue, 17 Mar 2026 14:33:17 +0530 Subject: [PATCH 1/2] changes for DAP fallback --- package-lock.json | 8 --- src/debuggingExecutor.ts | 117 +++++++++++++++++++++++++++++++-------- src/debuggingHandler.ts | 14 ++--- 3 files changed, 102 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9558bd8..7c47db8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -499,7 +499,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -758,7 +757,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1517,7 +1515,6 @@ "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1748,7 +1745,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2153,7 +2149,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -3727,7 +3722,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3803,7 +3797,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4119,7 +4112,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/debuggingExecutor.ts b/src/debuggingExecutor.ts index 47d1526..a12f752 100644 --- a/src/debuggingExecutor.ts +++ b/src/debuggingExecutor.ts @@ -23,6 +23,7 @@ export interface IDebuggingExecutor { clearAllBreakpoints(): void; hasActiveSession(): Promise; getActiveSession(): vscode.DebugSession | undefined; + resolveActiveFrameId(): Promise; } /** @@ -38,7 +39,7 @@ export class DebuggingExecutor implements IDebuggingExecutor { config: vscode.DebugConfiguration ): Promise { try { - if (config.type === 'coreclr') { + if (config.type === 'coreclr' && config.request !== 'attach') { // Open the specific test file instead of the workspace folder const testFileUri = vscode.Uri.file(config.program); await vscode.commands.executeCommand('vscode.open', testFileUri); @@ -168,12 +169,13 @@ export class DebuggingExecutor implements IDebuggingExecutor { state.sessionActive = true; state.updateConfigurationName(activeSession.configuration.name ?? null); - const activeStackItem = vscode.debug.activeStackItem; - if (activeStackItem && 'frameId' in activeStackItem) { - state.updateContext(activeStackItem.frameId, activeStackItem.threadId); - + // Resolve frame context — handles DebugStackFrame, DebugThread, and undefined activeStackItem + const frameContext = await this.resolveFrameContext(activeSession); + if (frameContext) { + state.updateContext(frameContext.frameId, frameContext.threadId); + // Extract frame name from stack frame - await this.extractFrameName(activeSession, activeStackItem.frameId, state); + await this.extractFrameName(activeSession, frameContext.frameId, state); // Get the active editor const activeEditor = vscode.window.activeTextEditor; @@ -341,27 +343,98 @@ export class DebuggingExecutor implements IDebuggingExecutor { } /** - * Check if there's an active debug session that is ready for debugging operations + * Resolve the active frame context. + * Tries three strategies in order: + * 1. DebugStackFrame from activeStackItem (has frameId directly) + * 2. DebugThread from activeStackItem (resolve top frame via DAP stackTrace) + * 3. DAP threads request fallback (when activeStackItem is undefined) + * Returns null if no paused frame can be found. */ - public async hasActiveSession(): Promise { - // Quick check first - no session at all - if (!vscode.debug.activeDebugSession) { - return false; + private async resolveFrameContext(session: vscode.DebugSession): Promise<{ frameId: number; threadId: number } | null> { + const activeStackItem = vscode.debug.activeStackItem; + + // Strategy 1: DebugStackFrame — frameId is directly available + if (activeStackItem && 'frameId' in activeStackItem) { + return { frameId: activeStackItem.frameId, threadId: activeStackItem.threadId }; } + // Strategy 2: DebugThread — resolve top frame via DAP stackTrace + if (activeStackItem && 'threadId' in activeStackItem) { + const frame = await this.getTopFrameForThread(session, activeStackItem.threadId); + if (frame) { + return frame; + } + } + + // Strategy 3: activeStackItem is undefined — query DAP for threads directly + // and find the first thread that has a stack (i.e., is paused). + return await this.resolveFrameFromDAPThreads(session); + } + + /** + * Get the top stack frame for a specific thread via DAP stackTrace request. + * Returns null if the thread is running (not paused). + */ + private async getTopFrameForThread(session: vscode.DebugSession, threadId: number): Promise<{ frameId: number; threadId: number } | null> { try { - // Get the current debug state and check if it has location information - // This is the most reliable way to determine if the debugger is truly ready - const debugState = await this.getCurrentDebugState(); - - // A session is ready when it has location info (file name and line number) - // This means the debugger has attached and we can see where we are in the code - return debugState.sessionActive && debugState.hasLocationInfo(); - } catch (error) { - // Any error means session isn't ready (e.g., Python still initializing) - console.log('Session readiness check failed:', error); - return false; + const response = await session.customRequest('stackTrace', { + threadId, + startFrame: 0, + levels: 1 + }); + if (response?.stackFrames?.length > 0) { + return { frameId: response.stackFrames[0].id, threadId }; + } + } catch { + // Thread is running or stackTrace not supported + } + return null; + } + + /** + * Fallback: query DAP for all threads and find the first one with a valid stack frame. + * Used when activeStackItem is undefined (VS Code hasn't selected a thread). + */ + private async resolveFrameFromDAPThreads(session: vscode.DebugSession): Promise<{ frameId: number; threadId: number } | null> { + try { + const threadsResponse = await session.customRequest('threads'); + if (!threadsResponse?.threads?.length) { + return null; + } + + for (const thread of threadsResponse.threads) { + const frame = await this.getTopFrameForThread(session, thread.id); + if (frame) { + return frame; + } + } + } catch { + // threads request not supported or session not ready + } + return null; + } + + /** + * Resolve the active frame ID, handling DebugStackFrame, DebugThread, and + * undefined activeStackItem. Returns null if no paused frame is available. + */ + public async resolveActiveFrameId(): Promise { + const session = vscode.debug.activeDebugSession; + if (!session) { + return null; } + const context = await this.resolveFrameContext(session); + return context?.frameId ?? null; + } + + /** + * Check if there's an active debug session. + * This only checks session existence (debug adapter is attached), NOT whether + * execution is paused at a frame. Use resolveActiveFrameId() to check for a + * paused frame when needed (e.g., before variable inspection or expression eval). + */ + public async hasActiveSession(): Promise { + return !!vscode.debug.activeDebugSession; } /** diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts index 8dedcb8..8d8adf9 100644 --- a/src/debuggingHandler.ts +++ b/src/debuggingHandler.ts @@ -343,12 +343,12 @@ export class DebuggingHandler implements IDebuggingHandler { throw new Error('Debug session is not ready. Start debugging first and ensure execution is paused.'); } - const activeStackItem = vscode.debug.activeStackItem; - if (!activeStackItem || !('frameId' in activeStackItem)) { + const frameId = await this.executor.resolveActiveFrameId(); + if (frameId === null) { throw new Error('No active stack frame. Make sure execution is paused at a breakpoint.'); } - const variablesData = await this.executor.getVariables(activeStackItem.frameId, scope); + const variablesData = await this.executor.getVariables(frameId, scope); if (!variablesData.scopes || variablesData.scopes.length === 0) { return 'No variable scopes available at current execution point.'; @@ -393,13 +393,13 @@ export class DebuggingHandler implements IDebuggingHandler { throw new Error('Debug session is not ready. Start debugging first and ensure execution is paused.'); } - const activeStackItem = vscode.debug.activeStackItem; - if (!activeStackItem || !('frameId' in activeStackItem)) { + const frameId = await this.executor.resolveActiveFrameId(); + if (frameId === null) { throw new Error('No active stack frame. Make sure execution is paused at a breakpoint.'); } - const response = await this.executor.evaluateExpression(expression, activeStackItem.frameId); - + const response = await this.executor.evaluateExpression(expression, frameId); + if (response && response.result !== undefined) { let resultText = `Expression: ${expression}\n`; resultText += `Result: ${response.result}`; From 733e42a7611f797dffb5bbffb22c667a54be18a2 Mon Sep 17 00:00:00 2001 From: sharmahardik Date: Tue, 17 Mar 2026 14:36:53 +0530 Subject: [PATCH 2/2] add default launch config from workspace --- package.json | 5 +++++ src/debugMCPServer.ts | 4 ++-- src/debuggingHandler.ts | 11 ++++++++--- src/extension.ts | 6 +++++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a638ba2..a715df8 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,11 @@ "type": "number", "default": 3001, "description": "Port number for the DebugMCP server" + }, + "debugmcp.defaultConfigurationName": { + "type": "string", + "default": "", + "description": "Name of the launch.json configuration to use by default. When set, skips the configuration picker. Leave empty to prompt each time." } } } diff --git a/src/debugMCPServer.ts b/src/debugMCPServer.ts index 9e2369c..ecabbf2 100644 --- a/src/debugMCPServer.ts +++ b/src/debugMCPServer.ts @@ -27,11 +27,11 @@ export class DebugMCPServer { private debuggingHandler: IDebuggingHandler; private transports: Map = new Map(); - constructor(port: number, timeoutInSeconds: number) { + constructor(port: number, timeoutInSeconds: number, defaultConfigurationName?: string) { // Initialize the debugging components with dependency injection const executor = new DebuggingExecutor(); const configManager = new ConfigurationManager(); - this.debuggingHandler = new DebuggingHandler(executor, configManager, timeoutInSeconds); + this.debuggingHandler = new DebuggingHandler(executor, configManager, timeoutInSeconds, defaultConfigurationName); this.port = port; } diff --git a/src/debuggingHandler.ts b/src/debuggingHandler.ts index 8d8adf9..8831ac5 100644 --- a/src/debuggingHandler.ts +++ b/src/debuggingHandler.ts @@ -32,13 +32,16 @@ export class DebuggingHandler implements IDebuggingHandler { private readonly numNextLines: number = 3; private readonly executionDelay: number = 300; // ms to wait for debugger updates private readonly timeoutInSeconds: number; + private readonly defaultConfigurationName?: string; constructor( private readonly executor: IDebuggingExecutor, private readonly configManager: IDebugConfigurationManager, - timeoutInSeconds: number + timeoutInSeconds: number, + defaultConfigurationName?: string ) { this.timeoutInSeconds = timeoutInSeconds; + this.defaultConfigurationName = defaultConfigurationName; } /** @@ -53,7 +56,9 @@ export class DebuggingHandler implements IDebuggingHandler { const { fileFullPath, workingDirectory, testName, configurationName } = args; try { - let selectedConfigName = configurationName ?? await this.configManager.promptForConfiguration(workingDirectory); + let selectedConfigName = configurationName + ?? this.defaultConfigurationName + ?? await this.configManager.promptForConfiguration(workingDirectory); // Get debug configuration from launch.json or create default const debugConfig = await this.configManager.getDebugConfig( @@ -399,7 +404,7 @@ export class DebuggingHandler implements IDebuggingHandler { } const response = await this.executor.evaluateExpression(expression, frameId); - + if (response && response.result !== undefined) { let resultText = `Expression: ${expression}\n`; resultText += `Result: ${response.result}`; diff --git a/src/extension.ts b/src/extension.ts index 70b9c34..20170ef 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,9 +17,13 @@ export async function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration('debugmcp'); const timeoutInSeconds = config.get('timeoutInSeconds', 180); const serverPort = config.get('serverPort', 3001); + const defaultConfigurationName = config.get('defaultConfigurationName', '') || undefined; logger.info(`Using timeoutInSeconds: ${timeoutInSeconds} seconds`); logger.info(`Using serverPort: ${serverPort}`); + if (defaultConfigurationName) { + logger.info(`Using default configuration: ${defaultConfigurationName}`); + } // Initialize Agent Configuration Manager agentConfigManager = new AgentConfigurationManager(context, timeoutInSeconds, serverPort); @@ -35,7 +39,7 @@ export async function activate(context: vscode.ExtensionContext) { try { logger.info('Starting MCP server initialization...'); - mcpServer = new DebugMCPServer(serverPort, timeoutInSeconds); + mcpServer = new DebugMCPServer(serverPort, timeoutInSeconds, defaultConfigurationName); await mcpServer.initialize(); await mcpServer.start();