Skip to main content
Glama

Watchtower DAP Windows Debugging

by rlaksana
registry.ts13.1 kB
import { execa } from 'execa'; import { existsSync } from 'fs'; import { homedir } from 'os'; import { Logger } from '../server/logger'; import { ConfigManager } from '../server/config'; import { MetricsCollector } from '../server/metrics'; import { AdapterConfig, AdapterDiscovery, AdapterHealthCheck } from '../schemas/adapter.schemas'; import { createError } from '../server/error-taxonomy'; /** * Adapter Registry and Locator for Windows * * Discovers, validates, and manages Windows-specific debug adapters * with detailed diagnostics and user-friendly error messages. */ export class AdapterRegistry { private logger: Logger; private config: ConfigManager; private metrics: MetricsCollector; private cachedAdapters: Map<string, AdapterConfig> = new Map(); private healthChecks: Map<string, AdapterHealthCheck> = new Map(); private lastCacheUpdate: number = 0; constructor() { this.logger = new Logger('AdapterRegistry'); this.config = ConfigManager.getInstance(); this.metrics = MetricsCollector.getInstance(); } /** * Discover all available Windows debug adapters */ async discoverAdapters(): Promise<AdapterDiscovery> { this.logger.info('Discovering Windows debug adapters...'); this.metrics.startTimer('adapters.discover'); const discovery: AdapterDiscovery = { adapters: [], errors: [], missingAdapters: [], foundAdapters: [], }; try { const windowsAdapters = this.config.get('adapters.windows', {}); for (const [adapterType, config] of Object.entries(windowsAdapters)) { try { const adapter = await this.validateAdapter(adapterType, config as AdapterConfig); if (adapter) { discovery.adapters.push(adapter); discovery.foundAdapters.push(adapterType); this.cachedAdapters.set(adapterType, adapter); } else { discovery.missingAdapters.push(adapterType); } } catch (error) { this.logger.error(`Error validating ${adapterType} adapter:`, error); discovery.errors.push({ adapter: adapterType, error: (error as Error).message, suggestion: this.getAdapterSuggestion(adapterType, error as Error), }); } } // Update health checks await this.updateHealthChecks(discovery.adapters); this.metrics.increment('adapters.discover.count'); this.metrics.stopTimer('adapters.discover'); this.logger.info( `Discovery complete: ${discovery.foundAdapters.length}/${Object.keys(windowsAdapters).length} adapters found` ); } catch (error) { this.logger.error('Adapter discovery failed:', error); throw createError('ADAPTER_001'); } return discovery; } /** * Validate and get adapter configuration */ async getAdapter(adapterType: string): Promise<AdapterConfig | null> { // Check cache first const cached = this.cachedAdapters.get(adapterType); if (cached && Date.now() - this.lastCacheUpdate < 300000) { // 5 minute cache return cached; } // Re-discover if needed const discovery = await this.discoverAdapters(); return discovery.adapters.find(adapter => adapter.type === adapterType) || null; } /** * Validate a specific adapter configuration */ private async validateAdapter( type: string, config: AdapterConfig ): Promise<AdapterConfig | null> { this.logger.debug(`Validating ${type} adapter...`); // Check if adapter path exists and is executable let adapterPath = this.resolveAdapterPath(config.path) || ''; // Special handling for VS Code extensions if (type === 'vscode-js-debug') { adapterPath = (await this.resolveVSCodeJSDebug()) || ''; if (!adapterPath) { throw createError('ADAPTER_001', { adapterType: type }); } } // Verify the path exists if (!this.isPathValid(adapterPath)) { throw createError('ADAPTER_001', { adapterType: type, path: adapterPath }); } // Check version if available if (config.windows.versionRequirements) { await this.checkAdapterVersion( type, adapterPath, config.windows.versionRequirements as { min: string; recommended: string } ); } // Test basic launchability await this.testAdapterLaunch(type, adapterPath); // Return updated config with resolved path const validatedConfig: AdapterConfig = { ...config, path: adapterPath, name: config.name || type, }; this.logger.info(`✓ ${type} adapter validated successfully`); return validatedConfig; } /** * Resolve adapter path with environment variable expansion */ private resolveAdapterPath(path: string): string { if (!path) return ''; // Expand Windows environment variables let resolvedPath = path.replace(/%([^%]+)%/g, (match, varName) => { return process.env[varName] || match; }); // Expand user home directory resolvedPath = resolvedPath.replace(/%USERPROFILE%/gi, homedir()); resolvedPath = resolvedPath.replace(/%HOME%/gi, homedir()); return resolvedPath; } /** * Resolve VS Code JavaScript Debugger path */ private async resolveVSCodeJSDebug(): Promise<string | null> { const userExtPath = `${homedir()}\\.vscode\\extensions`; const path = this.resolveAdapterPath(userExtPath); if (!existsSync(path)) { this.logger.warn(`VS Code extensions directory not found: ${path}`); return null; } try { const fs = await import('fs'); const extensions = fs.readdirSync(path); // Look for js-debug extension const jsDebugExtensions = extensions.filter(ext => ext.startsWith('ms-vscode.js-debug')); if (jsDebugExtensions.length === 0) { this.logger.warn('JavaScript Debugger extension not found in VS Code extensions'); return null; } // Use the latest version const latestExtension = jsDebugExtensions.sort().pop()!; const extensionPath = `${path}\\${latestExtension}\\out\\node\\debugAdapter.js`; if (existsSync(extensionPath)) { this.logger.info(`Found VS Code JS Debug at: ${extensionPath}`); return extensionPath; } } catch (error) { this.logger.error('Error resolving VS Code JS Debug:', error); } return null; } /** * Check if path is valid and accessible */ private isPathValid(path: string): boolean { if (!path || path.trim() === '') { return false; } try { return existsSync(path); } catch (error) { return false; } } /** * Check adapter version requirements */ private async checkAdapterVersion( type: string, _path: string, requirements: { min: string; recommended: string } ): Promise<void> { // For most adapters, we'll just check existence // Version checking would be adapter-specific this.logger.debug(`Version checking for ${type} (min: ${requirements.min})`); // Placeholder for actual version checking logic // This would need to be implemented per adapter type } /** * Test adapter launch capability */ private async testAdapterLaunch(type: string, path: string): Promise<void> { this.logger.debug(`Testing launch for ${type}...`); // For VS Code JS Debug, test with Node.js if (type === 'vscode-js-debug') { try { await execa('node', [path, '--version']); } catch (error) { throw new Error(`Failed to launch ${type}: ${(error as Error).message}`); } return; } // For other adapters, just test basic file accessibility if (!existsSync(path)) { throw new Error(`Adapter executable not found: ${path}`); } // Test file readability try { await import('fs').then(fs => fs.promises.access(path, fs.constants.R_OK)); } catch (error) { throw new Error(`Cannot read adapter executable: ${path}`); } } /** * Get user-friendly suggestion for adapter errors */ private getAdapterSuggestion(type: string, error: Error): string { const suggestions: Record<string, string> = { vsdbg: 'Install Visual Studio 2022 with "Desktop development with C#" workload', netcoredbg: 'Install .NET SDK from https://dotnet.microsoft.com/download', 'vscode-js-debug': 'Install VS Code and JavaScript Debugger extension', debugpy: 'Install Python and run: pip install debugpy', dap: 'Install compatible DAP adapter and place in PATH', }; const baseSuggestion = suggestions[type] || `Install ${type} debug adapter`; return `${baseSuggestion}. Error details: ${error.message}`; } /** * Update health checks for all adapters */ private async updateHealthChecks(adapters: AdapterConfig[]): Promise<void> { for (const adapter of adapters) { const healthCheck = await this.performHealthCheck(adapter); this.healthChecks.set(adapter.type, healthCheck); } } /** * Perform health check on adapter */ async performHealthCheck(adapter: AdapterConfig): Promise<AdapterHealthCheck> { // const startTime = Date.now(); // Unused let status: 'healthy' | 'unhealthy' | 'timeout' | 'error' = 'healthy'; let message = 'Adapter is healthy'; const details: any = {}; try { // Check if path still exists if (!existsSync(adapter.path)) { status = 'unhealthy'; message = 'Adapter executable not found'; details.path = adapter.path; } else { // Test adapter launch with timeout const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error('Health check timeout')), 5000); }); const testPromise = this.testAdapterLaunch(adapter.type, adapter.path); await Promise.race([testPromise, timeoutPromise]); } } catch (error) { const errorMessage = (error as Error).message; status = errorMessage === 'Health check timeout' ? 'timeout' : 'error'; message = errorMessage; details.error = errorMessage; } return { adapter: adapter.type, status, message, details, lastCheck: Date.now(), }; } /** * Get health check status for adapter */ getHealthCheck(adapterType: string): AdapterHealthCheck | null { return this.healthChecks.get(adapterType) || null; } /** * Get all health checks */ getAllHealthChecks(): Map<string, AdapterHealthCheck> { return new Map(this.healthChecks); } /** * Clear adapter cache */ clearCache(): void { this.cachedAdapters.clear(); this.healthChecks.clear(); this.lastCacheUpdate = 0; this.logger.info('Adapter cache cleared'); } /** * Get Windows-specific adapter diagnostics */ getWindowsDiagnostics(): { adapters: { [type: string]: { found: boolean; path: string; health?: AdapterHealthCheck } }; systemInfo: { [key: string]: string }; recommendations: string[]; } { const diagnostics = { adapters: {} as { [type: string]: { found: boolean; path: string; health?: AdapterHealthCheck }; }, systemInfo: { platform: process.platform, arch: process.arch, nodeVersion: process.version, envPath: process.env['PATH']?.substring(0, 100) + '...', }, recommendations: [] as string[], }; // Get adapter information const windowsAdapters = this.config.get('adapters.windows', {}); for (const [type, config] of Object.entries(windowsAdapters)) { const healthCheck = this.getHealthCheck(type); diagnostics.adapters[type] = { found: healthCheck?.status === 'healthy', path: this.resolveAdapterPath((config as any).path || ''), health: healthCheck || { status: 'unhealthy' as const, message: 'Health check unavailable', adapter: type, lastCheck: Date.now(), }, }; } // Generate recommendations for (const [type, info] of Object.entries(diagnostics.adapters)) { if (!info.found) { switch (type) { case 'vsdbg': diagnostics.recommendations.push( 'Install Visual Studio with "Desktop development with C#" workload' ); break; case 'netcoredbg': diagnostics.recommendations.push( 'Install .NET SDK: https://dotnet.microsoft.com/download' ); break; case 'debugpy': diagnostics.recommendations.push('Install Python and run: pip install debugpy'); break; case 'vscode-js-debug': diagnostics.recommendations.push('Install VS Code with JavaScript Debugger extension'); break; } } } return diagnostics; } } /** * Create adapter registry instance */ export function createAdapterRegistry(): AdapterRegistry { return new AdapterRegistry(); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rlaksana/mcp-watchtower'

If you have feedback or need assistance with the MCP directory API, please join our Discord server