UNPKG

@bloopai/debugger-mcp

Version:

An MCP server that provides interactive debugging capabilities to AI coding agents

1,129 lines (1,128 loc) 49.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LaunchArgumentsBuilder = exports.DAP_DEFAULT_EVENT_TIMEOUT_MS = void 0; exports.handleGetPendingAsyncEvents = handleGetPendingAsyncEvents; exports.handleStartDebugSession = handleStartDebugSession; exports.handleSetBreakpoint = handleSetBreakpoint; exports.handleEvaluateExpression = handleEvaluateExpression; exports.handleGetVariables = handleGetVariables; exports.handleGetCallStack = handleGetCallStack; exports.handleContinue = handleContinue; exports.handleStepOver = handleStepOver; exports.handleTerminateSession = handleTerminateSession; exports.handleListSessions = handleListSessions; exports.handleGetSessionDetails = handleGetSessionDetails; exports.handleStepIn = handleStepIn; exports.handleStepOut = handleStepOut; exports.handleListAdapters = handleListAdapters; const path = __importStar(require("path")); const ergonomic_debugger_client_1 = require("@bloopai/ergonomic-debugger-client"); const types_1 = require("../types"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const errorUtils_1 = require("../errorUtils"); exports.DAP_DEFAULT_EVENT_TIMEOUT_MS = 15000; const DAP_TERMINATION_RACE_TIMEOUT_MS = 15100; // HELPER FUNCTIONS START function validateRequiredToolArgs(args, requiredParams, toolName, sessionProvider, sessionIdForErrorContext) { for (const param of requiredParams) { const value = args[param.key]; let isValid = true; if (value === undefined || value === null) { isValid = false; } else if (param.type) { // Simplified type check, can be expanded if (param.type === 'object' && (typeof value !== 'object' || Array.isArray(value))) { isValid = false; } else if (param.type !== 'object' && typeof value !== param.type) { isValid = false; } } if (!isValid) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`Missing or invalid parameter '${param.key}' for ${toolName}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidParams) .mcpDebugErrorType('invalid_tool_arguments') .sessionProvider(sessionProvider) .sessionId(sessionIdForErrorContext) .additionalDebugData({ argumentName: param.key, problemDetail: `Missing or invalid parameter '${param.key}'. Expected type: ${param.type || 'any'}.`, }) .build(); } } } async function getActiveDebugSessionOrThrow(sessionProvider, sessionId, toolName, checkTermination = true) { if (!sessionId) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`Missing sessionId for ${toolName}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidParams) .mcpDebugErrorType('invalid_tool_arguments') .sessionProvider(sessionProvider) .additionalDebugData({ argumentName: 'sessionId', problemDetail: `Missing sessionId for ${toolName}`, }) .build(); } if (checkTermination) { const terminatedEvent = sessionProvider.findAndConsumeSessionEvent(sessionId, 'unexpected_session_termination', true); if (terminatedEvent) { const eventData = terminatedEvent.data; const reason = eventData?.reason; const exitCode = eventData?.exitCode; const signal = eventData?.signal; const underlyingError = eventData?.underlyingError; const originalMessage = `Session ${sessionId} terminated before ${toolName} could be executed. Reason: ${reason || 'Unknown'}. Exit Code: ${exitCode}, Signal: ${signal}.`; let structuredUnderlyingError; if (underlyingError instanceof Error) { structuredUnderlyingError = { message: underlyingError.message, name: underlyingError.name, stack: underlyingError.stack, }; } else if (underlyingError) { structuredUnderlyingError = { message: String(underlyingError) }; } throw new errorUtils_1.McpErrorBuilder() .error(new Error(originalMessage)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidRequest) .mcpDebugErrorType('unexpected_session_termination') .sessionProvider(sessionProvider) .sessionId(sessionId) .additionalDebugData({ terminationReason: reason, terminationExitCode: exitCode, terminationSignal: signal, terminationUnderlyingError: structuredUnderlyingError, originalMessage: `Session ${sessionId} terminated before ${toolName} could be executed.`, }) .build(); } } const debugSession = sessionProvider.getDebugSession(sessionId); if (!debugSession) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`Debug session not found: ${sessionId}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidRequest) .mcpDebugErrorType('session_not_found') .sessionProvider(sessionProvider) .sessionId(sessionId) .build(); } return debugSession; } /** * Waits for a 'stopped' event from the DAP handler after a step-like operation. * Includes a timeout and handles logging if the event is not received. * @param dapHandler The DAPSessionHandler to listen on. * @param toolName The name of the tool initiating the wait, for logging. * @param sessionId The ID of the session, for logging. * @param logger The logger instance. * @param timeoutMs The timeout duration in milliseconds. * @returns A promise that resolves with the StoppedEvent body, or undefined if a timeout occurs or an error is caught. */ async function _waitForStoppedEventAfterStep(dapHandler, toolName, sessionId, logger, timeoutMs = exports.DAP_DEFAULT_EVENT_TIMEOUT_MS) { try { return await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(`Timeout waiting for 'stopped' event after ${toolName} for session ${sessionId}`)), timeoutMs); const listener = (eventBody) => { clearTimeout(timeout); dapHandler.off('stopped', listener); resolve(eventBody); }; dapHandler.on('stopped', listener); }); } catch (e) { logger.warn(`[${toolName}] Error waiting for stopped event for session ${sessionId}: ${e.message}`); return undefined; } } /** * Resolves the target thread ID for an operation. * If requestedThreadId is provided, it's returned. * Otherwise, it defaults based on available threads and the requireStopped flag. * @param sessionProvider The session provider. * @param sessionId The session ID. * @param requestedThreadId The thread ID from the tool arguments, if any. * @param toolName The name of the tool calling this helper. * @param requireStopped If true, a stopped thread is required for defaulting. * @returns A promise resolving to the target thread ID. * @throws McpError if a suitable thread cannot be determined. */ async function _resolveTargetThreadId(sessionProvider, sessionId, requestedThreadId, toolName, requireStopped) { if (requestedThreadId !== undefined) { return requestedThreadId; } const dapHandler = sessionProvider.getDAPSessionHandler(sessionId); if (!dapHandler) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`DAPSessionHandler not found for session ${sessionId} when resolving target threadId for ${toolName}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('dap_handler_not_found') .sessionProvider(sessionProvider) .sessionId(sessionId) .build(); } const threads = dapHandler.getThreads(); if (threads.length === 0) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`No threads available in session ${sessionId} to default for ${toolName}.`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidRequest) .mcpDebugErrorType('no_threads_available') .sessionProvider(sessionProvider) .sessionId(sessionId) .build(); } const stoppedThread = threads.find((t) => t.stoppedDetails); if (requireStopped) { if (!stoppedThread) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`No stopped thread available in session ${sessionId} to default for ${toolName}. An active thread must be paused.`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidRequest) .mcpDebugErrorType('no_stopped_thread_available') .sessionProvider(sessionProvider) .sessionId(sessionId) .additionalDebugData({ problemDetail: `No stopped thread available to default for ${toolName}. Current threads: ${JSON.stringify(threads.map((t) => ({ id: t.id, name: t.name, stopped: !!t.stoppedDetails })))}`, }) .build(); } sessionProvider .getLogger() .info(`[${toolName}] threadId not provided, defaulted to stopped thread ${stoppedThread.id} for session ${sessionId}`); return stoppedThread.id; } else { // For operations like get_call_stack, prefer a stopped thread, but any thread is fine. const targetThread = stoppedThread || threads[0]; sessionProvider .getLogger() .info(`[${toolName}] threadId not provided, defaulted to thread ${targetThread.id} (stopped: ${!!stoppedThread}) for session ${sessionId}`); return targetThread.id; } } // HELPER FUNCTIONS END function _ensureThreadIsStopped(dapHandler, threadId, toolName, sessionId, sessionProvider) { const thread = dapHandler.getThread(threadId); if (!thread || !thread.stoppedDetails) { const sessionStatus = dapHandler.status; const allThreadsInfo = dapHandler.getThreads().map((t) => ({ id: t.id, name: t.name, isStopped: !!t.stoppedDetails, stopReason: t.stoppedDetails?.reason, })); const problemDetailText = `Thread ${threadId} is not in a stopped state for operation '${toolName}'. It must be paused (e.g., at a breakpoint or after a previous step). Current session status: '${sessionStatus}'.`; sessionProvider .getLogger() .warn(`[${toolName}] Precondition failed for session ${sessionId}: ${problemDetailText} Thread states: ${JSON.stringify(allThreadsInfo)}`); throw new errorUtils_1.McpErrorBuilder() .error(new Error(problemDetailText)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InvalidRequest) .mcpDebugErrorType('invalid_tool_arguments') .sessionProvider(sessionProvider) .sessionId(sessionId) .additionalDebugData({ operation: toolName, argumentName: 'threadState', problemDetail: `${problemDetailText} Expected thread state: 'stopped'. Actual session status: '${sessionStatus}'. Current thread ID: ${threadId}. All threads info: ${JSON.stringify(allThreadsInfo)}`, }) .build(); } } function mapDebuggerStackFrameToModel(debuggerFrame, threadId) { return { id: debuggerFrame.id, name: debuggerFrame.name, source: debuggerFrame.source ? { name: debuggerFrame.source.name, path: debuggerFrame.source.path, sourceReference: debuggerFrame.source.sourceReference, adapterData: debuggerFrame.source.adapterData, } : undefined, line: debuggerFrame.line, column: debuggerFrame.column, instructionPointerReference: debuggerFrame.instructionPointerReference, threadId: threadId, }; } /** * Generic helper to perform a step operation (over, in, out), * wait for the 'stopped' event, and handle common outcomes. */ async function _performStepOperation(toolName, debugSession, dapHandler, targetThreadId, sessionProvider, localSessionId, stepActionFn) { const outputEvents = []; const outputListener = (eventBody) => { outputEvents.push(eventBody); }; dapHandler.on('output', outputListener); const stoppedPromise = _waitForStoppedEventAfterStep(dapHandler, toolName, localSessionId, sessionProvider.getLogger()); await stepActionFn(targetThreadId); const stoppedEvent = await stoppedPromise; dapHandler.off('output', outputListener); if (!stoppedEvent) { sessionProvider .getLogger() .warn(`[${toolName}] Did not receive a stopped event for session ${localSessionId} after ${toolName}. The program might have continued or terminated.`); const terminatedEventData = sessionProvider.findAndConsumeSessionEvent(localSessionId, 'unexpected_session_termination', true); if (terminatedEventData) { const eventData = terminatedEventData.data; return { sessionState: types_1.SessionState.TERMINATED, outputEvents, terminatedEvent: eventData, error: `Session terminated unexpectedly during ${toolName}. Reason: ${eventData?.reason || 'Unknown'}`, }; } return { sessionState: mapStatusToSessionState(dapHandler.status), outputEvents, error: `No 'stopped' event received after ${toolName}. Current session status: ${dapHandler.status}.`, }; } let topStackFrame; if (stoppedEvent) { try { const stackFrames = await debugSession.getCallStack(stoppedEvent.threadId); if (stackFrames.length > 0) { const topFrameFromDebugger = stackFrames[0]; topStackFrame = mapDebuggerStackFrameToModel(topFrameFromDebugger, stoppedEvent.threadId); } } catch (e) { sessionProvider .getLogger() .warn(`[${toolName}] Error getting stack frame for location: ${e.message}`); } } return { sessionState: types_1.SessionState.STOPPED, stoppedEvent, outputEvents, topStackFrame, }; } async function handleGetPendingAsyncEvents(sessionProvider, _args) { if (typeof sessionProvider.drainAsyncEventQueue !== 'function') { sessionProvider .getLogger() .error('[handleGetPendingAsyncEvents] drainAsyncEventQueue method is not available on sessionProvider.'); return { events: [] }; } const events = sessionProvider.drainAsyncEventQueue(); sessionProvider .getLogger() .info(`[handleGetPendingAsyncEvents] Drained ${events.length} events.`); return { events }; } class LaunchArgumentsBuilder { launchArgs = { noDebug: false, console: 'internalConsole', request: 'launch', type: '', name: '', }; constructor(mcpArgs) { this.launchArgs.program = mcpArgs.targetPath; this.launchArgs.name = `Debug ${mcpArgs.targetPath}`; if (mcpArgs.launchArgs) { this.launchArgs.args = mcpArgs.launchArgs; } if (mcpArgs.stopOnEntry !== undefined) { this.launchArgs.stopOnEntry = mcpArgs.stopOnEntry; } if (mcpArgs.cwd) { this.launchArgs.cwd = mcpArgs.cwd; } if (mcpArgs.env) { this.launchArgs.env = mcpArgs.env; } const knownMcpArgs = [ 'targetType', 'targetPath', 'launchArgs', 'stopOnEntry', 'cwd', 'env', 'request', 'type', 'name', 'program', 'noDebug', 'console', ]; for (const key in mcpArgs) { if (Object.prototype.hasOwnProperty.call(mcpArgs, key)) { if (!knownMcpArgs.includes(key)) { const value = mcpArgs[key]; if (value !== undefined) { this.launchArgs[key] = value; } } } } } withProgram(program) { this.launchArgs.program = program; return this; } withRequestType(request) { this.launchArgs.request = request; return this; } withDebugType(type) { this.launchArgs.type = type; return this; } withName(name) { this.launchArgs.name = name; return this; } withLaunchArguments(args) { this.launchArgs.args = args; return this; } withStopOnEntry(stop) { this.launchArgs.stopOnEntry = stop; return this; } withCwd(cwd) { this.launchArgs.cwd = cwd; return this; } withEnv(env) { const filteredEnv = {}; for (const key in env) { if (env[key] !== undefined) { filteredEnv[key] = env[key]; } } this.launchArgs.env = filteredEnv; return this; } withConsoleType(consoleType) { this.launchArgs.console = consoleType; return this; } withNoDebug(noDebug) { this.launchArgs.noDebug = noDebug; return this; } withAdapterOption(key, value) { if (key === 'request' || key === 'type' || key === 'name' || key === 'program') { console.warn(`[LaunchArgumentsBuilder] Attempting to set reserved key "${key}" with withAdapterOption. Use dedicated method if available.`); } this.launchArgs[key] = value; return this; } build() { if (!this.launchArgs.type) { console.warn('[LaunchArgumentsBuilder] Debug type not set. This might be required by the adapter.'); } if (!this.launchArgs.name) { this.launchArgs.name = `Debug Session`; } return this.launchArgs; } } exports.LaunchArgumentsBuilder = LaunchArgumentsBuilder; function mapStatusToSessionState(status) { switch (status) { case 'pending': return types_1.SessionState.INITIALIZING; case 'initializing': return types_1.SessionState.INITIALIZING; case 'initialized': return types_1.SessionState.CONFIGURED; case 'active': return types_1.SessionState.RUNNING; case 'terminating': return types_1.SessionState.TERMINATED; case 'terminated': return types_1.SessionState.TERMINATED; default: return types_1.SessionState.INITIALIZING; } } /** * Tool handler for starting a debug session */ async function handleStartDebugSession(sessionProvider, args) { try { const logger = sessionProvider.getLogger(); // Runtime validation of targetType against available adapters const { adapters: availableAdapterConfigs } = await handleListAdapters(sessionProvider, {}); const availableAdapterTypes = availableAdapterConfigs.map((config) => config.type); const targetTypeToUse = args.targetType || 'python'; if (!availableAdapterTypes.includes(targetTypeToUse)) { logger.warn(`[handleStartDebugSession] Unsupported targetType: '${targetTypeToUse}'. Available: ${availableAdapterTypes.join(', ')}`); throw new errorUtils_1.McpErrorBuilder() .error(new Error(`Unsupported targetType: '${targetTypeToUse}'. Available types are: ${availableAdapterTypes.join(', ')}.`)) .toolName('start_debug_session') .mcpErrorCode(types_js_1.ErrorCode.InvalidParams) .mcpDebugErrorType('invalid_tool_arguments') .sessionProvider(sessionProvider) .additionalDebugData({ problemDetail: `Unsupported targetType: '${targetTypeToUse}'. Requested: '${targetTypeToUse}', Available: ${availableAdapterTypes.join(', ')}. Please use one of the available types.`, }) .build(); } logger.info(`[handleStartDebugSession] targetType '${targetTypeToUse}' validated against available adapters: ${availableAdapterTypes.join(', ')}.`); // Validate required arguments (targetPath is primary here) validateRequiredToolArgs(args, [{ key: 'targetPath', type: 'string' }], 'start_debug_session', sessionProvider, undefined); if (!args.targetPath || !path.isAbsolute(args.targetPath)) { throw new errorUtils_1.McpErrorBuilder() .error(new Error('Missing, invalid, or relative targetPath from MCP call. An absolute path is required.')) .toolName('start_debug_session') .mcpErrorCode(types_js_1.ErrorCode.InvalidParams) .mcpDebugErrorType('invalid_tool_arguments') .sessionProvider(sessionProvider) .additionalDebugData({ argumentName: 'targetPath', problemDetail: 'Missing, invalid, or relative targetPath. An absolute path is required.', receivedPath: args.targetPath, }) .build(); } const launchArgs = new LaunchArgumentsBuilder(args) .withDebugType(targetTypeToUse) .withRequestType('launch') .build(); const edcSessionManager = sessionProvider.getSessionManager(); const dapHandler = await edcSessionManager.startSession(targetTypeToUse, launchArgs, 'launch'); const sessionId = dapHandler.sessionId; const startTime = new Date(); const debugSession = sessionProvider.createDebugSession(dapHandler, targetTypeToUse, args.targetPath, logger); sessionProvider.storeDebugSession(sessionId, debugSession, startTime, targetTypeToUse, args.targetPath); const sessionInfo = { id: sessionId, targetType: targetTypeToUse, targetPath: args.targetPath, state: mapStatusToSessionState(dapHandler.status), startTime: startTime, }; let stoppedEventBody; if (args.stopOnEntry) { logger.info(`[handleStartDebugSession] stopOnEntry is true for session ${sessionId}. Waiting for initial 'stopped' (entry) event...`); try { stoppedEventBody = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { dapHandler.off('stopped', entryStopListener); reject(new Error(`Timeout waiting for initial 'stopped' (entry) event for session ${sessionId}`)); }, exports.DAP_DEFAULT_EVENT_TIMEOUT_MS); const entryStopListener = (eventBody) => { if (eventBody.reason === 'entry' || eventBody.reason === 'step') { logger.info(`[handleStartDebugSession] Received initial 'stopped' event for session ${sessionId}:`, eventBody); clearTimeout(timeout); dapHandler.off('stopped', entryStopListener); resolve(eventBody); } else { logger.debug(`[handleStartDebugSession] Received 'stopped' event for session ${sessionId} with reason '${eventBody.reason}', waiting for 'entry'.`); } }; dapHandler.on('stopped', entryStopListener); }); } catch (eventErrorUnknown) { const eventErrorMessage = eventErrorUnknown instanceof Error ? eventErrorUnknown.message : String(eventErrorUnknown); logger.warn(`[handleStartDebugSession] Error or timeout waiting for initial 'stopped' event for session ${sessionId}: ${eventErrorMessage}`); } } return { sessionId, sessionInfo, stoppedEvent: stoppedEventBody }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('start_debug_session') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .build(); } } /** * Tool handler for setting a breakpoint */ async function handleSetBreakpoint(sessionProvider, args) { try { validateRequiredToolArgs(args, [ { key: 'sessionId', type: 'string' }, { key: 'filePath', type: 'string' }, { key: 'lineNumber', type: 'number' }, ], 'set_breakpoint', sessionProvider, args.sessionId); const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, args.sessionId, 'set_breakpoint'); const configBuilder = new ergonomic_debugger_client_1.BreakpointConfigBuilder(args.filePath, args.lineNumber); const resultFromDebugger = await debugSession.setBreakpoint(configBuilder.build()); return { breakpointId: resultFromDebugger.id, verified: resultFromDebugger.verified, line: resultFromDebugger.line, message: resultFromDebugger.message, }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('set_breakpoint') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for evaluating an expression */ async function handleEvaluateExpression(sessionProvider, args) { try { validateRequiredToolArgs(args, [ { key: 'sessionId', type: 'string' }, { key: 'expression', type: 'string' }, ], 'evaluate_expression', sessionProvider, args.sessionId); const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, args.sessionId, 'evaluate_expression'); const validContexts = ['watch', 'repl', 'hover']; const evaluationContext = args.context && validContexts.includes(args.context) ? args.context : 'repl'; const resultFromDebugger = await debugSession.evaluateExpression(args.expression, args.frameId, evaluationContext); return { result: resultFromDebugger.result, type: resultFromDebugger.type, variablesReference: resultFromDebugger.variablesReference, namedVariables: resultFromDebugger.namedVariables, indexedVariables: resultFromDebugger.indexedVariables, }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('evaluate_expression') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for getting variables */ async function handleGetVariables(sessionProvider, args) { try { validateRequiredToolArgs(args, [ { key: 'sessionId', type: 'string' }, { key: 'variablesReference', type: 'number' }, ], 'get_variables', sessionProvider, args.sessionId); const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, args.sessionId, 'get_variables'); const variablesFromDebugger = await debugSession.getVariables(args.variablesReference); return variablesFromDebugger.map((v) => ({ name: v.name, value: v.value, type: v.type, variablesReference: v.variablesReference, namedVariables: v.namedVariables, indexedVariables: v.indexedVariables, })); } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('get_variables') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for getting the call stack */ async function handleGetCallStack(sessionProvider, args) { try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], 'get_call_stack', sessionProvider, args.sessionId); const localSessionId = args.sessionId; const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, localSessionId, 'get_call_stack'); const targetThreadId = await _resolveTargetThreadId(sessionProvider, localSessionId, args.threadId, 'get_call_stack', false); const stackFramesFromDebugger = await debugSession.getCallStack(targetThreadId); return stackFramesFromDebugger.map((sf) => mapDebuggerStackFrameToModel(sf, targetThreadId)); } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('get_call_stack') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for continuing execution */ async function handleContinue(sessionProvider, args) { const toolName = 'continue'; try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], toolName, sessionProvider, args.sessionId); const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, args.sessionId, toolName, false); const dapHandler = sessionProvider.getDAPSessionHandler(args.sessionId); if (!dapHandler) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`DAPSessionHandler not available for session ${args.sessionId}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('dap_handler_not_found') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } const outputEvents = []; const outputListener = (eventBody) => { outputEvents.push(eventBody); }; dapHandler.on('output', outputListener); // Before sending 'continue', set up listeners. // The session might stop again, or it might terminate. const stoppedPromise = _waitForStoppedEventAfterStep(dapHandler, toolName, args.sessionId, sessionProvider.getLogger(), exports.DAP_DEFAULT_EVENT_TIMEOUT_MS); const terminatedPromise = new Promise((resolve) => { const timeout = setTimeout(() => { dapHandler.off('sessionEnded', listener); resolve(undefined); }, DAP_TERMINATION_RACE_TIMEOUT_MS); const listener = (payload) => { clearTimeout(timeout); dapHandler.off('sessionEnded', listener); resolve({ terminated: true, exitCode: payload?.exitCode, signal: payload?.signal, error: payload?.error, }); }; dapHandler.once('sessionEnded', listener); }); await debugSession.continue(args.threadId); // Wait for either a stopped event or a terminated event const raceResult = await Promise.race([ stoppedPromise.then((event) => ({ type: 'stopped', event })), terminatedPromise.then((event) => ({ type: 'terminated', event })), ]); let stoppedEvent; let terminatedEventInfo; let topStackFrame; if (raceResult.type === 'stopped' && raceResult.event) { stoppedEvent = raceResult.event; sessionProvider .getLogger() .info(`[${toolName}] Session ${args.sessionId} stopped. Reason: ${stoppedEvent.reason}`); try { const stackFrames = await debugSession.getCallStack(stoppedEvent.threadId); if (stackFrames.length > 0) { const topFrameFromDebugger = stackFrames[0]; topStackFrame = mapDebuggerStackFrameToModel(topFrameFromDebugger, stoppedEvent.threadId); } } catch (e) { sessionProvider .getLogger() .warn(`[${toolName}] Error getting stack frame for location after continue: ${e.message}`); } } else if (raceResult.type === 'terminated' && raceResult.event) { terminatedEventInfo = raceResult.event; sessionProvider .getLogger() .info(`[${toolName}] Session ${args.sessionId} terminated after continue. ExitCode: ${terminatedEventInfo.exitCode}, Signal: ${terminatedEventInfo.signal}`); } else { sessionProvider .getLogger() .info(`[${toolName}] Session ${args.sessionId} continued. It might be running or the 'stopped' event timed out (indicating it's running).`); } dapHandler.off('output', outputListener); const sessionState = mapStatusToSessionState(dapHandler.status); return { success: true, sessionState, stoppedEvent, outputEvents, terminatedEvent: terminatedEventInfo, topStackFrame, }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('continue') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for stepping over */ async function handleStepOver(sessionProvider, args) { const toolName = 'step_over'; try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], toolName, sessionProvider, args.sessionId); const localSessionId = args.sessionId; const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, localSessionId, toolName); const dapHandler = sessionProvider.getDAPSessionHandler(localSessionId); if (!dapHandler) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`DAPSessionHandler not available for session ${localSessionId} in ${toolName}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('dap_handler_not_found') .sessionProvider(sessionProvider) .sessionId(localSessionId) .build(); } const targetThreadId = await _resolveTargetThreadId(sessionProvider, localSessionId, args.threadId, toolName, true); _ensureThreadIsStopped(dapHandler, targetThreadId, toolName, localSessionId, sessionProvider); const stepResult = await _performStepOperation(toolName, debugSession, dapHandler, targetThreadId, sessionProvider, localSessionId, async (thrId) => debugSession.stepOver(thrId)); return { success: !!stepResult.stoppedEvent && !stepResult.error && !stepResult.terminatedEvent, ...stepResult, }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for terminating a debug session */ async function handleTerminateSession(sessionProvider, args) { const toolName = 'terminate_session'; try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], toolName, sessionProvider, args.sessionId); const debugSession = sessionProvider.getDebugSession(args.sessionId); if (!debugSession) { sessionProvider .getLogger() .warn(`[${toolName}] Session ${args.sessionId} not found. Assuming already terminated or never existed.`); return { success: true }; // Session already gone, consider it a success. } await debugSession.terminate(); sessionProvider .getLogger() .info(`[${toolName}] Terminated session ${args.sessionId}.`); return { success: true }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } /** * Tool handler for listing all active debug sessions */ async function handleListSessions(sessionProvider, _args) { try { const sessions = sessionProvider.getAllSessionInfo(); return { sessions }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('list_sessions') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .build(); } } /** * Tool handler for listing all registered debug adapter configurations. */ /** * Tool handler for getting detailed information about a specific debug session */ async function handleGetSessionDetails(sessionProvider, args) { try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], 'get_session_details', sessionProvider, args.sessionId); const sessionId = args.sessionId; const logger = sessionProvider.getLogger(); logger.info(`[handleGetSessionDetails] Getting details for session: ${sessionId}`); const allSessions = sessionProvider.getAllSessionInfo(); const modelSessionInfo = allSessions.find((s) => s.id === sessionId); if (!modelSessionInfo) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`Debug session not found: ${sessionId}`)) .toolName('get_session_details') .mcpErrorCode(types_js_1.ErrorCode.InvalidRequest) .mcpDebugErrorType('session_not_found') .sessionProvider(sessionProvider) .sessionId(sessionId) .build(); } const result = { sessionInfo: modelSessionInfo, threads: [], callStack: [], breakpoints: [], pendingEventsForSession: sessionProvider.getEventsForSession(sessionId), }; const debugSession = sessionProvider.getDebugSession(sessionId); const dapHandler = sessionProvider.getDAPSessionHandler(sessionId); if (debugSession && dapHandler) { try { const threadsFromDapHandler = dapHandler.getThreads(); result.threads = threadsFromDapHandler.map((t) => ({ id: t.id, name: t.name, })); if (threadsFromDapHandler && threadsFromDapHandler.length > 0) { let stoppedThreadId = undefined; for (const debugThread of threadsFromDapHandler) { if (debugThread.stoppedDetails) { stoppedThreadId = debugThread.id; logger.info(`[handleGetSessionDetails] Thread ${stoppedThreadId} is stopped.`); break; } } if (stoppedThreadId !== undefined) { logger.info(`[handleGetSessionDetails] Session ${sessionId} has a stopped thread (${stoppedThreadId}). Attempting to get call stack.`); const callStackFromDebugSession = await debugSession.getCallStack(stoppedThreadId); result.callStack = callStackFromDebugSession.map((frame) => { const modelFrame = mapDebuggerStackFrameToModel(frame, stoppedThreadId); return modelFrame; }); } } } catch (err) { logger.warn(`[handleGetSessionDetails] Error fetching dynamic details for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`); // Do not re-throw; return basic info if dynamic parts fail } } else { logger.info(`[handleGetSessionDetails] No active debugSession or dapHandler for session ${sessionId}, returning basic info.`); } return result; } catch (error) { if (error instanceof types_js_1.McpError) throw error; const toolName = 'get_session_details'; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } async function handleStepIn(sessionProvider, args) { const toolName = 'step_in'; try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], toolName, sessionProvider, args.sessionId); const localSessionId = args.sessionId; const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, localSessionId, toolName); const dapHandler = sessionProvider.getDAPSessionHandler(localSessionId); if (!dapHandler) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`DAPSessionHandler not available for session ${localSessionId} in ${toolName}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('dap_handler_not_found') .sessionProvider(sessionProvider) .sessionId(localSessionId) .build(); } const targetThreadId = await _resolveTargetThreadId(sessionProvider, localSessionId, args.threadId, toolName, true); _ensureThreadIsStopped(dapHandler, targetThreadId, toolName, localSessionId, sessionProvider); const stepResult = await _performStepOperation(toolName, debugSession, dapHandler, targetThreadId, sessionProvider, localSessionId, async (thrId) => debugSession.stepInto(thrId)); return { success: !!stepResult.stoppedEvent && !stepResult.error && !stepResult.terminatedEvent, ...stepResult, }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } async function handleStepOut(sessionProvider, args) { const toolName = 'step_out'; try { validateRequiredToolArgs(args, [{ key: 'sessionId', type: 'string' }], toolName, sessionProvider, args.sessionId); const localSessionId = args.sessionId; const debugSession = await getActiveDebugSessionOrThrow(sessionProvider, localSessionId, toolName); const dapHandler = sessionProvider.getDAPSessionHandler(localSessionId); if (!dapHandler) { throw new errorUtils_1.McpErrorBuilder() .error(new Error(`DAPSessionHandler not available for session ${localSessionId} in ${toolName}`)) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('dap_handler_not_found') .sessionProvider(sessionProvider) .sessionId(localSessionId) .build(); } const targetThreadId = await _resolveTargetThreadId(sessionProvider, localSessionId, args.threadId, toolName, true); _ensureThreadIsStopped(dapHandler, targetThreadId, toolName, localSessionId, sessionProvider); const stepResult = await _performStepOperation(toolName, debugSession, dapHandler, targetThreadId, sessionProvider, localSessionId, async (thrId) => debugSession.stepOut(thrId)); return { success: !!stepResult.stoppedEvent && !stepResult.error && !stepResult.terminatedEvent, ...stepResult, }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName(toolName) .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .sessionId(args.sessionId) .build(); } } async function handleListAdapters(sessionProvider, _args) { try { const edcSessionManager = sessionProvider.getSessionManager(); const configsMapToUse = edcSessionManager.adapterConfigurations; if (!configsMapToUse || !(configsMapToUse instanceof Map)) { sessionProvider .getLogger() .warn('[handleListAdapters] Could not retrieve adapter configurations from SessionManager or it is not a Map.'); return { adapters: [] }; } const adapters = []; configsMapToUse.forEach((config, type) => { adapters.push({ type, config: { command: config.command, args: config.args, env: config.env, cwd: config.cwd, }, }); }); return { adapters }; } catch (error) { if (error instanceof types_js_1.McpError) throw error; throw new errorUtils_1.McpErrorBuilder() .error(error) .toolName('list_adapters') .mcpErrorCode(types_js_1.ErrorCode.InternalError) .mcpDebugErrorType('tool_internal_error') .sessionProvider(sessionProvider) .build(); } }