diff --git a/mcp-servers/mcp-server-vscode/.gitignore b/mcp-servers/mcp-server-vscode/.gitignore index 61a68673f..a70fa3424 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 1572e9f23..4b4a3dcf9 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 85a4eb161..31696417a 100644 --- a/mcp-servers/mcp-server-vscode/src/extension.ts +++ b/mcp-servers/mcp-server-vscode/src/extension.ts @@ -9,13 +9,18 @@ import { z } from 'zod'; import packageJson from '../package.json'; import { codeCheckerTool } from './tools/code_checker'; import { - listDebugSessions, - listDebugSessionsSchema, + listBreakpoints, + listBreakpointsSchema, + onBreakpointHit, + resumeDebugSession, + resumeDebugSessionSchema, + setBreakpoint, + setBreakpointSchema, startDebugSession, startDebugSessionSchema, stopDebugSession, stopDebugSessionSchema, -} from './tools/debug_tools'; +} from './tools/debug'; import { focusEditorTool } from './tools/focus_editor'; import { resolvePort } from './utils/port'; @@ -99,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(), // { @@ -122,16 +127,53 @@ export const activate = async (context: vscode.ExtensionContext) => { // }, // ); - // Register 'list_debug_sessions' tool + // Register 'set_breakpoint' tool mcpServer.tool( - 'list_debug_sessions', - 'List all active debug sessions in the workspace.', - listDebugSessionsSchema.shape, // No parameters required - async () => { - const result = await listDebugSessions(); + '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) => ({ type: 'text', text: JSON.stringify(item.json) })), + content: result.content.map((item) => ({ + ...item, + type: 'text' as const, + })), + }; + }, + ); + + // 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) => ({ + ...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.', + stopDebugSessionSchema.shape, + async (params) => { + const result = await stopDebugSession(params); + return { + ...result, + content: result.content.map((item) => ({ + ...item, + type: 'text' as const, + })), }; }, ); @@ -139,7 +181,7 @@ export const activate = async (context: vscode.ExtensionContext) => { // Register 'start_debug_session' tool mcpServer.tool( 'start_debug_session', - 'Start a new debug session with the provided configuration.', + '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); @@ -153,8 +195,6 @@ export const activate = async (context: vscode.ExtensionContext) => { }, ); - // Register 'stop_debug_session' tool - // Register 'restart_debug_session' tool mcpServer.tool( 'restart_debug_session', @@ -175,18 +215,23 @@ export const activate = async (context: vscode.ExtensionContext) => { }; }, ); + + // Register 'list_breakpoints' tool mcpServer.tool( - 'stop_debug_session', - 'Stop all debug sessions that match the provided session name.', - stopDebugSessionSchema.shape, + '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 stopDebugSession(params); + const result = await listBreakpoints(params); return { ...result, - content: result.content.map((item) => ({ - ...item, - type: 'text' as const, - })), + 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 }); + }), }; }, ); @@ -236,6 +281,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/breakpoints.d.ts b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.d.ts new file mode 100644 index 000000000..de4cdf08d --- /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 000000000..74ec4f8f6 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/breakpoints.ts @@ -0,0 +1,164 @@ +import * as path from 'node: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 000000000..a443891f7 --- /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 000000000..f82668cc9 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/common.ts @@ -0,0 +1,168 @@ +import * as vscode from 'vscode'; +import { z } from 'zod'; + +// 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, + }; + } +}; +// 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); + 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 000000000..670e22457 --- /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 000000000..f9d6d075d --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/events.ts @@ -0,0 +1,309 @@ +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' as const, + text: `No matching debug sessions found.`, + }, + ], + isError: true, + }; + } + + try { + // Create a promise that resolves when a breakpoint is hit + 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(() => { + listener.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: 'text' as const, + text: JSON.stringify(result), + }, + ], + isError: false, + }; + } catch (error) { + return { + content: [ + { + type: 'text' as const, + 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 000000000..bb9b6c5f9 --- /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 000000000..aaa7ce0f8 --- /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, getCallStackSchema } from './common'; +export type { BreakpointHitInfo } from './common'; +export { + breakpointEventEmitter, + onBreakpointHit, + subscribeToBreakpointEvents, + subscribeToBreakpointEventsSchema, + waitForBreakpointHit, + waitForBreakpointHitSchema, +} from './events'; +export { 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 000000000..acd87a11d --- /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 000000000..59679d01d --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/inspection.ts @@ -0,0 +1,175 @@ +import { z } from 'zod'; +import { activeSessions } from './common'; + +/** + * 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; + + // 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: [ + { + type: 'text', + text: `No debug session found with ID '${sessionId}'.`, + }, + ], + isError: true, + }; + } + + 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: [], + }; + } + + 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), + ); + 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) + }`, + }; + } + }), + ); + + // 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: [ + { + type: 'json', + json: { + sessionId, + frameId, + 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) + }. This may be a limitation of the ${session.type} debug adapter.`, + }, + ], + 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 000000000..682de0f25 --- /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 000000000..d9c1df981 --- /dev/null +++ b/mcp-servers/mcp-server-vscode/src/tools/debug/session.ts @@ -0,0 +1,371 @@ +import * as vscode from 'vscode'; +import { z } from 'zod'; +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. + * + * 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) { + return await waitForDebugSessionToStop({ + sessionName: configuration.name, + actionType: 'started', + }); + } + + 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 and optional waitForStop flag. + */ +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); + 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 + 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: [ + { + 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.'), + 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.', + ), +}); 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..84c5131aa 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,81 +1 @@ -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 * 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 ebcf5bae8..11154c794 100644 --- a/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts +++ b/mcp-servers/mcp-server-vscode/src/tools/debug_tools.ts @@ -1,135 +1,2 @@ -import * as vscode from 'vscode'; -import { z } from 'zod'; - -/** Maintain a list of active debug sessions. */ -const activeSessions: vscode.DebugSession[] = []; - -// Track new debug sessions as they start. -vscode.debug.onDidStartDebugSession((session) => { - activeSessions.push(session); -}); - -// Remove debug sessions as they terminate. -vscode.debug.onDidTerminateDebugSession((session) => { - const index = activeSessions.indexOf(session); - if (index >= 0) { - activeSessions.splice(index, 1); - } -}); - -/** - * 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.'), -}); +// Re-export everything from the debug directory +export * from './debug'; 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 000000000..b9412f5a8 --- /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'], + }, +});