From 2b8f43aec3422b8679ea8f31dad1e6436aeb82b2 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Wed, 9 Apr 2025 09:13:54 -0500 Subject: [PATCH 1/5] Add support for setting breakpoints and viewing call stack information to VS Code MCP Server --- .../mcp-server-vscode/src/extension.ts | 40 ++++ .../src/tools/debug_tools.d.ts | 63 ++++++ .../src/tools/debug_tools.ts | 195 ++++++++++++++++++ 3 files changed, 298 insertions(+) diff --git a/mcp-servers/mcp-server-vscode/src/extension.ts b/mcp-servers/mcp-server-vscode/src/extension.ts index 85a4eb16..ab1c5c1f 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -9,8 +9,12 @@ import { z } from 'zod'; import packageJson from '../package.json'; import { codeCheckerTool } from './tools/code_checker'; import { + getCallStack, + getCallStackSchema, listDebugSessions, listDebugSessionsSchema, + setBreakpoint, + setBreakpointSchema, startDebugSession, startDebugSessionSchema, stopDebugSession, @@ -153,6 +157,42 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); + // Register 'set_breakpoint' tool + mcpServer.tool( + 'set_breakpoint', + 'Set a breakpoint at a specific line in a file.', + setBreakpointSchema.shape, + async (params) => { + const result = await setBreakpoint(params); + return { + ...result, + content: result.content.map((item) => ({ + ...item, + type: 'text' as const, + })), + }; + }, + ); + + // Register 'get_call_stack' tool + mcpServer.tool( + 'get_call_stack', + 'Get the current call stack information for an active debug session.', + getCallStackSchema.shape, + async (params) => { + const result = await getCallStack(params); + return { + ...result, + content: result.content.map((item) => { + if (item.type === 'json') { + return { type: 'text' as const, text: JSON.stringify(item.json) }; + } + return { ...item, type: 'text' as const }; + }), + }; + }, + ); + // Register 'stop_debug_session' tool // Register 'restart_debug_session' tool diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts index f974eb6b..09a03996 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts @@ -79,3 +79,66 @@ export declare const stopDebugSessionSchema: z.ZodObject<{ }, { sessionName: string; }>; + +export declare const setBreakpoint: (params: { + filePath: string; + line: number; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +}>; + +export declare const setBreakpointSchema: z.ZodObject<{ + filePath: z.ZodString; + line: z.ZodNumber; +}, "strip", z.ZodTypeAny, { + filePath: string; + line: number; +}, { + filePath: string; + line: number; +}>; + +export declare const getCallStack: (params: { + sessionName?: string; +}) => Promise<{ + content: ({ + type: string; + json: { + callStacks: { + sessionId: string; + sessionName: string; + threads: { + threadId: number; + threadName: string; + stackFrames?: { + id: number; + name: string; + source?: { + name: string; + path: string; + }; + line: number; + column: number; + }[]; + error?: string; + }[]; + }; + }; + } | { + type: string; + text: string; + })[]; + isError: boolean; +}>; + +export declare const getCallStackSchema: z.ZodObject<{ + sessionName: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionName?: string; +}, { + sessionName?: string; +}>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts index ebcf5bae..3e7f750e 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import * as vscode from 'vscode'; import { z } from 'zod'; @@ -133,3 +134,197 @@ export const stopDebugSession = async (params: { sessionName: string }) => { export const stopDebugSessionSchema = z.object({ sessionName: z.string().describe('The name of the debug session(s) to stop.'), }); + +/** + * Set a breakpoint at a specific line in a file. + * + * @param params - Object containing filePath and line number for the breakpoint. + */ +export const setBreakpoint = async (params: { filePath: string; line: number }) => { + const { filePath, line } = params; + + try { + // Create a URI from the file path + const fileUri = vscode.Uri.file(filePath); + + // Check if the file exists + try { + await vscode.workspace.fs.stat(fileUri); + } catch (error) { + return { + content: [ + { + type: 'text', + text: `File not found: ${filePath}`, + }, + ], + isError: true, + }; + } + + // Create a new breakpoint + const breakpoint = new vscode.SourceBreakpoint( + new vscode.Location(fileUri, new vscode.Position(line - 1, 0)) + ); + + // Add the breakpoint + const added = vscode.debug.addBreakpoints([breakpoint]); + + if (added.length === 0) { + return { + content: [ + { + type: 'text', + text: `Failed to set breakpoint at line ${line} in ${path.basename(filePath)}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: 'text', + text: `Breakpoint set at line ${line} in ${path.basename(filePath)}`, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error setting breakpoint: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating set_breakpoint parameters. +export const setBreakpointSchema = z.object({ + filePath: z.string().describe('The absolute path to the file where the breakpoint should be set.'), + line: z.number().int().min(1).describe('The line number where the breakpoint should be set (1-based).'), +}); + +/** + * Get the current call stack information for an active debug session. + * + * @param params - Object containing the sessionName to get call stack for. + */ +export const getCallStack = async (params: { sessionName?: string }) => { + const { sessionName } = params; + + // Get all active debug sessions or filter by name if provided + let sessions = activeSessions; + if (sessionName) { + sessions = activeSessions.filter((session) => session.name === sessionName); + if (sessions.length === 0) { + return { + content: [ + { + type: 'text', + text: `No debug session found with name '${sessionName}'.`, + }, + ], + isError: true, + }; + } + } + + if (sessions.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No active debug sessions found.', + }, + ], + isError: true, + }; + } + + try { + // Get call stack information for each session + const callStacks = await Promise.all( + sessions.map(async (session) => { + try { + // Get all threads for the session + const threads = await session.customRequest('threads'); + + // Get stack traces for each thread + const stackTraces = await Promise.all( + threads.threads.map(async (thread: { id: number; name: string }) => { + try { + const stackTrace = await session.customRequest('stackTrace', { + threadId: thread.id, + }); + + return { + threadId: thread.id, + threadName: thread.name, + stackFrames: stackTrace.stackFrames.map((frame: any) => ({ + id: frame.id, + name: frame.name, + source: frame.source ? { + name: frame.source.name, + path: frame.source.path, + } : undefined, + line: frame.line, + column: frame.column, + })), + }; + } catch (error) { + return { + threadId: thread.id, + threadName: thread.name, + error: error instanceof Error ? error.message : String(error), + }; + } + }) + ); + + return { + sessionId: session.id, + sessionName: session.name, + threads: stackTraces, + }; + } catch (error) { + return { + sessionId: session.id, + sessionName: session.name, + error: error instanceof Error ? error.message : String(error), + }; + } + }) + ); + + return { + content: [ + { + type: 'json', + json: { callStacks }, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting call stack: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating get_call_stack parameters. +export const getCallStackSchema = z.object({ + sessionName: z.string().optional().describe('The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.'), +}); From 70ebe7b477b5920848971d0907353a64082acd9f Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Wed, 9 Apr 2025 10:57:08 -0500 Subject: [PATCH 2/5] Add enhanced debugging capabilities to MCP Server extension - Fix setBreakpoint implementation to properly verify breakpoint setting - Add resumeDebugSession tool to continue execution after breakpoint hit - Add getStackFrameVariables tool to inspect variables at breakpoints - Add variable name filtering capability to limit stack frame variable output --- .../mcp-server-vscode/src/extension.ts | 66 ++++-- .../src/tools/debug_tools.d.ts | 103 ++++++--- .../src/tools/debug_tools.ts | 195 ++++++++++++++++-- 3 files changed, 309 insertions(+), 55 deletions(-) diff --git a/mcp-servers/mcp-server-vscode/src/extension.ts b/mcp-servers/mcp-server-vscode/src/extension.ts index ab1c5c1f..3f5a3c00 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -11,8 +11,12 @@ import { codeCheckerTool } from './tools/code_checker'; import { getCallStack, getCallStackSchema, + getStackFrameVariables, + getStackFrameVariablesSchema, listDebugSessions, listDebugSessionsSchema, + resumeDebugSession, + resumeDebugSessionSchema, setBreakpoint, setBreakpointSchema, startDebugSession, @@ -184,7 +188,8 @@ export const activate = async (context: vscode.ExtensionContext) => { return { ...result, content: result.content.map((item) => { - if (item.type === 'json') { + if ('json' in item) { + // Convert json content to text string return { type: 'text' as const, text: JSON.stringify(item.json) }; } return { ...item, type: 'text' as const }; @@ -193,19 +198,13 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); - // Register 'stop_debug_session' tool - - // Register 'restart_debug_session' tool + // Register 'resume_debug_session' tool mcpServer.tool( - 'restart_debug_session', - 'Restart a debug session by stopping it and then starting it with the provided configuration.', - startDebugSessionSchema.shape, // using the same schema as 'start_debug_session' + 'resume_debug_session', + 'Resume execution of a debug session that has been paused (e.g., by a breakpoint).', + resumeDebugSessionSchema.shape, async (params) => { - // Stop current session using the provided session name - await stopDebugSession({ sessionName: params.configuration.name }); - - // Then start a new debug session with the given configuration - const result = await startDebugSession(params); + const result = await resumeDebugSession(params); return { ...result, content: result.content.map((item) => ({ @@ -215,6 +214,28 @@ export const activate = async (context: vscode.ExtensionContext) => { }; }, ); + + // Register 'get_stack_frame_variables' tool + mcpServer.tool( + 'get_stack_frame_variables', + 'Get variables from a specific stack frame in a debug session.', + getStackFrameVariablesSchema.shape, + async (params) => { + const result = await getStackFrameVariables(params); + return { + ...result, + content: result.content.map((item) => { + if ('json' in item) { + // Convert json content to text string + return { type: 'text' as const, text: JSON.stringify(item.json) }; + } + return { ...item, type: 'text' as const }; + }), + }; + }, + ); + + // Register 'stop_debug_session' tool mcpServer.tool( 'stop_debug_session', 'Stop all debug sessions that match the provided session name.', @@ -231,6 +252,27 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); + // Register 'restart_debug_session' tool + mcpServer.tool( + 'restart_debug_session', + 'Restart a debug session by stopping it and then starting it with the provided configuration.', + startDebugSessionSchema.shape, // using the same schema as 'start_debug_session' + async (params) => { + // Stop current session using the provided session name + await stopDebugSession({ sessionName: params.configuration.name }); + + // Then start a new debug session with the given configuration + const result = await startDebugSession(params); + return { + ...result, + content: result.content.map((item) => ({ + ...item, + type: 'text' as const, + })), + }; + }, + ); + // Set up an Express app to handle SSE connections const app = express(); const mcpConfig = vscode.workspace.getConfiguration('mcpServer'); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts index 09a03996..5da80994 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts @@ -79,7 +79,6 @@ export declare const stopDebugSessionSchema: z.ZodObject<{ }, { sessionName: string; }>; - export declare const setBreakpoint: (params: { filePath: string; line: number; @@ -90,7 +89,6 @@ export declare const setBreakpoint: (params: { }[]; isError: boolean; }>; - export declare const setBreakpointSchema: z.ZodObject<{ filePath: z.ZodString; line: z.ZodNumber; @@ -101,44 +99,93 @@ export declare const setBreakpointSchema: z.ZodObject<{ filePath: string; line: number; }>; - export declare const getCallStack: (params: { sessionName?: string; }) => Promise<{ - content: ({ + content: { + type: string; + text: string; + }[]; + isError: boolean; +} | { + content: { type: string; json: { - callStacks: { + callStacks: ({ + sessionId: string; + sessionName: string; + threads: any[]; + error?: undefined; + } | { sessionId: string; sessionName: string; - threads: { - threadId: number; - threadName: string; - stackFrames?: { - id: number; - name: string; - source?: { - name: string; - path: string; - }; - line: number; - column: number; - }[]; - error?: string; - }[]; - }; + error: string; + threads?: undefined; + })[]; }; - } | { - type: string; - text: string; - })[]; + }[]; isError: boolean; }>; - export declare const getCallStackSchema: z.ZodObject<{ sessionName: z.ZodOptional; }, "strip", z.ZodTypeAny, { - sessionName?: string; + sessionName?: string | undefined; }, { - sessionName?: string; + sessionName?: string | undefined; +}>; +export declare const resumeDebugSession: (params: { + sessionId: string; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +}>; +export declare const resumeDebugSessionSchema: z.ZodObject<{ + sessionId: z.ZodString; +}, "strip", z.ZodTypeAny, { + sessionId: string; +}, { + sessionId: string; +}>; +export declare const getStackFrameVariables: (params: { + sessionId: string; + frameId: number; + threadId: number; + filter?: string; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +} | { + content: { + type: string; + json: { + sessionId: string; + frameId: number; + threadId: number; + variablesByScope: any[]; + filter: string | undefined; + }; + }[]; + isError: boolean; +}>; +export declare const getStackFrameVariablesSchema: z.ZodObject<{ + sessionId: z.ZodString; + frameId: z.ZodNumber; + threadId: z.ZodNumber; + filter: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionId: string; + frameId: number; + threadId: number; + filter?: string | undefined; +}, { + sessionId: string; + frameId: number; + threadId: number; + filter?: string | undefined; }>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts index 3e7f750e..d5dff785 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts @@ -163,14 +163,22 @@ export const setBreakpoint = async (params: { filePath: string; line: number }) } // Create a new breakpoint - const breakpoint = new vscode.SourceBreakpoint( - new vscode.Location(fileUri, new vscode.Position(line - 1, 0)) - ); - - // Add the breakpoint - const added = vscode.debug.addBreakpoints([breakpoint]); - - if (added.length === 0) { + const breakpoint = new vscode.SourceBreakpoint(new vscode.Location(fileUri, new vscode.Position(line - 1, 0))); + + // Add the breakpoint - note that addBreakpoints returns void, not an array + vscode.debug.addBreakpoints([breakpoint]); + + // Check if the breakpoint was successfully added by verifying it exists in VS Code's breakpoints + const breakpoints = vscode.debug.breakpoints; + const breakpointAdded = breakpoints.some((bp) => { + if (bp instanceof vscode.SourceBreakpoint) { + const loc = bp.location; + return loc.uri.fsPath === fileUri.fsPath && loc.range.start.line === line - 1; + } + return false; + }); + + if (!breakpointAdded) { return { content: [ { @@ -269,10 +277,12 @@ export const getCallStack = async (params: { sessionName?: string }) => { stackFrames: stackTrace.stackFrames.map((frame: any) => ({ id: frame.id, name: frame.name, - source: frame.source ? { - name: frame.source.name, - path: frame.source.path, - } : undefined, + source: frame.source + ? { + name: frame.source.name, + path: frame.source.path, + } + : undefined, line: frame.line, column: frame.column, })), @@ -284,7 +294,7 @@ export const getCallStack = async (params: { sessionName?: string }) => { error: error instanceof Error ? error.message : String(error), }; } - }) + }), ); return { @@ -299,7 +309,7 @@ export const getCallStack = async (params: { sessionName?: string }) => { error: error instanceof Error ? error.message : String(error), }; } - }) + }), ); return { @@ -326,5 +336,160 @@ export const getCallStack = async (params: { sessionName?: string }) => { // Zod schema for validating get_call_stack parameters. export const getCallStackSchema = z.object({ - sessionName: z.string().optional().describe('The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.'), + sessionName: z + .string() + .optional() + .describe( + 'The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.', + ), +}); + +/** + * Resume execution of a debug session that has been paused (e.g., by a breakpoint). + * + * @param params - Object containing the sessionId of the debug session to resume. + */ +export const resumeDebugSession = async (params: { sessionId: string }) => { + const { sessionId } = params; + + // Find the session with the given ID + const session = activeSessions.find((s) => s.id === sessionId); + if (!session) { + return { + content: [ + { + type: 'text', + text: `No debug session found with ID '${sessionId}'.`, + }, + ], + isError: true, + }; + } + + try { + // Send the continue request to the debug adapter + await session.customRequest('continue', { threadId: 0 }); // 0 means all threads + + return { + content: [ + { + type: 'text', + text: `Resumed debug session '${session.name}'.`, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error resuming debug session: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating resume_debug_session parameters. +export const resumeDebugSessionSchema = z.object({ + sessionId: z.string().describe('The ID of the debug session to resume.'), +}); + +/** + * Get variables from a specific stack frame. + * + * @param params - Object containing sessionId, frameId, threadId, and optional filter to get variables from. + */ +export const getStackFrameVariables = async (params: { + sessionId: string; + frameId: number; + threadId: number; + filter?: string; +}) => { + const { sessionId, frameId, threadId, filter } = params; + + // Find the session with the given ID + const session = activeSessions.find((s) => s.id === sessionId); + if (!session) { + return { + content: [ + { + type: 'text', + text: `No debug session found with ID '${sessionId}'.`, + }, + ], + isError: true, + }; + } + + try { + // First, get the scopes for the stack frame + const scopes = await session.customRequest('scopes', { frameId }); + + // Then, get variables for each scope + const variablesByScope = await Promise.all( + scopes.scopes.map(async (scope: { name: string; variablesReference: number }) => { + if (scope.variablesReference === 0) { + return { + scopeName: scope.name, + variables: [], + }; + } + + const response = await session.customRequest('variables', { + variablesReference: scope.variablesReference, + }); + + // Apply filter if provided + let filteredVariables = response.variables; + if (filter) { + const filterRegex = new RegExp(filter, 'i'); // Case insensitive match + filteredVariables = response.variables.filter((variable: { name: string }) => + filterRegex.test(variable.name), + ); + } + + return { + scopeName: scope.name, + variables: filteredVariables, + }; + }), + ); + + return { + content: [ + { + type: 'json', + json: { + sessionId, + frameId, + threadId, + variablesByScope, + filter: filter || undefined, + }, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting variables: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating get_stack_frame_variables parameters. +export const getStackFrameVariablesSchema = z.object({ + sessionId: z.string().describe('The ID of the debug session.'), + frameId: z.number().describe('The ID of the stack frame to get variables from.'), + threadId: z.number().describe('The ID of the thread containing the stack frame.'), + filter: z.string().optional().describe('Optional filter pattern to match variable names.'), }); From e23fd8d3b95a24ef6c82d0f0f89c2f28417c8025 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Wed, 9 Apr 2025 20:00:54 -0500 Subject: [PATCH 3/5] Implement breakpoint management and event handling in MCP Server extension --- mcp-servers/mcp-server-vscode/.gitignore | 1 + .../mcp-server-vscode/.vscode/tasks.json | 59 ++- .../mcp-server-vscode/src/extension.ts | 207 +++++--- .../src/tools/debug_tools.d.ts | 151 ++++++ .../src/tools/debug_tools.ts | 479 ++++++++++++++++++ mcp-servers/mcp-server-vscode/vite.config.ts | 47 ++ 6 files changed, 844 insertions(+), 100 deletions(-) create mode 100644 mcp-servers/mcp-server-vscode/vite.config.ts diff --git a/mcp-servers/mcp-server-vscode/.gitignore b/mcp-servers/mcp-server-vscode/.gitignore index 61a68673..a70fa342 100644 --- a/mcp-servers/mcp-server-vscode/.gitignore +++ b/mcp-servers/mcp-server-vscode/.gitignore @@ -1,2 +1,3 @@ *.vsix .vscode-test/** +out/ diff --git a/mcp-servers/mcp-server-vscode/.vscode/tasks.json b/mcp-servers/mcp-server-vscode/.vscode/tasks.json index 1572e9f2..4b4a3dcf 100644 --- a/mcp-servers/mcp-server-vscode/.vscode/tasks.json +++ b/mcp-servers/mcp-server-vscode/.vscode/tasks.json @@ -1,26 +1,41 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "watch", - "type": "npm", - "script": "watch", - "isBackground": true, - "problemMatcher": { - "owner": "custom", - "fileLocation": "absolute", - "pattern": { - "regexp": "a^" + "version": "2.0.0", + "tasks": [ + { + "label": "vite-build", + "type": "shell", + "command": "npx vite build", + "isBackground": false, + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "clear": true, + "revealProblems": "onProblem" + }, + "problemMatcher": ["$vite"] }, - "background": { - "activeOnStart": true, - "beginsPattern": "^\\[webpack-cli\\] Compiler starting", - "endsPattern": "^webpack\\s+.*compiled successfully.*$" + { + "label": "watch", + "type": "npm", + "script": "watch", + "isBackground": true, + "problemMatcher": { + "owner": "custom", + "fileLocation": "absolute", + "pattern": { + "regexp": "a^" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^\\[webpack-cli\\] Compiler starting", + "endsPattern": "^webpack\\s+.*compiled successfully.*$" + } + }, + "presentation": { + "clear": true + } } - }, - "presentation": { - "clear": true - } - } - ] + ] } diff --git a/mcp-servers/mcp-server-vscode/src/extension.ts b/mcp-servers/mcp-server-vscode/src/extension.ts index 3f5a3c00..cd672775 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -9,12 +9,9 @@ import { z } from 'zod'; import packageJson from '../package.json'; import { codeCheckerTool } from './tools/code_checker'; import { - getCallStack, - getCallStackSchema, - getStackFrameVariables, - getStackFrameVariablesSchema, - listDebugSessions, - listDebugSessionsSchema, + listBreakpoints, + listBreakpointsSchema, + onBreakpointHit, resumeDebugSession, resumeDebugSessionSchema, setBreakpoint, @@ -107,7 +104,7 @@ export const activate = async (context: vscode.ExtensionContext) => { // 'search_symbol', // dedent` // Search for a symbol within the workspace. - // - Tries to resolve the definition via VSCode’s "Go to Definition". + // - Tries to resolve the definition via VSCode's "Go to Definition". // - If not found, searches the entire workspace for the text, similar to Ctrl+Shift+F. // `.trim(), // { @@ -130,37 +127,6 @@ export const activate = async (context: vscode.ExtensionContext) => { // }, // ); - // Register 'list_debug_sessions' tool - mcpServer.tool( - 'list_debug_sessions', - 'List all active debug sessions in the workspace.', - listDebugSessionsSchema.shape, // No parameters required - async () => { - const result = await listDebugSessions(); - return { - ...result, - content: result.content.map((item) => ({ type: 'text', text: JSON.stringify(item.json) })), - }; - }, - ); - - // Register 'start_debug_session' tool - mcpServer.tool( - 'start_debug_session', - 'Start a new debug session with the provided configuration.', - startDebugSessionSchema.shape, - async (params) => { - const result = await startDebugSession(params); - return { - ...result, - content: result.content.map((item) => ({ - ...item, - type: 'text' as const, - })), - }; - }, - ); - // Register 'set_breakpoint' tool mcpServer.tool( 'set_breakpoint', @@ -178,26 +144,6 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); - // Register 'get_call_stack' tool - mcpServer.tool( - 'get_call_stack', - 'Get the current call stack information for an active debug session.', - getCallStackSchema.shape, - async (params) => { - const result = await getCallStack(params); - return { - ...result, - content: result.content.map((item) => { - if ('json' in item) { - // Convert json content to text string - return { type: 'text' as const, text: JSON.stringify(item.json) }; - } - return { ...item, type: 'text' as const }; - }), - }; - }, - ); - // Register 'resume_debug_session' tool mcpServer.tool( 'resume_debug_session', @@ -215,26 +161,6 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); - // Register 'get_stack_frame_variables' tool - mcpServer.tool( - 'get_stack_frame_variables', - 'Get variables from a specific stack frame in a debug session.', - getStackFrameVariablesSchema.shape, - async (params) => { - const result = await getStackFrameVariables(params); - return { - ...result, - content: result.content.map((item) => { - if ('json' in item) { - // Convert json content to text string - return { type: 'text' as const, text: JSON.stringify(item.json) }; - } - return { ...item, type: 'text' as const }; - }), - }; - }, - ); - // Register 'stop_debug_session' tool mcpServer.tool( 'stop_debug_session', @@ -252,6 +178,42 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); + // Register 'wait_for_breakpoint_hit' tool + // mcpServer.tool( + // 'wait_for_breakpoint_hit', + // 'Wait for a breakpoint to be hit in a debug session. This tool blocks until a breakpoint is hit or the timeout expires.', + // { + // sessionId: z + // .string() + // .optional() + // .describe('The ID of the debug session to watch. If not provided, sessionName must be provided.'), + // sessionName: z + // .string() + // .optional() + // .describe('The name of the debug session to watch. If not provided, sessionId must be provided.'), + // timeout: z + // .number() + // .positive() + // .optional() + // .describe( + // 'Maximum time to wait for a breakpoint to be hit, in milliseconds. Default is 30000 (30 seconds).', + // ), + // }, + // async (params: { sessionId?: string; sessionName?: string; timeout?: number }) => { + // const result = await waitForBreakpointHit(params); + // return { + // ...result, + // content: result.content.map((item) => { + // if ('json' in item) { + // // Convert json content to text string + // return { type: 'text' as const, text: JSON.stringify(item.json) }; + // } + // return { type: 'text', text: item.text || '' }; + // }), + // }; + // }, + // ); + // Register 'restart_debug_session' tool mcpServer.tool( 'restart_debug_session', @@ -273,6 +235,26 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); + // Register 'list_breakpoints' tool + mcpServer.tool( + 'list_breakpoints', + 'Get a list of all currently set breakpoints in the workspace, with optional filtering by file path.', + listBreakpointsSchema.shape, + async (params) => { + const result = await listBreakpoints(params); + return { + ...result, + content: result.content.map((item) => { + if ('json' in item) { + // Convert json content to text string + return { type: 'text' as const, text: JSON.stringify(item.json) }; + } + return Object.assign(item, { type: 'text' as const }); + }), + }; + }, + ); + // Set up an Express app to handle SSE connections const app = express(); const mcpConfig = vscode.workspace.getConfiguration('mcpServer'); @@ -318,6 +300,75 @@ export const activate = async (context: vscode.ExtensionContext) => { // Create and start the HTTP server const server = http.createServer(app); + + // Track active breakpoint event subscriptions + const breakpointSubscriptions = new Map< + string, + { + sessionId?: string; + sessionName?: string; + } + >(); + + // Listen for breakpoint hit events and notify subscribers + const breakpointListener = onBreakpointHit((event) => { + outputChannel.appendLine(`Breakpoint hit event received in extension: ${JSON.stringify(event)}`); + + if (sseTransport && sseTransport.sessionId) { + // Only send notifications if we have an active SSE connection + outputChannel.appendLine(`SSE transport is active with sessionId: ${sseTransport.sessionId}`); + + // Check all subscriptions to see if any match this event + if (breakpointSubscriptions.size === 0) { + outputChannel.appendLine('No active breakpoint subscriptions found'); + } + + breakpointSubscriptions.forEach((filter, subscriptionId) => { + outputChannel.appendLine( + `Checking subscription ${subscriptionId} with filter: ${JSON.stringify(filter)}`, + ); + + // If the subscription has a filter, check if this event matches + const sessionIdMatch = !filter.sessionId || filter.sessionId === event.sessionId; + const sessionNameMatch = !filter.sessionName || filter.sessionName === event.sessionName; + + outputChannel.appendLine( + `Session ID match: ${sessionIdMatch}, Session name match: ${sessionNameMatch}`, + ); + + // Send notification if this event matches the subscription filter + if (sessionIdMatch && sessionNameMatch && sseTransport) { + // Construct notification message with correct type for jsonrpc + const notification = { + jsonrpc: '2.0' as const, + method: 'mcp/notification', + params: { + type: 'breakpoint-hit', + subscriptionId, + data: { + ...event, + timestamp: new Date().toISOString(), + }, + }, + }; + + // Send the notification through the SSE transport + try { + sseTransport.send(notification); + outputChannel.appendLine(`Sent breakpoint hit notification for subscription ${subscriptionId}`); + } catch (error) { + outputChannel.appendLine(`Error sending breakpoint notification: ${error}`); + } + } + }); + } else { + outputChannel.appendLine('No active SSE transport, cannot send breakpoint notification'); + } + }); + + // Dispose of the breakpoint listener when the extension is deactivated + context.subscriptions.push({ dispose: () => breakpointListener.dispose() }); + function startServer(port: number): void { server.listen(port, () => { outputChannel.appendLine(`MCP SSE Server running at http://127.0.0.1:${port}/sse`); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts index 5da80994..fd3a286e 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts @@ -1,5 +1,20 @@ import * as vscode from 'vscode'; import { z } from 'zod'; +interface BreakpointHitInfo { + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + exceptionInfo?: { + description: string; + details: string; + }; +} +export declare const breakpointEventEmitter: vscode.EventEmitter; +export declare const onBreakpointHit: vscode.Event; export declare const listDebugSessions: () => { content: { type: string; @@ -189,3 +204,139 @@ export declare const getStackFrameVariablesSchema: z.ZodObject<{ threadId: number; filter?: string | undefined; }>; +export declare const waitForBreakpointHit: (params: { + sessionId?: string; + sessionName?: string; + timeout?: number; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +} | { + content: { + type: string; + json: { + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + }; + }[]; + isError: boolean; +}>; +export declare const waitForBreakpointHitSchema: z.ZodEffects; + sessionName: z.ZodOptional; + timeout: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionName?: string | undefined; + sessionId?: string | undefined; + timeout?: number | undefined; +}, { + sessionName?: string | undefined; + sessionId?: string | undefined; + timeout?: number | undefined; +}>, { + sessionName?: string | undefined; + sessionId?: string | undefined; + timeout?: number | undefined; +}, { + sessionName?: string | undefined; + sessionId?: string | undefined; + timeout?: number | undefined; +}>; +export declare const subscribeToBreakpointEvents: (params: { + sessionId?: string; + sessionName?: string; +}) => Promise<{ + content: { + type: string; + json: { + subscriptionId: string; + message: string; + }; + }[]; + isError: boolean; + _meta: { + subscriptionId: string; + type: string; + filter: { + sessionId: string | undefined; + sessionName: string | undefined; + }; + }; +}>; +export declare const subscribeToBreakpointEventsSchema: z.ZodObject<{ + sessionId: z.ZodOptional; + sessionName: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionName?: string | undefined; + sessionId?: string | undefined; +}, { + sessionName?: string | undefined; + sessionId?: string | undefined; +}>; +export declare const listBreakpoints: (params?: { + filePath?: string; +}) => { + content: { + type: string; + json: { + breakpoints: ({ + id: string; + enabled: boolean; + condition: string | undefined; + hitCondition: string | undefined; + logMessage: string | undefined; + file: { + path: string; + name: string; + }; + location: { + line: number; + column: number; + }; + functionName?: undefined; + type?: undefined; + } | { + id: string; + enabled: boolean; + functionName: string; + condition: string | undefined; + hitCondition: string | undefined; + logMessage: string | undefined; + file?: undefined; + location?: undefined; + type?: undefined; + } | { + id: string; + enabled: boolean; + type: string; + condition?: undefined; + hitCondition?: undefined; + logMessage?: undefined; + file?: undefined; + location?: undefined; + functionName?: undefined; + })[]; + count: number; + filter: { + filePath: string; + } | undefined; + }; + }[]; + isError: boolean; +}; +export declare const listBreakpointsSchema: z.ZodObject<{ + filePath: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + filePath?: string | undefined; +}, { + filePath?: string | undefined; +}>; +export {}; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts index d5dff785..51e9a89b 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts @@ -2,12 +2,36 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { z } from 'zod'; +// Create an output channel for debugging +const outputChannel = vscode.window.createOutputChannel('Debug Tools'); + /** Maintain a list of active debug sessions. */ const activeSessions: vscode.DebugSession[] = []; +/** Store breakpoint hit information for notification */ +interface BreakpointHitInfo { + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + exceptionInfo?: { + description: string; + details: string; + }; +} + +/** Event emitter for breakpoint hit notifications */ +export const breakpointEventEmitter = new vscode.EventEmitter(); +export const onBreakpointHit = breakpointEventEmitter.event; + // Track new debug sessions as they start. vscode.debug.onDidStartDebugSession((session) => { activeSessions.push(session); + outputChannel.appendLine(`Debug session started: ${session.name} (ID: ${session.id})`); + outputChannel.appendLine(`Active sessions: ${activeSessions.length}`); }); // Remove debug sessions as they terminate. @@ -15,9 +39,183 @@ vscode.debug.onDidTerminateDebugSession((session) => { const index = activeSessions.indexOf(session); if (index >= 0) { activeSessions.splice(index, 1); + outputChannel.appendLine(`Debug session terminated: ${session.name} (ID: ${session.id})`); + outputChannel.appendLine(`Active sessions: ${activeSessions.length}`); } }); +vscode.debug.onDidChangeActiveDebugSession((session) => { + outputChannel.appendLine(`Active debug session changed: ${session ? session.name : 'None'}`); +}); +vscode.debug.registerDebugAdapterTrackerFactory('*', { + createDebugAdapterTracker: (session: vscode.DebugSession): vscode.ProviderResult => { + // Create a class that implements the DebugAdapterTracker interface + class DebugAdapterTrackerImpl implements vscode.DebugAdapterTracker { + onWillStartSession?(): void { + outputChannel.appendLine(`Debug session starting: ${session.name}`); + } + + onWillReceiveMessage?(message: any): void { + // Optional: Log messages being received by the debug adapter + outputChannel.appendLine(`Message received by debug adapter: ${JSON.stringify(message)}`); + } + + onDidSendMessage(message: any): void { + // Log all messages sent from the debug adapter to VS Code + if (message.type === 'event') { + const event = message; + // The 'stopped' event is fired when execution stops (e.g., at a breakpoint or exception) + if (event.event === 'stopped') { + const body = event.body; + // Process any stop event - including breakpoints, exceptions, and other stops + const validReasons = ['breakpoint', 'step', 'pause', 'exception', 'assertion', 'entry']; + + if (validReasons.includes(body.reason)) { + // Use existing getCallStack function to get thread and stack information + (async () => { + try { + // Collect exception details if this is an exception + let exceptionDetails = undefined; + if (body.reason === 'exception' && body.description) { + exceptionDetails = { + description: body.description || 'Unknown exception', + details: body.text || 'No additional details available', + }; + } + + // Get call stack information for the session + const callStackResult = await getCallStack({ sessionName: session.name }); + + if (callStackResult.isError) { + // If we couldn't get call stack, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + if (!('json' in callStackResult.content[0])) { + // If the content is not JSON, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + // Extract call stack data from the result + const callStackData = callStackResult.content[0].json?.callStacks[0]; + if (!('threads' in callStackData)) { + // If threads are not present, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + // If threads are present, find the one that matches the threadId + if (!Array.isArray(callStackData.threads)) { + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + // Find the thread that triggered the event + const threadData = callStackData.threads.find( + (t: any) => t.threadId === body.threadId, + ); + + if (!threadData || !threadData.stackFrames || threadData.stackFrames.length === 0) { + // If thread or stack frames not found, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + + // Get the top stack frame + const topFrame = threadData.stackFrames[0]; + + // Emit breakpoint/exception hit event with stack frame information + const eventData = { + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + frameId: topFrame.id, + filePath: topFrame.source?.path, + line: topFrame.line, + exceptionInfo: exceptionDetails, + }; + + outputChannel.appendLine(`Firing breakpoint event: ${JSON.stringify(eventData)}`); + breakpointEventEmitter.fire(eventData); + } catch (error) { + console.error('Error processing debug event:', error); + // Still emit event with basic info + const exceptionDetails = + body.reason === 'exception' + ? { + description: body.description || 'Unknown exception', + details: body.text || 'No details available', + } + : undefined; + + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + } + })(); + } + } + } + outputChannel.appendLine(`Message from debug adapter: ${JSON.stringify(message)}`); + } + + onWillSendMessage(message: any): void { + // Log all messages sent to the debug adapter + outputChannel.appendLine(`Message sent to debug adapter: ${JSON.stringify(message)}`); + } + + onDidReceiveMessage(message: any): void { + // Log all messages received from the debug adapter + outputChannel.appendLine(`Message received from debug adapter: ${JSON.stringify(message)}`); + } + + onError?(error: Error): void { + outputChannel.appendLine(`Debug adapter error: ${error.message}`); + } + + onExit?(code: number | undefined, signal: string | undefined): void { + outputChannel.appendLine(`Debug adapter exited: code=${code}, signal=${signal}`); + } + } + + return new DebugAdapterTrackerImpl(); + }, +}); +// Listen for breakpoint hit events + /** * List all active debug sessions in the workspace. * @@ -493,3 +691,284 @@ export const getStackFrameVariablesSchema = z.object({ threadId: z.number().describe('The ID of the thread containing the stack frame.'), filter: z.string().optional().describe('Optional filter pattern to match variable names.'), }); + +/** + * Wait for a breakpoint to be hit in a debug session. + * + * @param params - Object containing sessionId or sessionName to identify the debug session, and optional timeout. + */ +export const waitForBreakpointHit = async (params: { sessionId?: string; sessionName?: string; timeout?: number }) => { + const { sessionId, sessionName, timeout = 30000 } = params; // Default timeout: 30 seconds + + // Find the targeted debug session(s) + let targetSessions: vscode.DebugSession[] = []; + + if (sessionId) { + const session = activeSessions.find((s) => s.id === sessionId); + if (session) { + targetSessions = [session]; + } + } else if (sessionName) { + targetSessions = activeSessions.filter((s) => s.name === sessionName); + } else { + targetSessions = [...activeSessions]; // All active sessions if neither ID nor name provided + } + + if (targetSessions.length === 0) { + return { + content: [ + { + type: 'text', + text: `No matching debug sessions found.`, + }, + ], + isError: true, + }; + } + + try { + // Create a promise that resolves when a breakpoint is hit + const breakpointHitPromise = new Promise<{ + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + }>((resolve, reject) => { + // Listen for the 'stopped' event from the debug adapter + const sessionStoppedListener = vscode.debug.onDidReceiveDebugSessionCustomEvent((event) => { + // The 'stopped' event is fired when execution stops (e.g., at a breakpoint) + if (event.event === 'stopped' && targetSessions.some((s) => s.id === event.session.id)) { + const session = event.session; + const body = event.body; + + if (body.reason === 'breakpoint' || body.reason === 'step' || body.reason === 'pause') { + // Get the first stack frame to provide the frameId + (async () => { + try { + const threadsResponse = await Promise.resolve(session.customRequest('threads')); + const thread = threadsResponse.threads.find((t: any) => t.id === body.threadId); + + if (thread) { + try { + const stackTrace = await Promise.resolve( + session.customRequest('stackTrace', { threadId: body.threadId }), + ); + + const frameId = + stackTrace.stackFrames.length > 0 + ? stackTrace.stackFrames[0].id + : undefined; + const filePath = + stackTrace.stackFrames.length > 0 && stackTrace.stackFrames[0].source + ? stackTrace.stackFrames[0].source.path + : undefined; + const line = + stackTrace.stackFrames.length > 0 + ? stackTrace.stackFrames[0].line + : undefined; + + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + frameId, + filePath, + line, + }); + } catch (error) { + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + // Still resolve, but without frameId + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + }); + } + } else { + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + }); + } + } catch (error) { + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + // Still resolve with basic info + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + }); + } + })(); + } + } + }); + + // Set a timeout to prevent blocking indefinitely + setTimeout(() => { + sessionStoppedListener.dispose(); + reject(new Error(`Timed out waiting for breakpoint to be hit (${timeout}ms).`)); + }, timeout); + }); + + // Wait for the breakpoint to be hit or timeout + const result = await breakpointHitPromise; + + return { + content: [ + { + type: 'json', + json: result, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error waiting for breakpoint: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +/** + * Provides a way for MCP clients to subscribe to breakpoint hit events. + * This tool returns immediately with a subscription ID, and the MCP client + * will receive notifications when breakpoints are hit. + * + * @param params - Object containing an optional filter for the debug sessions to monitor. + */ +export const subscribeToBreakpointEvents = async (params: { sessionId?: string; sessionName?: string }) => { + const { sessionId, sessionName } = params; + + // Generate a unique subscription ID + const subscriptionId = `breakpoint-subscription-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + // Return immediately with subscription info + return { + content: [ + { + type: 'json', + json: { + subscriptionId, + message: + 'Subscribed to breakpoint events. You will receive notifications when breakpoints are hit.', + }, + }, + ], + isError: false, + // Special metadata to indicate this is a subscription + _meta: { + subscriptionId, + type: 'breakpoint-events', + filter: { sessionId, sessionName }, + }, + }; +}; + +// Zod schema for validating subscribe_to_breakpoint_events parameters. +export const subscribeToBreakpointEventsSchema = z.object({ + sessionId: z.string().optional().describe('Filter events to this specific debug session ID.'), + sessionName: z.string().optional().describe('Filter events to debug sessions with this name.'), +}); + +/** + * Get a list of all currently set breakpoints in the workspace. + * + * @param params - Optional object containing a file path filter. + */ +export const listBreakpoints = (params: { filePath?: string } = {}) => { + const { filePath } = params; + + // Get all breakpoints + const allBreakpoints = vscode.debug.breakpoints; + + // Filter breakpoints by file path if provided + const filteredBreakpoints = filePath + ? allBreakpoints.filter((bp) => { + if (bp instanceof vscode.SourceBreakpoint) { + return bp.location.uri.fsPath === filePath; + } + return false; + }) + : allBreakpoints; + + // Transform breakpoints into a more readable format + const breakpointData = filteredBreakpoints.map((bp) => { + if (bp instanceof vscode.SourceBreakpoint) { + const location = bp.location; + return { + id: bp.id, + enabled: bp.enabled, + condition: bp.condition, + hitCondition: bp.hitCondition, + logMessage: bp.logMessage, + file: { + path: location.uri.fsPath, + name: path.basename(location.uri.fsPath), + }, + location: { + line: location.range.start.line + 1, // Convert to 1-based for user display + column: location.range.start.character + 1, + }, + }; + } else if (bp instanceof vscode.FunctionBreakpoint) { + return { + id: bp.id, + enabled: bp.enabled, + functionName: bp.functionName, + condition: bp.condition, + hitCondition: bp.hitCondition, + logMessage: bp.logMessage, + }; + } else { + return { + id: bp.id, + enabled: bp.enabled, + type: 'unknown', + }; + } + }); + + return { + content: [ + { + type: 'json', + json: { + breakpoints: breakpointData, + count: breakpointData.length, + filter: filePath ? { filePath } : undefined, + }, + }, + ], + isError: false, + }; +}; + +// Zod schema for validating list_breakpoints parameters. +export const listBreakpointsSchema = z.object({ + filePath: z.string().optional().describe('Optional file path to filter breakpoints by file.'), +}); diff --git a/mcp-servers/mcp-server-vscode/vite.config.ts b/mcp-servers/mcp-server-vscode/vite.config.ts new file mode 100644 index 00000000..b9412f5a --- /dev/null +++ b/mcp-servers/mcp-server-vscode/vite.config.ts @@ -0,0 +1,47 @@ +import path from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + lib: { + entry: './src/extension.ts', + formats: ['cjs'], + fileName: () => 'extension.js', + }, + outDir: 'dist', + minify: false, + sourcemap: true, + rollupOptions: { + external: [ + 'vscode', + 'net', + 'http', + 'express', + 'node:crypto', + 'crypto', + /node:.*/, // Handle all node: protocol imports + '@modelcontextprotocol/sdk', + ], + output: { + format: 'cjs', + entryFileNames: '[name].js', + chunkFileNames: '[name].js', + interop: 'compat', + }, + }, + commonjsOptions: { + transformMixedEsModules: true, + include: [/node_modules\/@modelcontextprotocol\/sdk/, /\.(js|ts)$/], + }, + }, + optimizeDeps: { + include: ['@modelcontextprotocol/sdk'], + }, + resolve: { + alias: { + '@modelcontextprotocol/sdk': path.resolve(__dirname, 'node_modules/@modelcontextprotocol/sdk/dist/esm'), + 'node:crypto': 'crypto', + }, + extensions: ['.js', '.ts'], + }, +}); From 98a4882a3603255f782b10aa7788558cd73051d5 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Wed, 9 Apr 2025 20:25:48 -0500 Subject: [PATCH 4/5] feat(debug): Implement breakpoint management and session control - Added functionality to set and list breakpoints with appropriate schemas for validation. - Implemented event handling for breakpoint hits, including an event emitter and subscription mechanism. - Created utilities for managing debug sessions, including starting, stopping, and resuming sessions. - Introduced call stack retrieval and variable inspection capabilities for active debug sessions. - Enhanced error handling and user feedback for debugging operations. --- .../mcp-server-vscode/src/extension.ts | 19 +- .../src/tools/debug/breakpoints.d.ts | 79 ++ .../src/tools/debug/breakpoints.ts | 164 +++ .../src/tools/debug/common.d.ts | 43 + .../src/tools/debug/common.ts | 159 +++ .../src/tools/debug/events.d.ts | 74 ++ .../src/tools/debug/events.ts | 389 +++++++ .../src/tools/debug/index.d.ts | 6 + .../src/tools/debug/index.ts | 23 + .../src/tools/debug/inspection.d.ts | 50 + .../src/tools/debug/inspection.ts | 112 ++ .../src/tools/debug/session.d.ts | 97 ++ .../src/tools/debug/session.ts | 242 +++++ .../src/tools/debug_tools.d.ts | 343 +----- .../src/tools/debug_tools.ts | 976 +----------------- 15 files changed, 1459 insertions(+), 1317 deletions(-) create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.d.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/common.d.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/common.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/events.d.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/events.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/index.d.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/index.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/inspection.d.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/session.d.ts create mode 100644 mcp-servers/mcp-server-vscode/src/tools/debug/session.ts diff --git a/mcp-servers/mcp-server-vscode/src/extension.ts b/mcp-servers/mcp-server-vscode/src/extension.ts index cd672775..2347a978 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -20,7 +20,7 @@ import { startDebugSessionSchema, stopDebugSession, stopDebugSessionSchema, -} from './tools/debug_tools'; +} from './tools/debug'; import { focusEditorTool } from './tools/focus_editor'; import { resolvePort } from './utils/port'; @@ -214,6 +214,23 @@ export const activate = async (context: vscode.ExtensionContext) => { // }, // ); + // Register 'start_debug_session' tool + mcpServer.tool( + 'start_debug_session', + 'Start a new debug session using the provided configuration. Can optionally wait for the session to stop at a breakpoint.', + startDebugSessionSchema.shape, + async (params) => { + const result = await startDebugSession(params); + return { + ...result, + content: result.content.map((item) => ({ + ...item, + type: 'text' as const, + })), + }; + }, + ); + // Register 'restart_debug_session' tool mcpServer.tool( 'restart_debug_session', diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.d.ts new file mode 100644 index 00000000..de4cdf08 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.d.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; +export declare const setBreakpoint: (params: { + filePath: string; + line: number; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +}>; +export declare const setBreakpointSchema: z.ZodObject<{ + filePath: z.ZodString; + line: z.ZodNumber; +}, "strip", z.ZodTypeAny, { + filePath: string; + line: number; +}, { + filePath: string; + line: number; +}>; +export declare const listBreakpoints: (params?: { + filePath?: string; +}) => { + content: { + type: string; + json: { + breakpoints: ({ + id: string; + enabled: boolean; + condition: string | undefined; + hitCondition: string | undefined; + logMessage: string | undefined; + file: { + path: string; + name: string; + }; + location: { + line: number; + column: number; + }; + functionName?: undefined; + type?: undefined; + } | { + id: string; + enabled: boolean; + functionName: string; + condition: string | undefined; + hitCondition: string | undefined; + logMessage: string | undefined; + file?: undefined; + location?: undefined; + type?: undefined; + } | { + id: string; + enabled: boolean; + type: string; + condition?: undefined; + hitCondition?: undefined; + logMessage?: undefined; + file?: undefined; + location?: undefined; + functionName?: undefined; + })[]; + count: number; + filter: { + filePath: string; + } | undefined; + }; + }[]; + isError: boolean; +}; +export declare const listBreakpointsSchema: z.ZodObject<{ + filePath: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + filePath?: string | undefined; +}, { + filePath?: string | undefined; +}>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts new file mode 100644 index 00000000..643a8322 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts @@ -0,0 +1,164 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; +import { z } from 'zod'; + +/** + * Set a breakpoint at a specific line in a file. + * + * @param params - Object containing filePath and line number for the breakpoint. + */ +export const setBreakpoint = async (params: { filePath: string; line: number }) => { + const { filePath, line } = params; + + try { + // Create a URI from the file path + const fileUri = vscode.Uri.file(filePath); + + // Check if the file exists + try { + await vscode.workspace.fs.stat(fileUri); + } catch (error) { + return { + content: [ + { + type: 'text', + text: `File not found: ${filePath}`, + }, + ], + isError: true, + }; + } + + // Create a new breakpoint + const breakpoint = new vscode.SourceBreakpoint(new vscode.Location(fileUri, new vscode.Position(line - 1, 0))); + + // Add the breakpoint - note that addBreakpoints returns void, not an array + vscode.debug.addBreakpoints([breakpoint]); + + // Check if the breakpoint was successfully added by verifying it exists in VS Code's breakpoints + const breakpoints = vscode.debug.breakpoints; + const breakpointAdded = breakpoints.some((bp) => { + if (bp instanceof vscode.SourceBreakpoint) { + const loc = bp.location; + return loc.uri.fsPath === fileUri.fsPath && loc.range.start.line === line - 1; + } + return false; + }); + + if (!breakpointAdded) { + return { + content: [ + { + type: 'text', + text: `Failed to set breakpoint at line ${line} in ${path.basename(filePath)}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: 'text', + text: `Breakpoint set at line ${line} in ${path.basename(filePath)}`, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error setting breakpoint: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating set_breakpoint parameters. +export const setBreakpointSchema = z.object({ + filePath: z.string().describe('The absolute path to the file where the breakpoint should be set.'), + line: z.number().int().min(1).describe('The line number where the breakpoint should be set (1-based).'), +}); + +/** + * Get a list of all currently set breakpoints in the workspace. + * + * @param params - Optional object containing a file path filter. + */ +export const listBreakpoints = (params: { filePath?: string } = {}) => { + const { filePath } = params; + + // Get all breakpoints + const allBreakpoints = vscode.debug.breakpoints; + + // Filter breakpoints by file path if provided + const filteredBreakpoints = filePath + ? allBreakpoints.filter((bp) => { + if (bp instanceof vscode.SourceBreakpoint) { + return bp.location.uri.fsPath === filePath; + } + return false; + }) + : allBreakpoints; + + // Transform breakpoints into a more readable format + const breakpointData = filteredBreakpoints.map((bp) => { + if (bp instanceof vscode.SourceBreakpoint) { + const location = bp.location; + return { + id: bp.id, + enabled: bp.enabled, + condition: bp.condition, + hitCondition: bp.hitCondition, + logMessage: bp.logMessage, + file: { + path: location.uri.fsPath, + name: path.basename(location.uri.fsPath), + }, + location: { + line: location.range.start.line + 1, // Convert to 1-based for user display + column: location.range.start.character + 1, + }, + }; + } else if (bp instanceof vscode.FunctionBreakpoint) { + return { + id: bp.id, + enabled: bp.enabled, + functionName: bp.functionName, + condition: bp.condition, + hitCondition: bp.hitCondition, + logMessage: bp.logMessage, + }; + } else { + return { + id: bp.id, + enabled: bp.enabled, + type: 'unknown', + }; + } + }); + + return { + content: [ + { + type: 'json', + json: { + breakpoints: breakpointData, + count: breakpointData.length, + filter: filePath ? { filePath } : undefined, + }, + }, + ], + isError: false, + }; +}; + +// Zod schema for validating list_breakpoints parameters. +export const listBreakpointsSchema = z.object({ + filePath: z.string().optional().describe('Optional file path to filter breakpoints by file.'), +}); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/common.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/common.d.ts new file mode 100644 index 00000000..a443891f --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/common.d.ts @@ -0,0 +1,43 @@ +import * as vscode from 'vscode'; +export declare const outputChannel: vscode.OutputChannel; +export declare const activeSessions: vscode.DebugSession[]; +export interface BreakpointHitInfo { + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + exceptionInfo?: { + description: string; + details: string; + }; +} +export declare const getCallStack: (params: { + sessionName?: string; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +} | { + content: { + type: string; + json: { + callStacks: ({ + sessionId: string; + sessionName: string; + threads: any[]; + error?: undefined; + } | { + sessionId: string; + sessionName: string; + error: string; + threads?: undefined; + })[]; + }; + }[]; + isError: boolean; +}>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts new file mode 100644 index 00000000..24f244b8 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts @@ -0,0 +1,159 @@ +import * as vscode from 'vscode'; + +// Create an output channel for debugging +export const outputChannel = vscode.window.createOutputChannel('Debug Tools'); + +/** Maintain a list of active debug sessions. */ +export const activeSessions: vscode.DebugSession[] = []; + +/** Store breakpoint hit information for notification */ +export interface BreakpointHitInfo { + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + exceptionInfo?: { + description: string; + details: string; + }; +} + +/** + * Get the current call stack information for an active debug session. + * + * @param params - Object containing the sessionName to get call stack for. + */ +export const getCallStack = async (params: { sessionName?: string }) => { + const { sessionName } = params; + + // Get all active debug sessions or filter by name if provided + let sessions = activeSessions; + if (sessionName) { + sessions = activeSessions.filter((session) => session.name === sessionName); + if (sessions.length === 0) { + return { + content: [ + { + type: 'text', + text: `No debug session found with name '${sessionName}'.`, + }, + ], + isError: true, + }; + } + } + + if (sessions.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No active debug sessions found.', + }, + ], + isError: true, + }; + } + + try { + // Get call stack information for each session + const callStacks = await Promise.all( + sessions.map(async (session) => { + try { + // Get all threads for the session + const threads = await session.customRequest('threads'); + + // Get stack traces for each thread + const stackTraces = await Promise.all( + threads.threads.map(async (thread: { id: number; name: string }) => { + try { + const stackTrace = await session.customRequest('stackTrace', { + threadId: thread.id, + }); + + return { + threadId: thread.id, + threadName: thread.name, + stackFrames: stackTrace.stackFrames.map((frame: any) => ({ + id: frame.id, + name: frame.name, + source: frame.source + ? { + name: frame.source.name, + path: frame.source.path, + } + : undefined, + line: frame.line, + column: frame.column, + })), + }; + } catch (error) { + return { + threadId: thread.id, + threadName: thread.name, + error: error instanceof Error ? error.message : String(error), + }; + } + }), + ); + + return { + sessionId: session.id, + sessionName: session.name, + threads: stackTraces, + }; + } catch (error) { + return { + sessionId: session.id, + sessionName: session.name, + error: error instanceof Error ? error.message : String(error), + }; + } + }), + ); + + return { + content: [ + { + type: 'json', + json: { callStacks }, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting call stack: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Track new debug sessions as they start. +vscode.debug.onDidStartDebugSession((session) => { + activeSessions.push(session); + outputChannel.appendLine(`Debug session started: ${session.name} (ID: ${session.id})`); + outputChannel.appendLine(`Active sessions: ${activeSessions.length}`); +}); + +// Remove debug sessions as they terminate. +vscode.debug.onDidTerminateDebugSession((session) => { + const index = activeSessions.indexOf(session); + if (index >= 0) { + activeSessions.splice(index, 1); + outputChannel.appendLine(`Debug session terminated: ${session.name} (ID: ${session.id})`); + outputChannel.appendLine(`Active sessions: ${activeSessions.length}`); + } +}); + +vscode.debug.onDidChangeActiveDebugSession((session) => { + outputChannel.appendLine(`Active debug session changed: ${session ? session.name : 'None'}`); +}); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/events.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/events.d.ts new file mode 100644 index 00000000..670e2245 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/events.d.ts @@ -0,0 +1,74 @@ +import * as vscode from 'vscode'; +import { z } from 'zod'; +import { BreakpointHitInfo } from './common'; +export declare const breakpointEventEmitter: vscode.EventEmitter; +export declare const onBreakpointHit: vscode.Event; +export declare const waitForBreakpointHit: (params: { + sessionId?: string; + sessionName?: string; + timeout?: number; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +} | { + content: { + type: string; + json: { + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + }; + }[]; + isError: boolean; +}>; +export declare const subscribeToBreakpointEvents: (params: { + sessionId?: string; + sessionName?: string; +}) => Promise<{ + content: { + type: string; + json: { + subscriptionId: string; + message: string; + }; + }[]; + isError: boolean; + _meta: { + subscriptionId: string; + type: string; + filter: { + sessionId: string | undefined; + sessionName: string | undefined; + }; + }; +}>; +export declare const subscribeToBreakpointEventsSchema: z.ZodObject<{ + sessionId: z.ZodOptional; + sessionName: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionId?: string | undefined; + sessionName?: string | undefined; +}, { + sessionId?: string | undefined; + sessionName?: string | undefined; +}>; +export declare const waitForBreakpointHitSchema: z.ZodObject<{ + sessionId: z.ZodOptional; + sessionName: z.ZodOptional; + timeout: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionId?: string | undefined; + sessionName?: string | undefined; + timeout?: number | undefined; +}, { + sessionId?: string | undefined; + sessionName?: string | undefined; + timeout?: number | undefined; +}>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts new file mode 100644 index 00000000..8cded88a --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts @@ -0,0 +1,389 @@ +import * as vscode from 'vscode'; +import { z } from 'zod'; +import { activeSessions, BreakpointHitInfo, getCallStack, outputChannel } from './common'; + +/** Event emitter for breakpoint hit notifications */ +export const breakpointEventEmitter = new vscode.EventEmitter(); +export const onBreakpointHit = breakpointEventEmitter.event; + +// Register debug adapter tracker to monitor debug events +vscode.debug.registerDebugAdapterTrackerFactory('*', { + createDebugAdapterTracker: (session: vscode.DebugSession): vscode.ProviderResult => { + // Create a class that implements the DebugAdapterTracker interface + class DebugAdapterTrackerImpl implements vscode.DebugAdapterTracker { + onWillStartSession?(): void { + outputChannel.appendLine(`Debug session starting: ${session.name}`); + } + + onWillReceiveMessage?(message: any): void { + // Optional: Log messages being received by the debug adapter + outputChannel.appendLine(`Message received by debug adapter: ${JSON.stringify(message)}`); + } + + onDidSendMessage(message: any): void { + // Log all messages sent from the debug adapter to VS Code + if (message.type === 'event') { + const event = message; + // The 'stopped' event is fired when execution stops (e.g., at a breakpoint or exception) + if (event.event === 'stopped') { + const body = event.body; + // Process any stop event - including breakpoints, exceptions, and other stops + const validReasons = ['breakpoint', 'step', 'pause', 'exception', 'assertion', 'entry']; + + if (validReasons.includes(body.reason)) { + // Use existing getCallStack function to get thread and stack information + (async () => { + try { + // Collect exception details if this is an exception + let exceptionDetails = undefined; + if (body.reason === 'exception' && body.description) { + exceptionDetails = { + description: body.description || 'Unknown exception', + details: body.text || 'No additional details available', + }; + } + + // Get call stack information for the session + const callStackResult = await getCallStack({ sessionName: session.name }); + + if (callStackResult.isError) { + // If we couldn't get call stack, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + if (!('json' in callStackResult.content[0])) { + // If the content is not JSON, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + // Extract call stack data from the result + const callStackData = callStackResult.content[0].json?.callStacks[0]; + if (!('threads' in callStackData)) { + // If threads are not present, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + // If threads are present, find the one that matches the threadId + if (!Array.isArray(callStackData.threads)) { + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + // Find the thread that triggered the event + const threadData = callStackData.threads.find( + (t: any) => t.threadId === body.threadId, + ); + + if (!threadData || !threadData.stackFrames || threadData.stackFrames.length === 0) { + // If thread or stack frames not found, emit basic event + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + return; + } + + // Get the top stack frame + const topFrame = threadData.stackFrames[0]; + + // Emit breakpoint/exception hit event with stack frame information + const eventData = { + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + frameId: topFrame.id, + filePath: topFrame.source?.path, + line: topFrame.line, + exceptionInfo: exceptionDetails, + }; + + outputChannel.appendLine(`Firing breakpoint event: ${JSON.stringify(eventData)}`); + breakpointEventEmitter.fire(eventData); + } catch (error) { + console.error('Error processing debug event:', error); + // Still emit event with basic info + const exceptionDetails = + body.reason === 'exception' + ? { + description: body.description || 'Unknown exception', + details: body.text || 'No details available', + } + : undefined; + + breakpointEventEmitter.fire({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + exceptionInfo: exceptionDetails, + }); + } + })(); + } + } + } + outputChannel.appendLine(`Message from debug adapter: ${JSON.stringify(message)}`); + } + + onWillSendMessage(message: any): void { + // Log all messages sent to the debug adapter + outputChannel.appendLine(`Message sent to debug adapter: ${JSON.stringify(message)}`); + } + + onDidReceiveMessage(message: any): void { + // Log all messages received from the debug adapter + outputChannel.appendLine(`Message received from debug adapter: ${JSON.stringify(message)}`); + } + + onError?(error: Error): void { + outputChannel.appendLine(`Debug adapter error: ${error.message}`); + } + + onExit?(code: number | undefined, signal: string | undefined): void { + outputChannel.appendLine(`Debug adapter exited: code=${code}, signal=${signal}`); + } + } + + return new DebugAdapterTrackerImpl(); + }, +}); + +/** + * Wait for a breakpoint to be hit in a debug session. + * + * @param params - Object containing sessionId or sessionName to identify the debug session, and optional timeout. + */ +export const waitForBreakpointHit = async (params: { sessionId?: string; sessionName?: string; timeout?: number }) => { + const { sessionId, sessionName, timeout = 30000 } = params; // Default timeout: 30 seconds + + // Find the targeted debug session(s) + let targetSessions: vscode.DebugSession[] = []; + + if (sessionId) { + const session = activeSessions.find((s) => s.id === sessionId); + if (session) { + targetSessions = [session]; + } + } else if (sessionName) { + targetSessions = activeSessions.filter((s) => s.name === sessionName); + } else { + targetSessions = [...activeSessions]; // All active sessions if neither ID nor name provided + } + + if (targetSessions.length === 0) { + return { + content: [ + { + type: 'text', + text: `No matching debug sessions found.`, + }, + ], + isError: true, + }; + } + + try { + // Create a promise that resolves when a breakpoint is hit + const breakpointHitPromise = new Promise<{ + sessionId: string; + sessionName: string; + threadId: number; + reason: string; + frameId?: number; + filePath?: string; + line?: number; + }>((resolve, reject) => { + // Listen for the 'stopped' event from the debug adapter + const sessionStoppedListener = vscode.debug.onDidReceiveDebugSessionCustomEvent((event) => { + // The 'stopped' event is fired when execution stops (e.g., at a breakpoint) + if (event.event === 'stopped' && targetSessions.some((s) => s.id === event.session.id)) { + const session = event.session; + const body = event.body; + + if (body.reason === 'breakpoint' || body.reason === 'step' || body.reason === 'pause') { + // Get the first stack frame to provide the frameId + (async () => { + try { + const threadsResponse = await Promise.resolve(session.customRequest('threads')); + const thread = threadsResponse.threads.find((t: any) => t.id === body.threadId); + + if (thread) { + try { + const stackTrace = await Promise.resolve( + session.customRequest('stackTrace', { threadId: body.threadId }), + ); + + const frameId = + stackTrace.stackFrames.length > 0 + ? stackTrace.stackFrames[0].id + : undefined; + const filePath = + stackTrace.stackFrames.length > 0 && stackTrace.stackFrames[0].source + ? stackTrace.stackFrames[0].source.path + : undefined; + const line = + stackTrace.stackFrames.length > 0 + ? stackTrace.stackFrames[0].line + : undefined; + + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + frameId, + filePath, + line, + }); + } catch (error) { + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + // Still resolve, but without frameId + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + }); + } + } else { + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + }); + } + } catch (error) { + // Clean up the listener before resolving + sessionStoppedListener.dispose(); + + // Still resolve with basic info + resolve({ + sessionId: session.id, + sessionName: session.name, + threadId: body.threadId, + reason: body.reason, + }); + } + })(); + } + } + }); + + // Set a timeout to prevent blocking indefinitely + setTimeout(() => { + sessionStoppedListener.dispose(); + reject(new Error(`Timed out waiting for breakpoint to be hit (${timeout}ms).`)); + }, timeout); + }); + + // Wait for the breakpoint to be hit or timeout + const result = await breakpointHitPromise; + + return { + content: [ + { + type: 'json', + json: result, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error waiting for breakpoint: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +/** + * Provides a way for MCP clients to subscribe to breakpoint hit events. + * This tool returns immediately with a subscription ID, and the MCP client + * will receive notifications when breakpoints are hit. + * + * @param params - Object containing an optional filter for the debug sessions to monitor. + */ +export const subscribeToBreakpointEvents = async (params: { sessionId?: string; sessionName?: string }) => { + const { sessionId, sessionName } = params; + + // Generate a unique subscription ID + const subscriptionId = `breakpoint-subscription-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + // Return immediately with subscription info + return { + content: [ + { + type: 'json', + json: { + subscriptionId, + message: + 'Subscribed to breakpoint events. You will receive notifications when breakpoints are hit.', + }, + }, + ], + isError: false, + // Special metadata to indicate this is a subscription + _meta: { + subscriptionId, + type: 'breakpoint-events', + filter: { sessionId, sessionName }, + }, + }; +}; + +// Zod schema for validating subscribe_to_breakpoint_events parameters. +export const subscribeToBreakpointEventsSchema = z.object({ + sessionId: z.string().optional().describe('Filter events to this specific debug session ID.'), + sessionName: z.string().optional().describe('Filter events to debug sessions with this name.'), +}); + +// Zod schema for validating wait_for_breakpoint_hit parameters. +export const waitForBreakpointHitSchema = z.object({ + sessionId: z.string().optional().describe('The ID of the debug session to wait for a breakpoint hit.'), + sessionName: z.string().optional().describe('The name of the debug session to wait for a breakpoint hit.'), + timeout: z + .number() + .optional() + .describe('Timeout in milliseconds to wait for a breakpoint hit. Default: 30000 (30 seconds).'), +}); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/index.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/index.d.ts new file mode 100644 index 00000000..bb9b6c5f --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/index.d.ts @@ -0,0 +1,6 @@ +export { listBreakpoints, listBreakpointsSchema, setBreakpoint, setBreakpointSchema } from './breakpoints'; +export { activeSessions, getCallStack } from './common'; +export type { BreakpointHitInfo } from './common'; +export { breakpointEventEmitter, onBreakpointHit, subscribeToBreakpointEvents, subscribeToBreakpointEventsSchema, waitForBreakpointHit, waitForBreakpointHitSchema, } from './events'; +export { getCallStackSchema, getStackFrameVariables, getStackFrameVariablesSchema } from './inspection'; +export { listDebugSessions, listDebugSessionsSchema, resumeDebugSession, resumeDebugSessionSchema, startDebugSession, startDebugSessionSchema, stopDebugSession, stopDebugSessionSchema, } from './session'; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts new file mode 100644 index 00000000..72f6c3ce --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts @@ -0,0 +1,23 @@ +// Re-export everything from the individual modules +export { listBreakpoints, listBreakpointsSchema, setBreakpoint, setBreakpointSchema } from './breakpoints'; +export { activeSessions, getCallStack } from './common'; +export type { BreakpointHitInfo } from './common'; +export { + breakpointEventEmitter, + onBreakpointHit, + subscribeToBreakpointEvents, + subscribeToBreakpointEventsSchema, + waitForBreakpointHit, + waitForBreakpointHitSchema, +} from './events'; +export { getCallStackSchema, getStackFrameVariables, getStackFrameVariablesSchema } from './inspection'; +export { + listDebugSessions, + listDebugSessionsSchema, + resumeDebugSession, + resumeDebugSessionSchema, + startDebugSession, + startDebugSessionSchema, + stopDebugSession, + stopDebugSessionSchema, +} from './session'; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.d.ts new file mode 100644 index 00000000..acd87a11 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.d.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { getCallStack } from './common'; +export { getCallStack }; +export declare const getCallStackSchema: z.ZodObject<{ + sessionName: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionName?: string | undefined; +}, { + sessionName?: string | undefined; +}>; +export declare const getStackFrameVariables: (params: { + sessionId: string; + frameId: number; + threadId: number; + filter?: string; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +} | { + content: { + type: string; + json: { + sessionId: string; + frameId: number; + threadId: number; + variablesByScope: any[]; + filter: string | undefined; + }; + }[]; + isError: boolean; +}>; +export declare const getStackFrameVariablesSchema: z.ZodObject<{ + sessionId: z.ZodString; + frameId: z.ZodNumber; + threadId: z.ZodNumber; + filter: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + sessionId: string; + threadId: number; + frameId: number; + filter?: string | undefined; +}, { + sessionId: string; + threadId: number; + frameId: number; + filter?: string | undefined; +}>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts new file mode 100644 index 00000000..31ab0aba --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts @@ -0,0 +1,112 @@ +import { z } from 'zod'; +import { activeSessions, getCallStack } from './common'; + +// Re-export getCallStack and its schema +export { getCallStack }; + +// Zod schema for validating get_call_stack parameters. +export const getCallStackSchema = z.object({ + sessionName: z + .string() + .optional() + .describe( + 'The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.', + ), +}); + +/** + * Get variables from a specific stack frame. + * + * @param params - Object containing sessionId, frameId, threadId, and optional filter to get variables from. + */ +export const getStackFrameVariables = async (params: { + sessionId: string; + frameId: number; + threadId: number; + filter?: string; +}) => { + const { sessionId, frameId, threadId, filter } = params; + + // Find the session with the given ID + const session = activeSessions.find((s) => s.id === sessionId); + if (!session) { + return { + content: [ + { + type: 'text', + text: `No debug session found with ID '${sessionId}'.`, + }, + ], + isError: true, + }; + } + + try { + // First, get the scopes for the stack frame + const scopes = await session.customRequest('scopes', { frameId }); + + // Then, get variables for each scope + const variablesByScope = await Promise.all( + scopes.scopes.map(async (scope: { name: string; variablesReference: number }) => { + if (scope.variablesReference === 0) { + return { + scopeName: scope.name, + variables: [], + }; + } + + const response = await session.customRequest('variables', { + variablesReference: scope.variablesReference, + }); + + // Apply filter if provided + let filteredVariables = response.variables; + if (filter) { + const filterRegex = new RegExp(filter, 'i'); // Case insensitive match + filteredVariables = response.variables.filter((variable: { name: string }) => + filterRegex.test(variable.name), + ); + } + + return { + scopeName: scope.name, + variables: filteredVariables, + }; + }), + ); + + return { + content: [ + { + type: 'json', + json: { + sessionId, + frameId, + threadId, + variablesByScope, + filter: filter || undefined, + }, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error getting variables: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating get_stack_frame_variables parameters. +export const getStackFrameVariablesSchema = z.object({ + sessionId: z.string().describe('The ID of the debug session.'), + frameId: z.number().describe('The ID of the stack frame to get variables from.'), + threadId: z.number().describe('The ID of the thread containing the stack frame.'), + filter: z.string().optional().describe('Optional filter pattern to match variable names.'), +}); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/session.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/session.d.ts new file mode 100644 index 00000000..682de0f2 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/session.d.ts @@ -0,0 +1,97 @@ +import * as vscode from 'vscode'; +import { z } from 'zod'; +export declare const listDebugSessions: () => { + content: { + type: string; + json: { + sessions: { + id: string; + name: string; + configuration: vscode.DebugConfiguration; + }[]; + }; + }[]; + isError: boolean; +}; +export declare const listDebugSessionsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>; +export declare const startDebugSession: (params: { + workspaceFolder: string; + configuration: { + type: string; + request: string; + name: string; + [key: string]: any; + }; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +}>; +export declare const startDebugSessionSchema: z.ZodObject<{ + workspaceFolder: z.ZodString; + configuration: z.ZodObject<{ + type: z.ZodString; + request: z.ZodString; + name: z.ZodString; + }, "passthrough", z.ZodTypeAny, z.objectOutputType<{ + type: z.ZodString; + request: z.ZodString; + name: z.ZodString; + }, z.ZodTypeAny, "passthrough">, z.objectInputType<{ + type: z.ZodString; + request: z.ZodString; + name: z.ZodString; + }, z.ZodTypeAny, "passthrough">>; +}, "strip", z.ZodTypeAny, { + workspaceFolder: string; + configuration: { + name: string; + type: string; + request: string; + } & { + [k: string]: unknown; + }; +}, { + workspaceFolder: string; + configuration: { + name: string; + type: string; + request: string; + } & { + [k: string]: unknown; + }; +}>; +export declare const stopDebugSession: (params: { + sessionName: string; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +}>; +export declare const stopDebugSessionSchema: z.ZodObject<{ + sessionName: z.ZodString; +}, "strip", z.ZodTypeAny, { + sessionName: string; +}, { + sessionName: string; +}>; +export declare const resumeDebugSession: (params: { + sessionId: string; +}) => Promise<{ + content: { + type: string; + text: string; + }[]; + isError: boolean; +}>; +export declare const resumeDebugSessionSchema: z.ZodObject<{ + sessionId: z.ZodString; +}, "strip", z.ZodTypeAny, { + sessionId: string; +}, { + sessionId: string; +}>; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts new file mode 100644 index 00000000..296add68 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts @@ -0,0 +1,242 @@ +import * as vscode from 'vscode'; +import { z } from 'zod'; +import { activeSessions } from './common'; + +/** + * List all active debug sessions in the workspace. + * + * Exposes debug session information, including each session's ID, name, and associated launch configuration. + */ +export const listDebugSessions = () => { + // Retrieve all active debug sessions using the activeSessions array. + const sessions = activeSessions.map((session: vscode.DebugSession) => ({ + id: session.id, + name: session.name, + configuration: session.configuration, + })); + + // Return session list + return { + content: [ + { + type: 'json', + json: { sessions }, + }, + ], + isError: false, + }; +}; + +// Zod schema for validating tool parameters (none for this tool). +export const listDebugSessionsSchema = z.object({}); + +/** + * Start a new debug session using the provided configuration. + * + * @param params - Object containing workspaceFolder, configuration details, and optional waitForStop flag. + */ +export const startDebugSession = async (params: { + workspaceFolder: string; + configuration: { type: string; request: string; name: string; [key: string]: any }; + waitForStop?: boolean; +}) => { + const { workspaceFolder, configuration, waitForStop = false } = params; + // Ensure that workspace folders exist and are accessible. + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new Error('No workspace folders are currently open.'); + } + + const folder = workspaceFolders.find((f) => f.uri?.fsPath === workspaceFolder); + if (!folder) { + throw new Error(`Workspace folder '${workspaceFolder}' not found.`); + } + + const success = await vscode.debug.startDebugging(folder, configuration); + + if (!success) { + throw new Error(`Failed to start debug session '${configuration.name}'.`); + } + + // If waitForStop is true, wait for the debug session to stop at a breakpoint or other stopping point + if (waitForStop) { + try { + // Import the waitForBreakpointHit function from events.ts + const { waitForBreakpointHit } = await import('./events'); + + // Wait for the debug session to stop + const stopResult = await waitForBreakpointHit({ + sessionName: configuration.name, + timeout: 30000, // Default timeout of 30 seconds + }); + + if (stopResult.isError) { + return { + content: [ + { type: 'text', text: `Debug session '${configuration.name}' started successfully.` }, + { + type: 'text', + text: `Warning: ${ + 'text' in stopResult.content[0] + ? stopResult.content[0].text + : 'Failed to wait for debug session to stop' + }`, + }, + ], + isError: false, + }; + } + + return { + content: [ + { + type: 'text', + text: `Debug session '${configuration.name}' started successfully and stopped at a breakpoint.`, + }, + { + type: 'text', + text: + 'json' in stopResult.content[0] + ? JSON.stringify(stopResult.content[0].json) + : 'Breakpoint hit, but no details available', + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { type: 'text', text: `Debug session '${configuration.name}' started successfully.` }, + { + type: 'text', + text: `Warning: Failed to wait for debug session to stop: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + isError: false, + }; + } + } + + return { + content: [{ type: 'text', text: `Debug session '${configuration.name}' started successfully.` }], + isError: false, + }; +}; + +// Zod schema for validating start_debug_session parameters. +export const startDebugSessionSchema = z.object({ + workspaceFolder: z.string().describe('The workspace folder where the debug session should start.'), + configuration: z + .object({ + type: z.string().describe("Type of the debugger (e.g., 'node', 'python', etc.)."), + request: z.string().describe("Type of debug request (e.g., 'launch' or 'attach')."), + name: z.string().describe('Name of the debug session.'), + }) + .passthrough() + .describe('The debug configuration object.'), + waitForStop: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, the tool will wait until a breakpoint is hit or the debugger otherwise stops before returning. Prevents the LLM from getting impatient waiting for the breakpoint to hit.', + ), +}); + +/** + * Stop debug sessions that match the provided session name. + * + * @param params - Object containing the sessionName to stop. + */ +export const stopDebugSession = async (params: { sessionName: string }) => { + const { sessionName } = params; + // Filter active sessions to find matching sessions. + const matchingSessions = activeSessions.filter((session: vscode.DebugSession) => session.name === sessionName); + + if (matchingSessions.length === 0) { + return { + content: [ + { + type: 'text', + text: `No debug session(s) found with name '${sessionName}'.`, + }, + ], + isError: true, + }; + } + + // Stop each matching debug session. + for (const session of matchingSessions) { + await vscode.debug.stopDebugging(session); + } + + return { + content: [ + { + type: 'text', + text: `Stopped debug session(s) with name '${sessionName}'.`, + }, + ], + isError: false, + }; +}; + +// Zod schema for validating stop_debug_session parameters. +export const stopDebugSessionSchema = z.object({ + sessionName: z.string().describe('The name of the debug session(s) to stop.'), +}); + +/** + * Resume execution of a debug session that has been paused (e.g., by a breakpoint). + * + * @param params - Object containing the sessionId of the debug session to resume. + */ +export const resumeDebugSession = async (params: { sessionId: string }) => { + const { sessionId } = params; + + // Find the session with the given ID + const session = activeSessions.find((s) => s.id === sessionId); + if (!session) { + return { + content: [ + { + type: 'text', + text: `No debug session found with ID '${sessionId}'.`, + }, + ], + isError: true, + }; + } + + try { + // Send the continue request to the debug adapter + await session.customRequest('continue', { threadId: 0 }); // 0 means all threads + + return { + content: [ + { + type: 'text', + text: `Resumed debug session '${session.name}'.`, + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error resuming debug session: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } +}; + +// Zod schema for validating resume_debug_session parameters. +export const resumeDebugSessionSchema = z.object({ + sessionId: z.string().describe('The ID of the debug session to resume.'), +}); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts index fd3a286e..84c5131a 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.d.ts @@ -1,342 +1 @@ -import * as vscode from 'vscode'; -import { z } from 'zod'; -interface BreakpointHitInfo { - sessionId: string; - sessionName: string; - threadId: number; - reason: string; - frameId?: number; - filePath?: string; - line?: number; - exceptionInfo?: { - description: string; - details: string; - }; -} -export declare const breakpointEventEmitter: vscode.EventEmitter; -export declare const onBreakpointHit: vscode.Event; -export declare const listDebugSessions: () => { - content: { - type: string; - json: { - sessions: { - id: string; - name: string; - configuration: vscode.DebugConfiguration; - }[]; - }; - }[]; - isError: boolean; -}; -export declare const listDebugSessionsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>; -export declare const startDebugSession: (params: { - workspaceFolder: string; - configuration: { - type: string; - request: string; - name: string; - [key: string]: any; - }; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -}>; -export declare const startDebugSessionSchema: z.ZodObject<{ - workspaceFolder: z.ZodString; - configuration: z.ZodObject<{ - type: z.ZodString; - request: z.ZodString; - name: z.ZodString; - }, "passthrough", z.ZodTypeAny, z.objectOutputType<{ - type: z.ZodString; - request: z.ZodString; - name: z.ZodString; - }, z.ZodTypeAny, "passthrough">, z.objectInputType<{ - type: z.ZodString; - request: z.ZodString; - name: z.ZodString; - }, z.ZodTypeAny, "passthrough">>; -}, "strip", z.ZodTypeAny, { - workspaceFolder: string; - configuration: { - name: string; - type: string; - request: string; - } & { - [k: string]: unknown; - }; -}, { - workspaceFolder: string; - configuration: { - name: string; - type: string; - request: string; - } & { - [k: string]: unknown; - }; -}>; -export declare const stopDebugSession: (params: { - sessionName: string; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -}>; -export declare const stopDebugSessionSchema: z.ZodObject<{ - sessionName: z.ZodString; -}, "strip", z.ZodTypeAny, { - sessionName: string; -}, { - sessionName: string; -}>; -export declare const setBreakpoint: (params: { - filePath: string; - line: number; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -}>; -export declare const setBreakpointSchema: z.ZodObject<{ - filePath: z.ZodString; - line: z.ZodNumber; -}, "strip", z.ZodTypeAny, { - filePath: string; - line: number; -}, { - filePath: string; - line: number; -}>; -export declare const getCallStack: (params: { - sessionName?: string; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -} | { - content: { - type: string; - json: { - callStacks: ({ - sessionId: string; - sessionName: string; - threads: any[]; - error?: undefined; - } | { - sessionId: string; - sessionName: string; - error: string; - threads?: undefined; - })[]; - }; - }[]; - isError: boolean; -}>; -export declare const getCallStackSchema: z.ZodObject<{ - sessionName: z.ZodOptional; -}, "strip", z.ZodTypeAny, { - sessionName?: string | undefined; -}, { - sessionName?: string | undefined; -}>; -export declare const resumeDebugSession: (params: { - sessionId: string; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -}>; -export declare const resumeDebugSessionSchema: z.ZodObject<{ - sessionId: z.ZodString; -}, "strip", z.ZodTypeAny, { - sessionId: string; -}, { - sessionId: string; -}>; -export declare const getStackFrameVariables: (params: { - sessionId: string; - frameId: number; - threadId: number; - filter?: string; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -} | { - content: { - type: string; - json: { - sessionId: string; - frameId: number; - threadId: number; - variablesByScope: any[]; - filter: string | undefined; - }; - }[]; - isError: boolean; -}>; -export declare const getStackFrameVariablesSchema: z.ZodObject<{ - sessionId: z.ZodString; - frameId: z.ZodNumber; - threadId: z.ZodNumber; - filter: z.ZodOptional; -}, "strip", z.ZodTypeAny, { - sessionId: string; - frameId: number; - threadId: number; - filter?: string | undefined; -}, { - sessionId: string; - frameId: number; - threadId: number; - filter?: string | undefined; -}>; -export declare const waitForBreakpointHit: (params: { - sessionId?: string; - sessionName?: string; - timeout?: number; -}) => Promise<{ - content: { - type: string; - text: string; - }[]; - isError: boolean; -} | { - content: { - type: string; - json: { - sessionId: string; - sessionName: string; - threadId: number; - reason: string; - frameId?: number; - filePath?: string; - line?: number; - }; - }[]; - isError: boolean; -}>; -export declare const waitForBreakpointHitSchema: z.ZodEffects; - sessionName: z.ZodOptional; - timeout: z.ZodOptional; -}, "strip", z.ZodTypeAny, { - sessionName?: string | undefined; - sessionId?: string | undefined; - timeout?: number | undefined; -}, { - sessionName?: string | undefined; - sessionId?: string | undefined; - timeout?: number | undefined; -}>, { - sessionName?: string | undefined; - sessionId?: string | undefined; - timeout?: number | undefined; -}, { - sessionName?: string | undefined; - sessionId?: string | undefined; - timeout?: number | undefined; -}>; -export declare const subscribeToBreakpointEvents: (params: { - sessionId?: string; - sessionName?: string; -}) => Promise<{ - content: { - type: string; - json: { - subscriptionId: string; - message: string; - }; - }[]; - isError: boolean; - _meta: { - subscriptionId: string; - type: string; - filter: { - sessionId: string | undefined; - sessionName: string | undefined; - }; - }; -}>; -export declare const subscribeToBreakpointEventsSchema: z.ZodObject<{ - sessionId: z.ZodOptional; - sessionName: z.ZodOptional; -}, "strip", z.ZodTypeAny, { - sessionName?: string | undefined; - sessionId?: string | undefined; -}, { - sessionName?: string | undefined; - sessionId?: string | undefined; -}>; -export declare const listBreakpoints: (params?: { - filePath?: string; -}) => { - content: { - type: string; - json: { - breakpoints: ({ - id: string; - enabled: boolean; - condition: string | undefined; - hitCondition: string | undefined; - logMessage: string | undefined; - file: { - path: string; - name: string; - }; - location: { - line: number; - column: number; - }; - functionName?: undefined; - type?: undefined; - } | { - id: string; - enabled: boolean; - functionName: string; - condition: string | undefined; - hitCondition: string | undefined; - logMessage: string | undefined; - file?: undefined; - location?: undefined; - type?: undefined; - } | { - id: string; - enabled: boolean; - type: string; - condition?: undefined; - hitCondition?: undefined; - logMessage?: undefined; - file?: undefined; - location?: undefined; - functionName?: undefined; - })[]; - count: number; - filter: { - filePath: string; - } | undefined; - }; - }[]; - isError: boolean; -}; -export declare const listBreakpointsSchema: z.ZodObject<{ - filePath: z.ZodOptional; -}, "strip", z.ZodTypeAny, { - filePath?: string | undefined; -}, { - filePath?: string | undefined; -}>; -export {}; +export * from './debug'; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts index 51e9a89b..11154c79 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts @@ -1,974 +1,2 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { z } from 'zod'; - -// Create an output channel for debugging -const outputChannel = vscode.window.createOutputChannel('Debug Tools'); - -/** Maintain a list of active debug sessions. */ -const activeSessions: vscode.DebugSession[] = []; - -/** Store breakpoint hit information for notification */ -interface BreakpointHitInfo { - sessionId: string; - sessionName: string; - threadId: number; - reason: string; - frameId?: number; - filePath?: string; - line?: number; - exceptionInfo?: { - description: string; - details: string; - }; -} - -/** Event emitter for breakpoint hit notifications */ -export const breakpointEventEmitter = new vscode.EventEmitter(); -export const onBreakpointHit = breakpointEventEmitter.event; - -// Track new debug sessions as they start. -vscode.debug.onDidStartDebugSession((session) => { - activeSessions.push(session); - outputChannel.appendLine(`Debug session started: ${session.name} (ID: ${session.id})`); - outputChannel.appendLine(`Active sessions: ${activeSessions.length}`); -}); - -// Remove debug sessions as they terminate. -vscode.debug.onDidTerminateDebugSession((session) => { - const index = activeSessions.indexOf(session); - if (index >= 0) { - activeSessions.splice(index, 1); - outputChannel.appendLine(`Debug session terminated: ${session.name} (ID: ${session.id})`); - outputChannel.appendLine(`Active sessions: ${activeSessions.length}`); - } -}); - -vscode.debug.onDidChangeActiveDebugSession((session) => { - outputChannel.appendLine(`Active debug session changed: ${session ? session.name : 'None'}`); -}); -vscode.debug.registerDebugAdapterTrackerFactory('*', { - createDebugAdapterTracker: (session: vscode.DebugSession): vscode.ProviderResult => { - // Create a class that implements the DebugAdapterTracker interface - class DebugAdapterTrackerImpl implements vscode.DebugAdapterTracker { - onWillStartSession?(): void { - outputChannel.appendLine(`Debug session starting: ${session.name}`); - } - - onWillReceiveMessage?(message: any): void { - // Optional: Log messages being received by the debug adapter - outputChannel.appendLine(`Message received by debug adapter: ${JSON.stringify(message)}`); - } - - onDidSendMessage(message: any): void { - // Log all messages sent from the debug adapter to VS Code - if (message.type === 'event') { - const event = message; - // The 'stopped' event is fired when execution stops (e.g., at a breakpoint or exception) - if (event.event === 'stopped') { - const body = event.body; - // Process any stop event - including breakpoints, exceptions, and other stops - const validReasons = ['breakpoint', 'step', 'pause', 'exception', 'assertion', 'entry']; - - if (validReasons.includes(body.reason)) { - // Use existing getCallStack function to get thread and stack information - (async () => { - try { - // Collect exception details if this is an exception - let exceptionDetails = undefined; - if (body.reason === 'exception' && body.description) { - exceptionDetails = { - description: body.description || 'Unknown exception', - details: body.text || 'No additional details available', - }; - } - - // Get call stack information for the session - const callStackResult = await getCallStack({ sessionName: session.name }); - - if (callStackResult.isError) { - // If we couldn't get call stack, emit basic event - breakpointEventEmitter.fire({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - exceptionInfo: exceptionDetails, - }); - return; - } - if (!('json' in callStackResult.content[0])) { - // If the content is not JSON, emit basic event - breakpointEventEmitter.fire({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - exceptionInfo: exceptionDetails, - }); - return; - } - // Extract call stack data from the result - const callStackData = callStackResult.content[0].json?.callStacks[0]; - if (!('threads' in callStackData)) { - // If threads are not present, emit basic event - breakpointEventEmitter.fire({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - exceptionInfo: exceptionDetails, - }); - return; - } - // If threads are present, find the one that matches the threadId - if (!Array.isArray(callStackData.threads)) { - breakpointEventEmitter.fire({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - exceptionInfo: exceptionDetails, - }); - return; - } - // Find the thread that triggered the event - const threadData = callStackData.threads.find( - (t: any) => t.threadId === body.threadId, - ); - - if (!threadData || !threadData.stackFrames || threadData.stackFrames.length === 0) { - // If thread or stack frames not found, emit basic event - breakpointEventEmitter.fire({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - exceptionInfo: exceptionDetails, - }); - return; - } - - // Get the top stack frame - const topFrame = threadData.stackFrames[0]; - - // Emit breakpoint/exception hit event with stack frame information - const eventData = { - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - frameId: topFrame.id, - filePath: topFrame.source?.path, - line: topFrame.line, - exceptionInfo: exceptionDetails, - }; - - outputChannel.appendLine(`Firing breakpoint event: ${JSON.stringify(eventData)}`); - breakpointEventEmitter.fire(eventData); - } catch (error) { - console.error('Error processing debug event:', error); - // Still emit event with basic info - const exceptionDetails = - body.reason === 'exception' - ? { - description: body.description || 'Unknown exception', - details: body.text || 'No details available', - } - : undefined; - - breakpointEventEmitter.fire({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - exceptionInfo: exceptionDetails, - }); - } - })(); - } - } - } - outputChannel.appendLine(`Message from debug adapter: ${JSON.stringify(message)}`); - } - - onWillSendMessage(message: any): void { - // Log all messages sent to the debug adapter - outputChannel.appendLine(`Message sent to debug adapter: ${JSON.stringify(message)}`); - } - - onDidReceiveMessage(message: any): void { - // Log all messages received from the debug adapter - outputChannel.appendLine(`Message received from debug adapter: ${JSON.stringify(message)}`); - } - - onError?(error: Error): void { - outputChannel.appendLine(`Debug adapter error: ${error.message}`); - } - - onExit?(code: number | undefined, signal: string | undefined): void { - outputChannel.appendLine(`Debug adapter exited: code=${code}, signal=${signal}`); - } - } - - return new DebugAdapterTrackerImpl(); - }, -}); -// Listen for breakpoint hit events - -/** - * List all active debug sessions in the workspace. - * - * Exposes debug session information, including each session's ID, name, and associated launch configuration. - */ -export const listDebugSessions = () => { - // Retrieve all active debug sessions using the activeSessions array. - const sessions = activeSessions.map((session: vscode.DebugSession) => ({ - id: session.id, - name: session.name, - configuration: session.configuration, - })); - - // Return session list - return { - content: [ - { - type: 'json', - json: { sessions }, - }, - ], - isError: false, - }; -}; - -// Zod schema for validating tool parameters (none for this tool). -export const listDebugSessionsSchema = z.object({}); - -/** - * Start a new debug session using the provided configuration. - * - * @param params - Object containing workspaceFolder and configuration details. - */ -export const startDebugSession = async (params: { - workspaceFolder: string; - configuration: { type: string; request: string; name: string; [key: string]: any }; -}) => { - const { workspaceFolder, configuration } = params; - // Ensure that workspace folders exist and are accessible. - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - throw new Error('No workspace folders are currently open.'); - } - - const folder = workspaceFolders.find((f) => f.uri?.fsPath === workspaceFolder); - if (!folder) { - throw new Error(`Workspace folder '${workspaceFolder}' not found.`); - } - - const success = await vscode.debug.startDebugging(folder, configuration); - - if (!success) { - throw new Error(`Failed to start debug session '${configuration.name}'.`); - } - - return { - content: [{ type: 'text', text: `Debug session '${configuration.name}' started successfully.` }], - isError: false, - }; -}; - -// Zod schema for validating start_debug_session parameters. -export const startDebugSessionSchema = z.object({ - workspaceFolder: z.string().describe('The workspace folder where the debug session should start.'), - configuration: z - .object({ - type: z.string().describe("Type of the debugger (e.g., 'node', 'python', etc.)."), - request: z.string().describe("Type of debug request (e.g., 'launch' or 'attach')."), - name: z.string().describe('Name of the debug session.'), - }) - .passthrough() - .describe('The debug configuration object.'), -}); - -/** - * Stop debug sessions that match the provided session name. - * - * @param params - Object containing the sessionName to stop. - */ -export const stopDebugSession = async (params: { sessionName: string }) => { - const { sessionName } = params; - // Filter active sessions to find matching sessions. - const matchingSessions = activeSessions.filter((session: vscode.DebugSession) => session.name === sessionName); - - if (matchingSessions.length === 0) { - return { - content: [ - { - type: 'text', - text: `No debug session(s) found with name '${sessionName}'.`, - }, - ], - isError: true, - }; - } - - // Stop each matching debug session. - for (const session of matchingSessions) { - await vscode.debug.stopDebugging(session); - } - - return { - content: [ - { - type: 'text', - text: `Stopped debug session(s) with name '${sessionName}'.`, - }, - ], - isError: false, - }; -}; - -// Zod schema for validating stop_debug_session parameters. -export const stopDebugSessionSchema = z.object({ - sessionName: z.string().describe('The name of the debug session(s) to stop.'), -}); - -/** - * Set a breakpoint at a specific line in a file. - * - * @param params - Object containing filePath and line number for the breakpoint. - */ -export const setBreakpoint = async (params: { filePath: string; line: number }) => { - const { filePath, line } = params; - - try { - // Create a URI from the file path - const fileUri = vscode.Uri.file(filePath); - - // Check if the file exists - try { - await vscode.workspace.fs.stat(fileUri); - } catch (error) { - return { - content: [ - { - type: 'text', - text: `File not found: ${filePath}`, - }, - ], - isError: true, - }; - } - - // Create a new breakpoint - const breakpoint = new vscode.SourceBreakpoint(new vscode.Location(fileUri, new vscode.Position(line - 1, 0))); - - // Add the breakpoint - note that addBreakpoints returns void, not an array - vscode.debug.addBreakpoints([breakpoint]); - - // Check if the breakpoint was successfully added by verifying it exists in VS Code's breakpoints - const breakpoints = vscode.debug.breakpoints; - const breakpointAdded = breakpoints.some((bp) => { - if (bp instanceof vscode.SourceBreakpoint) { - const loc = bp.location; - return loc.uri.fsPath === fileUri.fsPath && loc.range.start.line === line - 1; - } - return false; - }); - - if (!breakpointAdded) { - return { - content: [ - { - type: 'text', - text: `Failed to set breakpoint at line ${line} in ${path.basename(filePath)}`, - }, - ], - isError: true, - }; - } - - return { - content: [ - { - type: 'text', - text: `Breakpoint set at line ${line} in ${path.basename(filePath)}`, - }, - ], - isError: false, - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error setting breakpoint: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } -}; - -// Zod schema for validating set_breakpoint parameters. -export const setBreakpointSchema = z.object({ - filePath: z.string().describe('The absolute path to the file where the breakpoint should be set.'), - line: z.number().int().min(1).describe('The line number where the breakpoint should be set (1-based).'), -}); - -/** - * Get the current call stack information for an active debug session. - * - * @param params - Object containing the sessionName to get call stack for. - */ -export const getCallStack = async (params: { sessionName?: string }) => { - const { sessionName } = params; - - // Get all active debug sessions or filter by name if provided - let sessions = activeSessions; - if (sessionName) { - sessions = activeSessions.filter((session) => session.name === sessionName); - if (sessions.length === 0) { - return { - content: [ - { - type: 'text', - text: `No debug session found with name '${sessionName}'.`, - }, - ], - isError: true, - }; - } - } - - if (sessions.length === 0) { - return { - content: [ - { - type: 'text', - text: 'No active debug sessions found.', - }, - ], - isError: true, - }; - } - - try { - // Get call stack information for each session - const callStacks = await Promise.all( - sessions.map(async (session) => { - try { - // Get all threads for the session - const threads = await session.customRequest('threads'); - - // Get stack traces for each thread - const stackTraces = await Promise.all( - threads.threads.map(async (thread: { id: number; name: string }) => { - try { - const stackTrace = await session.customRequest('stackTrace', { - threadId: thread.id, - }); - - return { - threadId: thread.id, - threadName: thread.name, - stackFrames: stackTrace.stackFrames.map((frame: any) => ({ - id: frame.id, - name: frame.name, - source: frame.source - ? { - name: frame.source.name, - path: frame.source.path, - } - : undefined, - line: frame.line, - column: frame.column, - })), - }; - } catch (error) { - return { - threadId: thread.id, - threadName: thread.name, - error: error instanceof Error ? error.message : String(error), - }; - } - }), - ); - - return { - sessionId: session.id, - sessionName: session.name, - threads: stackTraces, - }; - } catch (error) { - return { - sessionId: session.id, - sessionName: session.name, - error: error instanceof Error ? error.message : String(error), - }; - } - }), - ); - - return { - content: [ - { - type: 'json', - json: { callStacks }, - }, - ], - isError: false, - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error getting call stack: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } -}; - -// Zod schema for validating get_call_stack parameters. -export const getCallStackSchema = z.object({ - sessionName: z - .string() - .optional() - .describe( - 'The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.', - ), -}); - -/** - * Resume execution of a debug session that has been paused (e.g., by a breakpoint). - * - * @param params - Object containing the sessionId of the debug session to resume. - */ -export const resumeDebugSession = async (params: { sessionId: string }) => { - const { sessionId } = params; - - // Find the session with the given ID - const session = activeSessions.find((s) => s.id === sessionId); - if (!session) { - return { - content: [ - { - type: 'text', - text: `No debug session found with ID '${sessionId}'.`, - }, - ], - isError: true, - }; - } - - try { - // Send the continue request to the debug adapter - await session.customRequest('continue', { threadId: 0 }); // 0 means all threads - - return { - content: [ - { - type: 'text', - text: `Resumed debug session '${session.name}'.`, - }, - ], - isError: false, - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error resuming debug session: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } -}; - -// Zod schema for validating resume_debug_session parameters. -export const resumeDebugSessionSchema = z.object({ - sessionId: z.string().describe('The ID of the debug session to resume.'), -}); - -/** - * Get variables from a specific stack frame. - * - * @param params - Object containing sessionId, frameId, threadId, and optional filter to get variables from. - */ -export const getStackFrameVariables = async (params: { - sessionId: string; - frameId: number; - threadId: number; - filter?: string; -}) => { - const { sessionId, frameId, threadId, filter } = params; - - // Find the session with the given ID - const session = activeSessions.find((s) => s.id === sessionId); - if (!session) { - return { - content: [ - { - type: 'text', - text: `No debug session found with ID '${sessionId}'.`, - }, - ], - isError: true, - }; - } - - try { - // First, get the scopes for the stack frame - const scopes = await session.customRequest('scopes', { frameId }); - - // Then, get variables for each scope - const variablesByScope = await Promise.all( - scopes.scopes.map(async (scope: { name: string; variablesReference: number }) => { - if (scope.variablesReference === 0) { - return { - scopeName: scope.name, - variables: [], - }; - } - - const response = await session.customRequest('variables', { - variablesReference: scope.variablesReference, - }); - - // Apply filter if provided - let filteredVariables = response.variables; - if (filter) { - const filterRegex = new RegExp(filter, 'i'); // Case insensitive match - filteredVariables = response.variables.filter((variable: { name: string }) => - filterRegex.test(variable.name), - ); - } - - return { - scopeName: scope.name, - variables: filteredVariables, - }; - }), - ); - - return { - content: [ - { - type: 'json', - json: { - sessionId, - frameId, - threadId, - variablesByScope, - filter: filter || undefined, - }, - }, - ], - isError: false, - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error getting variables: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } -}; - -// Zod schema for validating get_stack_frame_variables parameters. -export const getStackFrameVariablesSchema = z.object({ - sessionId: z.string().describe('The ID of the debug session.'), - frameId: z.number().describe('The ID of the stack frame to get variables from.'), - threadId: z.number().describe('The ID of the thread containing the stack frame.'), - filter: z.string().optional().describe('Optional filter pattern to match variable names.'), -}); - -/** - * Wait for a breakpoint to be hit in a debug session. - * - * @param params - Object containing sessionId or sessionName to identify the debug session, and optional timeout. - */ -export const waitForBreakpointHit = async (params: { sessionId?: string; sessionName?: string; timeout?: number }) => { - const { sessionId, sessionName, timeout = 30000 } = params; // Default timeout: 30 seconds - - // Find the targeted debug session(s) - let targetSessions: vscode.DebugSession[] = []; - - if (sessionId) { - const session = activeSessions.find((s) => s.id === sessionId); - if (session) { - targetSessions = [session]; - } - } else if (sessionName) { - targetSessions = activeSessions.filter((s) => s.name === sessionName); - } else { - targetSessions = [...activeSessions]; // All active sessions if neither ID nor name provided - } - - if (targetSessions.length === 0) { - return { - content: [ - { - type: 'text', - text: `No matching debug sessions found.`, - }, - ], - isError: true, - }; - } - - try { - // Create a promise that resolves when a breakpoint is hit - const breakpointHitPromise = new Promise<{ - sessionId: string; - sessionName: string; - threadId: number; - reason: string; - frameId?: number; - filePath?: string; - line?: number; - }>((resolve, reject) => { - // Listen for the 'stopped' event from the debug adapter - const sessionStoppedListener = vscode.debug.onDidReceiveDebugSessionCustomEvent((event) => { - // The 'stopped' event is fired when execution stops (e.g., at a breakpoint) - if (event.event === 'stopped' && targetSessions.some((s) => s.id === event.session.id)) { - const session = event.session; - const body = event.body; - - if (body.reason === 'breakpoint' || body.reason === 'step' || body.reason === 'pause') { - // Get the first stack frame to provide the frameId - (async () => { - try { - const threadsResponse = await Promise.resolve(session.customRequest('threads')); - const thread = threadsResponse.threads.find((t: any) => t.id === body.threadId); - - if (thread) { - try { - const stackTrace = await Promise.resolve( - session.customRequest('stackTrace', { threadId: body.threadId }), - ); - - const frameId = - stackTrace.stackFrames.length > 0 - ? stackTrace.stackFrames[0].id - : undefined; - const filePath = - stackTrace.stackFrames.length > 0 && stackTrace.stackFrames[0].source - ? stackTrace.stackFrames[0].source.path - : undefined; - const line = - stackTrace.stackFrames.length > 0 - ? stackTrace.stackFrames[0].line - : undefined; - - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - frameId, - filePath, - line, - }); - } catch (error) { - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - // Still resolve, but without frameId - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - }); - } - } else { - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - }); - } - } catch (error) { - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - // Still resolve with basic info - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - }); - } - })(); - } - } - }); - - // Set a timeout to prevent blocking indefinitely - setTimeout(() => { - sessionStoppedListener.dispose(); - reject(new Error(`Timed out waiting for breakpoint to be hit (${timeout}ms).`)); - }, timeout); - }); - - // Wait for the breakpoint to be hit or timeout - const result = await breakpointHitPromise; - - return { - content: [ - { - type: 'json', - json: result, - }, - ], - isError: false, - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error waiting for breakpoint: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; - } -}; - -/** - * Provides a way for MCP clients to subscribe to breakpoint hit events. - * This tool returns immediately with a subscription ID, and the MCP client - * will receive notifications when breakpoints are hit. - * - * @param params - Object containing an optional filter for the debug sessions to monitor. - */ -export const subscribeToBreakpointEvents = async (params: { sessionId?: string; sessionName?: string }) => { - const { sessionId, sessionName } = params; - - // Generate a unique subscription ID - const subscriptionId = `breakpoint-subscription-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - - // Return immediately with subscription info - return { - content: [ - { - type: 'json', - json: { - subscriptionId, - message: - 'Subscribed to breakpoint events. You will receive notifications when breakpoints are hit.', - }, - }, - ], - isError: false, - // Special metadata to indicate this is a subscription - _meta: { - subscriptionId, - type: 'breakpoint-events', - filter: { sessionId, sessionName }, - }, - }; -}; - -// Zod schema for validating subscribe_to_breakpoint_events parameters. -export const subscribeToBreakpointEventsSchema = z.object({ - sessionId: z.string().optional().describe('Filter events to this specific debug session ID.'), - sessionName: z.string().optional().describe('Filter events to debug sessions with this name.'), -}); - -/** - * Get a list of all currently set breakpoints in the workspace. - * - * @param params - Optional object containing a file path filter. - */ -export const listBreakpoints = (params: { filePath?: string } = {}) => { - const { filePath } = params; - - // Get all breakpoints - const allBreakpoints = vscode.debug.breakpoints; - - // Filter breakpoints by file path if provided - const filteredBreakpoints = filePath - ? allBreakpoints.filter((bp) => { - if (bp instanceof vscode.SourceBreakpoint) { - return bp.location.uri.fsPath === filePath; - } - return false; - }) - : allBreakpoints; - - // Transform breakpoints into a more readable format - const breakpointData = filteredBreakpoints.map((bp) => { - if (bp instanceof vscode.SourceBreakpoint) { - const location = bp.location; - return { - id: bp.id, - enabled: bp.enabled, - condition: bp.condition, - hitCondition: bp.hitCondition, - logMessage: bp.logMessage, - file: { - path: location.uri.fsPath, - name: path.basename(location.uri.fsPath), - }, - location: { - line: location.range.start.line + 1, // Convert to 1-based for user display - column: location.range.start.character + 1, - }, - }; - } else if (bp instanceof vscode.FunctionBreakpoint) { - return { - id: bp.id, - enabled: bp.enabled, - functionName: bp.functionName, - condition: bp.condition, - hitCondition: bp.hitCondition, - logMessage: bp.logMessage, - }; - } else { - return { - id: bp.id, - enabled: bp.enabled, - type: 'unknown', - }; - } - }); - - return { - content: [ - { - type: 'json', - json: { - breakpoints: breakpointData, - count: breakpointData.length, - filter: filePath ? { filePath } : undefined, - }, - }, - ], - isError: false, - }; -}; - -// Zod schema for validating list_breakpoints parameters. -export const listBreakpointsSchema = z.object({ - filePath: z.string().optional().describe('Optional file path to filter breakpoints by file.'), -}); +// Re-export everything from the debug directory +export * from './debug'; From d09b34c7b2dcdeff43623606fd1db39ba7762e13 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Wed, 9 Apr 2025 21:12:36 -0500 Subject: [PATCH 5/5] feat(debug): Refactor debug session handling and enhance variable inspection logging --- .../mcp-server-vscode/src/extension.ts | 36 --- .../src/tools/debug/breakpoints.ts | 2 +- .../src/tools/debug/common.ts | 11 +- .../src/tools/debug/events.ts | 112 ++------ .../src/tools/debug/index.ts | 4 +- .../src/tools/debug/inspection.ts | 121 +++++++-- .../src/tools/debug/session.ts | 251 +++++++++++++----- 7 files changed, 311 insertions(+), 226 deletions(-) diff --git a/mcp-servers/mcp-server-vscode/src/extension.ts b/mcp-servers/mcp-server-vscode/src/extension.ts index 2347a978..31696417 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -178,42 +178,6 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); - // Register 'wait_for_breakpoint_hit' tool - // mcpServer.tool( - // 'wait_for_breakpoint_hit', - // 'Wait for a breakpoint to be hit in a debug session. This tool blocks until a breakpoint is hit or the timeout expires.', - // { - // sessionId: z - // .string() - // .optional() - // .describe('The ID of the debug session to watch. If not provided, sessionName must be provided.'), - // sessionName: z - // .string() - // .optional() - // .describe('The name of the debug session to watch. If not provided, sessionId must be provided.'), - // timeout: z - // .number() - // .positive() - // .optional() - // .describe( - // 'Maximum time to wait for a breakpoint to be hit, in milliseconds. Default is 30000 (30 seconds).', - // ), - // }, - // async (params: { sessionId?: string; sessionName?: string; timeout?: number }) => { - // const result = await waitForBreakpointHit(params); - // return { - // ...result, - // content: result.content.map((item) => { - // if ('json' in item) { - // // Convert json content to text string - // return { type: 'text' as const, text: JSON.stringify(item.json) }; - // } - // return { type: 'text', text: item.text || '' }; - // }), - // }; - // }, - // ); - // Register 'start_debug_session' tool mcpServer.tool( 'start_debug_session', diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts index 643a8322..74ec4f8f 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts @@ -1,4 +1,4 @@ -import * as path from 'path'; +import * as path from 'node:path'; import * as vscode from 'vscode'; import { z } from 'zod'; diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts index 24f244b8..f82668cc 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import { z } from 'zod'; // Create an output channel for debugging export const outputChannel = vscode.window.createOutputChannel('Debug Tools'); @@ -136,7 +137,15 @@ export const getCallStack = async (params: { sessionName?: string }) => { }; } }; - +// Zod schema for validating get_call_stack parameters. +export const getCallStackSchema = z.object({ + sessionName: z + .string() + .optional() + .describe( + 'The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.', + ), +}); // Track new debug sessions as they start. vscode.debug.onDidStartDebugSession((session) => { activeSessions.push(session); diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts index 8cded88a..f9d6d075 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts @@ -201,7 +201,7 @@ export const waitForBreakpointHit = async (params: { sessionId?: string; session return { content: [ { - type: 'text', + type: 'text' as const, text: `No matching debug sessions found.`, }, ], @@ -211,103 +211,23 @@ export const waitForBreakpointHit = async (params: { sessionId?: string; session try { // Create a promise that resolves when a breakpoint is hit - const breakpointHitPromise = new Promise<{ - sessionId: string; - sessionName: string; - threadId: number; - reason: string; - frameId?: number; - filePath?: string; - line?: number; - }>((resolve, reject) => { - // Listen for the 'stopped' event from the debug adapter - const sessionStoppedListener = vscode.debug.onDidReceiveDebugSessionCustomEvent((event) => { - // The 'stopped' event is fired when execution stops (e.g., at a breakpoint) - if (event.event === 'stopped' && targetSessions.some((s) => s.id === event.session.id)) { - const session = event.session; - const body = event.body; - - if (body.reason === 'breakpoint' || body.reason === 'step' || body.reason === 'pause') { - // Get the first stack frame to provide the frameId - (async () => { - try { - const threadsResponse = await Promise.resolve(session.customRequest('threads')); - const thread = threadsResponse.threads.find((t: any) => t.id === body.threadId); - - if (thread) { - try { - const stackTrace = await Promise.resolve( - session.customRequest('stackTrace', { threadId: body.threadId }), - ); - - const frameId = - stackTrace.stackFrames.length > 0 - ? stackTrace.stackFrames[0].id - : undefined; - const filePath = - stackTrace.stackFrames.length > 0 && stackTrace.stackFrames[0].source - ? stackTrace.stackFrames[0].source.path - : undefined; - const line = - stackTrace.stackFrames.length > 0 - ? stackTrace.stackFrames[0].line - : undefined; - - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - frameId, - filePath, - line, - }); - } catch (error) { - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - // Still resolve, but without frameId - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - }); - } - } else { - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - }); - } - } catch (error) { - // Clean up the listener before resolving - sessionStoppedListener.dispose(); - - // Still resolve with basic info - resolve({ - sessionId: session.id, - sessionName: session.name, - threadId: body.threadId, - reason: body.reason, - }); - } - })(); - } + const breakpointHitPromise = new Promise((resolve, reject) => { + // Use the breakpointEventEmitter which is already wired up to the debug adapter tracker + const listener = onBreakpointHit((event) => { + // Check if this event is for one of our target sessions + if (targetSessions.some((s) => s.id === event.sessionId)) { + // If it is, clean up the listener and resolve the promise + listener.dispose(); + resolve(event); + outputChannel.appendLine( + `Breakpoint hit detected for waitForBreakpointHit: ${JSON.stringify(event)}`, + ); } }); // Set a timeout to prevent blocking indefinitely setTimeout(() => { - sessionStoppedListener.dispose(); + listener.dispose(); reject(new Error(`Timed out waiting for breakpoint to be hit (${timeout}ms).`)); }, timeout); }); @@ -318,8 +238,8 @@ export const waitForBreakpointHit = async (params: { sessionId?: string; session return { content: [ { - type: 'json', - json: result, + type: 'text' as const, + text: JSON.stringify(result), }, ], isError: false, @@ -328,7 +248,7 @@ export const waitForBreakpointHit = async (params: { sessionId?: string; session return { content: [ { - type: 'text', + type: 'text' as const, text: `Error waiting for breakpoint: ${error instanceof Error ? error.message : String(error)}`, }, ], diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts index 72f6c3ce..aaa7ce0f 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/index.ts @@ -1,6 +1,6 @@ // Re-export everything from the individual modules export { listBreakpoints, listBreakpointsSchema, setBreakpoint, setBreakpointSchema } from './breakpoints'; -export { activeSessions, getCallStack } from './common'; +export { activeSessions, getCallStack, getCallStackSchema } from './common'; export type { BreakpointHitInfo } from './common'; export { breakpointEventEmitter, @@ -10,7 +10,7 @@ export { waitForBreakpointHit, waitForBreakpointHitSchema, } from './events'; -export { getCallStackSchema, getStackFrameVariables, getStackFrameVariablesSchema } from './inspection'; +export { getStackFrameVariables, getStackFrameVariablesSchema } from './inspection'; export { listDebugSessions, listDebugSessionsSchema, diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts index 31ab0aba..59679d01 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts @@ -1,18 +1,5 @@ import { z } from 'zod'; -import { activeSessions, getCallStack } from './common'; - -// Re-export getCallStack and its schema -export { getCallStack }; - -// Zod schema for validating get_call_stack parameters. -export const getCallStackSchema = z.object({ - sessionName: z - .string() - .optional() - .describe( - 'The name of the debug session to get call stack for. If not provided, returns call stacks for all active sessions.', - ), -}); +import { activeSessions } from './common'; /** * Get variables from a specific stack frame. @@ -27,9 +14,14 @@ export const getStackFrameVariables = async (params: { }) => { const { sessionId, frameId, threadId, filter } = params; + // Import the output channel for logging + const { outputChannel } = await import('./common'); + outputChannel.appendLine(`Getting variables for session ${sessionId}, frame ${frameId}, thread ${threadId}`); + // Find the session with the given ID const session = activeSessions.find((s) => s.id === sessionId); if (!session) { + outputChannel.appendLine(`No debug session found with ID '${sessionId}'`); return { content: [ { @@ -43,38 +35,102 @@ export const getStackFrameVariables = async (params: { try { // First, get the scopes for the stack frame + outputChannel.appendLine(`Requesting scopes for frameId ${frameId}`); const scopes = await session.customRequest('scopes', { frameId }); + outputChannel.appendLine(`Received scopes: ${JSON.stringify(scopes)}`); + + if (!scopes || !scopes.scopes || !Array.isArray(scopes.scopes)) { + outputChannel.appendLine(`Invalid scopes response: ${JSON.stringify(scopes)}`); + return { + content: [ + { + type: 'text', + text: `Invalid scopes response from debug adapter. This may be a limitation of the ${session.type} debug adapter.`, + }, + ], + isError: true, + }; + } // Then, get variables for each scope const variablesByScope = await Promise.all( scopes.scopes.map(async (scope: { name: string; variablesReference: number }) => { + outputChannel.appendLine( + `Processing scope: ${scope.name}, variablesReference: ${scope.variablesReference}`, + ); + if (scope.variablesReference === 0) { + outputChannel.appendLine(`Scope ${scope.name} has no variables (variablesReference is 0)`); return { scopeName: scope.name, variables: [], }; } - const response = await session.customRequest('variables', { - variablesReference: scope.variablesReference, - }); + try { + outputChannel.appendLine( + `Requesting variables for scope ${scope.name} with reference ${scope.variablesReference}`, + ); + const response = await session.customRequest('variables', { + variablesReference: scope.variablesReference, + }); + outputChannel.appendLine(`Received variables response: ${JSON.stringify(response)}`); + + if (!response || !response.variables || !Array.isArray(response.variables)) { + outputChannel.appendLine( + `Invalid variables response for scope ${scope.name}: ${JSON.stringify(response)}`, + ); + return { + scopeName: scope.name, + variables: [], + error: `Invalid variables response from debug adapter for scope ${scope.name}`, + }; + } - // Apply filter if provided - let filteredVariables = response.variables; - if (filter) { - const filterRegex = new RegExp(filter, 'i'); // Case insensitive match - filteredVariables = response.variables.filter((variable: { name: string }) => - filterRegex.test(variable.name), + // Apply filter if provided + let filteredVariables = response.variables; + if (filter) { + const filterRegex = new RegExp(filter, 'i'); // Case insensitive match + filteredVariables = response.variables.filter((variable: { name: string }) => + filterRegex.test(variable.name), + ); + outputChannel.appendLine( + `Applied filter '${filter}', filtered from ${response.variables.length} to ${filteredVariables.length} variables`, + ); + } + + return { + scopeName: scope.name, + variables: filteredVariables, + }; + } catch (scopeError) { + outputChannel.appendLine( + `Error getting variables for scope ${scope.name}: ${ + scopeError instanceof Error ? scopeError.message : String(scopeError) + }`, ); + return { + scopeName: scope.name, + variables: [], + error: `Error getting variables: ${ + scopeError instanceof Error ? scopeError.message : String(scopeError) + }`, + }; } - - return { - scopeName: scope.name, - variables: filteredVariables, - }; }), ); + // Check if we got any variables at all + const hasVariables = variablesByScope.some( + (scope) => scope.variables && Array.isArray(scope.variables) && scope.variables.length > 0, + ); + + if (!hasVariables) { + outputChannel.appendLine( + `No variables found in any scope. This may be a limitation of the ${session.type} debug adapter or the current debugging context.`, + ); + } + return { content: [ { @@ -85,17 +141,24 @@ export const getStackFrameVariables = async (params: { threadId, variablesByScope, filter: filter || undefined, + debuggerType: session.type, }, }, ], isError: false, }; } catch (error) { + outputChannel.appendLine( + `Error in getStackFrameVariables: ${error instanceof Error ? error.message : String(error)}`, + ); + outputChannel.appendLine(`Error stack: ${error instanceof Error ? error.stack : 'No stack available'}`); return { content: [ { type: 'text', - text: `Error getting variables: ${error instanceof Error ? error.message : String(error)}`, + text: `Error getting variables: ${ + error instanceof Error ? error.message : String(error) + }. This may be a limitation of the ${session.type} debug adapter.`, }, ], isError: true, diff --git a/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts index 296add68..d9c1df98 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts @@ -1,6 +1,170 @@ import * as vscode from 'vscode'; import { z } from 'zod'; -import { activeSessions } from './common'; +import { activeSessions, outputChannel } from './common'; + +/** + * Helper function to wait for a debug session to stop and gather debug information. + * This is used by both startDebugSession and resumeDebugSession when waitForStop is true. + * + * @param params - Object containing session information and options for waiting. + * @returns A response object with debug information or error details. + */ +async function waitForDebugSessionToStop(params: { + sessionId?: string; + sessionName?: string; + actionType: 'started' | 'resumed'; + timeout?: number; +}) { + const { sessionId, sessionName, actionType, timeout = 30000 } = params; + + try { + // Import the functions we need + const { waitForBreakpointHit } = await import('./events'); + const { getCallStack } = await import('./common'); + const { getStackFrameVariables } = await import('./inspection'); + + // Find the session if we have a sessionId but not a sessionName + let resolvedSessionName = sessionName; + if (!resolvedSessionName && sessionId) { + const session = activeSessions.find((s) => s.id === sessionId); + if (session) { + resolvedSessionName = session.name; + } + } + + outputChannel.appendLine( + `Waiting for debug session ${resolvedSessionName || sessionId} to stop at a breakpoint`, + ); + + // Wait for the debug session to stop + const stopResult = await waitForBreakpointHit({ + sessionId, + sessionName: resolvedSessionName, + timeout, + }); + + if (stopResult.isError) { + return { + content: [ + { + type: 'text', + text: `Debug session ${resolvedSessionName || sessionId} ${actionType} successfully.`, + }, + { + type: 'text', + text: `Warning: ${ + 'text' in stopResult.content[0] + ? stopResult.content[0].text + : 'Failed to wait for debug session to stop' + }`, + }, + ], + isError: false, + }; + } + + // Extract breakpoint hit information - now it's in text format + const breakpointInfoText = stopResult.content[0].text; + let breakpointInfo; + try { + breakpointInfo = JSON.parse(breakpointInfoText); + } catch (e) { + return { + content: [ + { + type: 'text', + text: `Debug session ${ + resolvedSessionName || sessionId + } ${actionType} successfully and stopped.`, + }, + { type: 'text', text: 'Breakpoint hit, but failed to parse details.' }, + ], + isError: false, + }; + } + + // Get detailed call stack information + const callStackResult = await getCallStack({ sessionName: resolvedSessionName || breakpointInfo.sessionName }); + let callStackData = null; + if (!callStackResult.isError && 'json' in callStackResult.content[0]) { + callStackData = callStackResult.content[0].json; + } + + // Get variables for the top frame if we have a frameId + let variablesData = null; + let variablesError = null; + if (breakpointInfo.frameId !== undefined && breakpointInfo.sessionId && breakpointInfo.threadId) { + outputChannel.appendLine(`Attempting to get variables for frameId ${breakpointInfo.frameId}`); + try { + const variablesResult = await getStackFrameVariables({ + sessionId: breakpointInfo.sessionId, + frameId: breakpointInfo.frameId, + threadId: breakpointInfo.threadId, + }); + + if (!variablesResult.isError && 'json' in variablesResult.content[0]) { + variablesData = variablesResult.content[0].json; + outputChannel.appendLine(`Successfully retrieved variables: ${JSON.stringify(variablesData)}`); + } else { + // Capture the error message if there was one + variablesError = variablesResult.isError + ? 'text' in variablesResult.content[0] + ? variablesResult.content[0].text + : 'Unknown error' + : 'Invalid response format'; + outputChannel.appendLine(`Failed to get variables: ${variablesError}`); + } + } catch (error) { + variablesError = error instanceof Error ? error.message : String(error); + outputChannel.appendLine(`Exception getting variables: ${variablesError}`); + } + } else { + variablesError = 'Missing required information for variable inspection'; + outputChannel.appendLine( + `Cannot get variables: ${variablesError} - frameId: ${breakpointInfo.frameId}, sessionId: ${breakpointInfo.sessionId}, threadId: ${breakpointInfo.threadId}`, + ); + } + + // Construct a comprehensive response with all the debug information + const debugInfo = { + breakpoint: breakpointInfo, + callStack: callStackData, + variables: variablesData, + variablesError: variablesError, + }; + + return { + content: [ + { + type: 'text', + text: `Debug session ${ + resolvedSessionName || breakpointInfo.sessionName + } ${actionType} successfully and stopped at ${ + breakpointInfo.reason === 'breakpoint' ? 'a breakpoint' : `due to ${breakpointInfo.reason}` + }.`, + }, + { + type: 'text', + text: JSON.stringify(debugInfo), + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { type: 'text', text: `Debug session ${sessionName || sessionId} ${actionType} successfully.` }, + { + type: 'text', + text: `Warning: Failed to wait for debug session to stop: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + isError: false, + }; + } +} /** * List all active debug sessions in the workspace. @@ -60,63 +224,10 @@ export const startDebugSession = async (params: { // If waitForStop is true, wait for the debug session to stop at a breakpoint or other stopping point if (waitForStop) { - try { - // Import the waitForBreakpointHit function from events.ts - const { waitForBreakpointHit } = await import('./events'); - - // Wait for the debug session to stop - const stopResult = await waitForBreakpointHit({ - sessionName: configuration.name, - timeout: 30000, // Default timeout of 30 seconds - }); - - if (stopResult.isError) { - return { - content: [ - { type: 'text', text: `Debug session '${configuration.name}' started successfully.` }, - { - type: 'text', - text: `Warning: ${ - 'text' in stopResult.content[0] - ? stopResult.content[0].text - : 'Failed to wait for debug session to stop' - }`, - }, - ], - isError: false, - }; - } - - return { - content: [ - { - type: 'text', - text: `Debug session '${configuration.name}' started successfully and stopped at a breakpoint.`, - }, - { - type: 'text', - text: - 'json' in stopResult.content[0] - ? JSON.stringify(stopResult.content[0].json) - : 'Breakpoint hit, but no details available', - }, - ], - isError: false, - }; - } catch (error) { - return { - content: [ - { type: 'text', text: `Debug session '${configuration.name}' started successfully.` }, - { - type: 'text', - text: `Warning: Failed to wait for debug session to stop: ${ - error instanceof Error ? error.message : String(error) - }`, - }, - ], - isError: false, - }; - } + return await waitForDebugSessionToStop({ + sessionName: configuration.name, + actionType: 'started', + }); } return { @@ -191,10 +302,10 @@ export const stopDebugSessionSchema = z.object({ /** * Resume execution of a debug session that has been paused (e.g., by a breakpoint). * - * @param params - Object containing the sessionId of the debug session to resume. + * @param params - Object containing the sessionId of the debug session to resume and optional waitForStop flag. */ -export const resumeDebugSession = async (params: { sessionId: string }) => { - const { sessionId } = params; +export const resumeDebugSession = async (params: { sessionId: string; waitForStop?: boolean }) => { + const { sessionId, waitForStop = false } = params; // Find the session with the given ID const session = activeSessions.find((s) => s.id === sessionId); @@ -212,8 +323,19 @@ export const resumeDebugSession = async (params: { sessionId: string }) => { try { // Send the continue request to the debug adapter + outputChannel.appendLine(`Resuming debug session '${session.name}' (ID: ${sessionId})`); await session.customRequest('continue', { threadId: 0 }); // 0 means all threads + // If waitForStop is true, wait for the debug session to stop at a breakpoint or other stopping point + if (waitForStop) { + return await waitForDebugSessionToStop({ + sessionId, + sessionName: session.name, + actionType: 'resumed', + }); + } + + // If not waiting for stop, return immediately return { content: [ { @@ -239,4 +361,11 @@ export const resumeDebugSession = async (params: { sessionId: string }) => { // Zod schema for validating resume_debug_session parameters. export const resumeDebugSessionSchema = z.object({ sessionId: z.string().describe('The ID of the debug session to resume.'), + waitForStop: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, the tool will wait until a breakpoint is hit or the debugger otherwise stops before returning. Provides detailed information about the stopped state.', + ), });