UNPKG

@debugmcp/mcp-debugger

Version:

Run-time step-through debugging for LLM agents.

651 lines (554 loc) 18.8 kB
/** * Python Debug Adapter implementation * * Provides Python-specific debugging functionality using debugpy. * Encapsulates all Python-specific logic including executable discovery, * environment validation, and debugpy integration. * * @since 2.0.0 */ import { EventEmitter } from 'events'; import { spawn } from 'child_process'; import { DebugProtocol } from '@vscode/debugprotocol'; import * as path from 'path'; import { IDebugAdapter, AdapterState, ValidationResult, ValidationError, ValidationWarning, DependencyInfo, AdapterCommand, AdapterConfig, GenericLaunchConfig, LanguageSpecificLaunchConfig, DebugFeature, FeatureRequirement, AdapterCapabilities, AdapterError, AdapterErrorCode, AdapterEvents } from '../debug-adapter-interface.js'; import { DebugLanguage } from '../../session/models.js'; import { AdapterDependencies } from '../adapter-registry-interface.js'; import { findPythonExecutable, getPythonVersion } from '../../utils/python-utils.js'; /** * Cache entry for Python executable paths */ interface PythonPathCacheEntry { path: string; timestamp: number; version?: string; hasDebugpy?: boolean; } /** * Python-specific launch configuration */ interface PythonLaunchConfig extends LanguageSpecificLaunchConfig { module?: string; // For -m module execution pythonArgs?: string[]; // Additional Python arguments console?: 'integratedTerminal' | 'internalConsole' | 'externalTerminal'; django?: boolean; // Django debugging support flask?: boolean; // Flask debugging support jinja?: boolean; // Jinja template debugging redirectOutput?: boolean; // Redirect output to debug console showReturnValue?: boolean; // Show function return values subProcess?: boolean; // Debug child processes } /** * Python Debug Adapter implementation */ export class PythonDebugAdapter extends EventEmitter implements IDebugAdapter { readonly language = DebugLanguage.PYTHON; readonly name = 'Python Debug Adapter'; private state: AdapterState = AdapterState.UNINITIALIZED; private dependencies: AdapterDependencies; // Caching private pythonPathCache = new Map<string, PythonPathCacheEntry>(); private readonly cacheTimeout = 60000; // 1 minute // State private currentThreadId: number | null = null; private connected = false; constructor(dependencies: AdapterDependencies) { super(); this.dependencies = dependencies; } // ===== Lifecycle Management ===== async initialize(): Promise<void> { this.transitionTo(AdapterState.INITIALIZING); try { // Validate environment const validation = await this.validateEnvironment(); if (!validation.valid) { this.transitionTo(AdapterState.ERROR); throw new AdapterError( validation.errors[0]?.message || 'Python environment validation failed', AdapterErrorCode.ENVIRONMENT_INVALID ); } this.transitionTo(AdapterState.READY); this.emit('initialized'); } catch (error) { this.transitionTo(AdapterState.ERROR); throw error; } } async dispose(): Promise<void> { this.pythonPathCache.clear(); this.currentThreadId = null; this.connected = false; this.state = AdapterState.UNINITIALIZED; this.emit('disposed'); } // ===== State Management ===== getState(): AdapterState { return this.state; } isReady(): boolean { return this.state === AdapterState.READY || this.state === AdapterState.CONNECTED || this.state === AdapterState.DEBUGGING; } getCurrentThreadId(): number | null { return this.currentThreadId; } private transitionTo(newState: AdapterState): void { const oldState = this.state; this.state = newState; this.emit('stateChanged', oldState, newState); } // ===== Environment Validation ===== async validateEnvironment(): Promise<ValidationResult> { const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; try { // Check Python executable const pythonPath = await this.resolveExecutablePath(); // Check Python version const version = await this.checkPythonVersion(pythonPath); if (version) { const [major, minor] = version.split('.').map(Number); if (major < 3 || (major === 3 && minor < 7)) { errors.push({ code: 'PYTHON_VERSION_TOO_OLD', message: `Python 3.7 or higher required. Current version: ${version}`, recoverable: false }); } } else { warnings.push({ code: 'PYTHON_VERSION_CHECK_FAILED', message: 'Could not determine Python version' }); } // Check debugpy installation const hasDebugpy = await this.checkDebugpyInstalled(pythonPath); if (!hasDebugpy) { errors.push({ code: 'DEBUGPY_NOT_INSTALLED', message: 'debugpy not installed. Run: pip install debugpy', recoverable: true }); } // Check if in virtual environment const isVenv = await this.detectVirtualEnv(pythonPath); if (isVenv) { this.dependencies.logger?.info('[PythonDebugAdapter] Virtual environment detected'); } } catch (error) { errors.push({ code: 'PYTHON_NOT_FOUND', message: error instanceof Error ? error.message : 'Python executable not found', recoverable: false }); } return { valid: errors.length === 0, errors, warnings }; } getRequiredDependencies(): DependencyInfo[] { return [ { name: 'Python', version: '3.7+', required: true, installCommand: 'Download from https://python.org' }, { name: 'debugpy', version: 'latest', required: true, installCommand: 'pip install debugpy' } ]; } // ===== Executable Management ===== async resolveExecutablePath(preferredPath?: string): Promise<string> { // Check cache first const cacheKey = preferredPath || 'default'; const cached = this.pythonPathCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { this.dependencies.logger?.debug(`[PythonDebugAdapter] Using cached Python path: ${cached.path}`); return cached.path; } // Find Python executable const pythonPath = await findPythonExecutable( preferredPath, this.dependencies.logger ); // Cache the result this.pythonPathCache.set(cacheKey, { path: pythonPath, timestamp: Date.now() }); return pythonPath; } getDefaultExecutableName(): string { switch (process.platform) { case 'win32': return 'py'; default: return 'python3'; } } getExecutableSearchPaths(): string[] { const paths: string[] = []; // Add common Python installation paths if (process.platform === 'win32') { paths.push( 'C:\\Python39', 'C:\\Python38', 'C:\\Python37', 'C:\\Program Files\\Python39', 'C:\\Program Files\\Python38', 'C:\\Program Files\\Python37', `${process.env.LOCALAPPDATA}\\Programs\\Python\\Python39`, `${process.env.LOCALAPPDATA}\\Programs\\Python\\Python38`, `${process.env.LOCALAPPDATA}\\Programs\\Python\\Python37` ); } else if (process.platform === 'darwin') { paths.push( '/usr/local/bin', '/opt/homebrew/bin', '/usr/bin' ); } else { paths.push( '/usr/bin', '/usr/local/bin', '/opt/python/bin' ); } // Add PATH directories if (process.env.PATH) { paths.push(...process.env.PATH.split(path.delimiter)); } return paths; } // ===== Adapter Configuration ===== buildAdapterCommand(config: AdapterConfig): AdapterCommand { return { command: config.executablePath, args: [ '-m', 'debugpy.adapter', '--host', config.adapterHost, '--port', config.adapterPort.toString() ], env: { ...process.env, PYTHONUNBUFFERED: '1', // Ensure unbuffered output DEBUGPY_LOG_DIR: config.logDir } }; } getAdapterModuleName(): string { return 'debugpy.adapter'; } getAdapterInstallCommand(): string { return 'pip install debugpy'; } // ===== Debug Configuration ===== transformLaunchConfig(config: GenericLaunchConfig): PythonLaunchConfig { const pythonConfig: PythonLaunchConfig = { ...config, type: 'python', request: 'launch', name: 'Python: Current File', console: 'integratedTerminal', redirectOutput: true, showReturnValue: true, justMyCode: config.justMyCode ?? true, stopOnEntry: config.stopOnEntry ?? false }; return pythonConfig; } getDefaultLaunchConfig(): Partial<GenericLaunchConfig> { return { stopOnEntry: false, justMyCode: true, env: {}, cwd: process.cwd() }; } // ===== DAP Protocol Operations ===== async sendDapRequest<T extends DebugProtocol.Response>( command: string, args?: unknown ): Promise<T> { // This will be handled by ProxyManager // Adapter just needs to validate the request is appropriate for Python // Validate Python-specific commands if (command === 'setExceptionBreakpoints' && args) { const exceptionArgs = args as DebugProtocol.SetExceptionBreakpointsArguments; // Ensure Python exception filters are valid const validFilters = ['raised', 'uncaught', 'userUnhandled']; const invalidFilters = exceptionArgs.filters?.filter(f => !validFilters.includes(f)); if (invalidFilters?.length) { throw new AdapterError( `Invalid Python exception filters: ${invalidFilters.join(', ')}`, AdapterErrorCode.INVALID_RESPONSE ); } } // ProxyManager will handle actual communication return {} as T; } handleDapEvent(event: DebugProtocol.Event): void { // Update thread ID on stopped events if (event.event === 'stopped' && event.body?.threadId) { this.currentThreadId = event.body.threadId; } this.emit(event.event as keyof AdapterEvents, event.body); } handleDapResponse(_response: DebugProtocol.Response): void { // Python adapter doesn't need special response handling void _response; } // ===== Connection Management ===== async connect(host: string, port: number): Promise<void> { // Connection is handled by ProxyManager // Store connection info for debugging purposes this.dependencies.logger?.debug(`[PythonDebugAdapter] Connect request to ${host}:${port}`); this.connected = true; this.transitionTo(AdapterState.CONNECTED); this.emit('connected'); } async disconnect(): Promise<void> { this.connected = false; this.currentThreadId = null; this.transitionTo(AdapterState.DISCONNECTED); this.emit('disconnected'); } isConnected(): boolean { return this.connected; } // ===== Error Handling ===== getInstallationInstructions(): string { return `Python Debugging Setup: 1. Install Python 3.7 or higher: - Windows: Download from https://python.org - macOS: brew install python3 - Linux: sudo apt install python3 python3-pip 2. Install debugpy: pip install debugpy 3. Verify installation: python -m debugpy --version For virtual environments: python -m venv myenv source myenv/bin/activate # On Windows: myenv\\Scripts\\activate pip install debugpy`; } getMissingExecutableError(): string { return `Python not found. Please ensure Python 3.7+ is installed and available in PATH. Windows users: Try 'py' command or install from https://python.org macOS users: Try 'brew install python3' Linux users: Try 'sudo apt install python3' You can also specify the Python path explicitly in your debug configuration.`; } translateErrorMessage(error: Error): string { const message = error.message.toLowerCase(); if (message.includes('debugpy') && message.includes('modulenotfounderror')) { return 'debugpy is not installed. Please run: pip install debugpy'; } if (message.includes('python') && message.includes('not found')) { return this.getMissingExecutableError(); } if (message.includes('permission denied')) { return `Permission denied accessing Python executable. Check file permissions.`; } if (message.includes('windows store')) { return `Windows Store Python alias detected. Please install Python from https://python.org`; } return error.message; } // ===== Feature Support ===== supportsFeature(feature: DebugFeature): boolean { const supportedFeatures = [ DebugFeature.CONDITIONAL_BREAKPOINTS, DebugFeature.FUNCTION_BREAKPOINTS, DebugFeature.EXCEPTION_BREAKPOINTS, DebugFeature.VARIABLE_PAGING, DebugFeature.EVALUATE_FOR_HOVERS, DebugFeature.SET_VARIABLE, DebugFeature.LOG_POINTS, DebugFeature.TERMINATE_REQUEST, DebugFeature.EXCEPTION_OPTIONS, DebugFeature.EXCEPTION_INFO_REQUEST ]; return supportedFeatures.includes(feature); } getFeatureRequirements(feature: DebugFeature): FeatureRequirement[] { const requirements: FeatureRequirement[] = []; switch (feature) { case DebugFeature.CONDITIONAL_BREAKPOINTS: requirements.push({ type: 'dependency', description: 'debugpy 1.0+', required: true }); break; case DebugFeature.LOG_POINTS: requirements.push({ type: 'version', description: 'debugpy 1.5+', required: true }); break; case DebugFeature.EXCEPTION_INFO_REQUEST: requirements.push({ type: 'version', description: 'Python 3.7+', required: true }); break; } return requirements; } getCapabilities(): AdapterCapabilities { return { supportsConfigurationDoneRequest: true, supportsFunctionBreakpoints: true, supportsConditionalBreakpoints: true, supportsHitConditionalBreakpoints: true, supportsEvaluateForHovers: true, exceptionBreakpointFilters: [ { filter: 'raised', label: 'Raised Exceptions', description: 'Break on all raised exceptions', default: false, supportsCondition: true }, { filter: 'uncaught', label: 'Uncaught Exceptions', description: 'Break on uncaught exceptions', default: true, supportsCondition: true }, { filter: 'userUnhandled', label: 'User Unhandled Exceptions', description: 'Break on exceptions not handled by user code', default: false, supportsCondition: true } ], supportsStepBack: false, supportsSetVariable: true, supportsRestartFrame: false, supportsGotoTargetsRequest: false, supportsStepInTargetsRequest: true, supportsCompletionsRequest: true, completionTriggerCharacters: ['.', '['], supportsModulesRequest: true, supportsRestartRequest: false, supportsExceptionOptions: true, supportsValueFormattingOptions: true, supportsExceptionInfoRequest: true, supportTerminateDebuggee: true, supportSuspendDebuggee: false, supportsDelayedStackTraceLoading: true, supportsLoadedSourcesRequest: true, supportsLogPoints: true, supportsTerminateThreadsRequest: false, supportsSetExpression: false, supportsTerminateRequest: true, supportsDataBreakpoints: false, supportsReadMemoryRequest: false, supportsWriteMemoryRequest: false, supportsDisassembleRequest: false, supportsCancelRequest: false, supportsBreakpointLocationsRequest: true, supportsClipboardContext: false, supportsSteppingGranularity: false, supportsInstructionBreakpoints: false, supportsExceptionFilterOptions: true, supportsSingleThreadExecutionRequests: false }; } // ===== Python-specific helper methods ===== /** * Check Python version */ private async checkPythonVersion(pythonPath: string): Promise<string | null> { // Check cache first const cached = this.pythonPathCache.get(pythonPath); if (cached?.version) { return cached.version; } const version = await getPythonVersion(pythonPath); // Update cache if (version && cached) { cached.version = version; } return version; } /** * Check if debugpy is installed */ private async checkDebugpyInstalled(pythonPath: string): Promise<boolean> { // Check cache first const cached = this.pythonPathCache.get(pythonPath); if (cached?.hasDebugpy !== undefined) { return cached.hasDebugpy; } return new Promise((resolve) => { const child = spawn(pythonPath, ['-c', 'import debugpy; print(debugpy.__version__)'], { stdio: ['ignore', 'pipe', 'pipe'] }); let output = ''; child.stdout?.on('data', (data) => { output += data.toString(); }); child.on('error', () => resolve(false)); child.on('exit', (code) => { const hasDebugpy = code === 0 && output.trim().length > 0; // Update cache if (cached) { cached.hasDebugpy = hasDebugpy; } if (hasDebugpy) { this.dependencies.logger?.info(`[PythonDebugAdapter] debugpy version: ${output.trim()}`); } resolve(hasDebugpy); }); }); } /** * Detect if Python is in a virtual environment */ private async detectVirtualEnv(pythonPath: string): Promise<boolean> { return new Promise((resolve) => { const child = spawn(pythonPath, ['-c', 'import sys; print(hasattr(sys, "real_prefix") or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix))'], { stdio: ['ignore', 'pipe', 'ignore'] }); let output = ''; child.stdout?.on('data', (data) => { output += data.toString(); }); child.on('error', () => resolve(false)); child.on('exit', () => { resolve(output.trim().toLowerCase() === 'true'); }); }); } }