diff --git a/mcp-servers/mcp-server-vscode/src/extension.ts b/mcp-servers/mcp-server-vscode/src/extension.ts index 85a4eb161..3f5a3c005 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -9,8 +9,16 @@ import { z } from 'zod'; import packageJson from '../package.json'; import { codeCheckerTool } from './tools/code_checker'; import { + getCallStack, + getCallStackSchema, + getStackFrameVariables, + getStackFrameVariablesSchema, listDebugSessions, listDebugSessionsSchema, + resumeDebugSession, + resumeDebugSessionSchema, + setBreakpoint, + setBreakpointSchema, startDebugSession, startDebugSessionSchema, stopDebugSession, @@ -153,19 +161,50 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); - // Register 'stop_debug_session' tool + // 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 'restart_debug_session' tool + // Register 'get_call_stack' 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' + 'get_call_stack', + 'Get the current call stack information for an active debug session.', + getCallStackSchema.shape, async (params) => { - // Stop current session using the provided session name - await stopDebugSession({ sessionName: params.configuration.name }); + 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 }; + }), + }; + }, + ); - // Then start a new debug session with the given configuration - const result = await startDebugSession(params); + // Register 'resume_debug_session' tool + mcpServer.tool( + 'resume_debug_session', + 'Resume execution of a debug session that has been paused (e.g., by a breakpoint).', + resumeDebugSessionSchema.shape, + async (params) => { + const result = await resumeDebugSession(params); return { ...result, content: result.content.map((item) => ({ @@ -175,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.', @@ -191,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 f974eb6b0..5da809947 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,113 @@ 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; + 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; +}>; 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 ebcf5bae8..d5dff785f 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,362 @@ 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 - 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.'), +});