Skip to main content
Glama

Watchtower DAP Windows Debugging

by rlaksana
nodejs-adapter.ts18.6 kB
import { EventEmitter } from 'events'; import { Logger } from '../server/logger'; import { MetricsCollector } from '../server/metrics'; import { DapStdioTransport } from '../transport/stdio-transport'; import { DapStartRequest, DapAttachRequest, DapContinueRequest, DapPauseRequest, DapStepInRequest, DapStepOverRequest, DapStepOutRequest, DapThreadsRequest, DapStackTraceRequest, DapScopesRequest, DapVariablesRequest, DapEvaluateRequest, DebugEvent, } from '../schemas/index'; import { createError } from '../server/error-taxonomy'; /** * Node.js Debug Adapter * * Implements debugging adapter for Node.js applications using * the vscode-js-debug adapter (Node Debug). */ export class NodeJSAdapter extends EventEmitter { private logger: Logger; private metrics: MetricsCollector; private transport: DapStdioTransport; private sessionId: string | null = null; private processId: number | null = null; private isAttached: boolean = false; private capabilities: Record<string, any> = {}; private breakpoints: Map<string, number[]> = new Map(); private lastEventId: number = 0; // private _currentThreadId: number | null = null; // Unused for now constructor() { super(); this.logger = new Logger('NodeJSAdapter'); this.metrics = MetricsCollector.getInstance(); this.transport = new DapStdioTransport(); this.setupTransportHandlers(); } /** * Initialize the adapter */ async initialize(): Promise<Record<string, any>> { this.logger.info('Initializing Node.js debug adapter'); try { // Discover and validate Node.js adapter const adapterRegistry = await import('../adapters/registry').then(m => m.createAdapterRegistry() ); const jsDebugAdapter = await adapterRegistry.getAdapter('vscode-js-debug'); if (!jsDebugAdapter) { throw createError('ADAPTER_001', { adapterType: 'vscode-js-debug' }); } // Set up transport const adapterPath = jsDebugAdapter.path; await this.transport.connect(['node', adapterPath]); // Negotiate capabilities this.capabilities = await this.negotiateCapabilities(); this.logger.info('Node.js debug adapter initialized successfully'); this.metrics.increment('nodejs.adapter.initialize.count'); return this.capabilities; } catch (error) { this.logger.error('Failed to initialize Node.js debug adapter:', error); throw createError('ADAPTER_003', { error: (error as Error).message }); } } /** * Handle launch request */ async launch(config: DapStartRequest): Promise<{ sessionId: string }> { this.logger.info('Handling Node.js launch request', { program: config.arguments.program }); try { this.sessionId = `nodejs_${Date.now()}`; // Validate configuration this.validateLaunchConfig(config); // Set up transport await this.setupTransport(); // Send initialize request await this.sendInitializeRequest(); // Send launch request await this.sendLaunchRequest(config); // Wait for adapter to be ready await this.waitForReady(); this.logger.info('Node.js launch completed successfully'); this.metrics.increment('nodejs.launch.count'); return { sessionId: this.sessionId }; } catch (error) { this.logger.error('Node.js launch failed:', error); await this.cleanup(); throw error; } } /** * Handle attach request */ async attach(config: DapAttachRequest): Promise<{ sessionId: string }> { this.logger.info('Handling Node.js attach request', { processId: config.arguments.processId }); try { this.sessionId = `nodejs_${Date.now()}`; this.isAttached = true; // Validate configuration this.validateAttachConfig(config); // Set up transport await this.setupTransport(); // Send initialize request await this.sendInitializeRequest(); // Send attach request await this.sendAttachRequest(config); // Wait for adapter to be ready await this.waitForReady(); this.logger.info('Node.js attach completed successfully'); this.metrics.increment('nodejs.attach.count'); return { sessionId: this.sessionId }; } catch (error) { this.logger.error('Node.js attach failed:', error); await this.cleanup(); throw error; } } /** * Handle execution control requests */ async continue(request: DapContinueRequest): Promise<void> { await this.sendRequest('continue', { threadId: request.arguments.threadId, }); this.emit( 'event', this.createDebugEvent('continued', { threadId: request.arguments.threadId }) ); } async pause(request: DapPauseRequest): Promise<void> { await this.sendRequest('pause', { threadId: request.arguments.threadId, }); this.emit('event', this.createDebugEvent('paused', { threadId: request.arguments.threadId })); } async stepIn(request: DapStepInRequest): Promise<void> { await this.sendRequest('stepIn', { threadId: request.arguments.threadId, targetId: request.arguments.targetId, waitForAdditionalInformation: request.arguments.waitForAdditionalInformation, }); this.emit( 'event', this.createDebugEvent('steppedIn', { threadId: request.arguments.threadId }) ); } async stepOver(request: DapStepOverRequest): Promise<void> { await this.sendRequest('stepOver', { threadId: request.arguments.threadId, waitForAdditionalInformation: request.arguments.waitForAdditionalInformation, }); this.emit( 'event', this.createDebugEvent('steppedOver', { threadId: request.arguments.threadId }) ); } async stepOut(request: DapStepOutRequest): Promise<void> { await this.sendRequest('stepOut', { threadId: request.arguments.threadId, waitForAdditionalInformation: request.arguments.waitForAdditionalInformation, }); this.emit( 'event', this.createDebugEvent('steppedOut', { threadId: request.arguments.threadId }) ); } /** * Handle thread and stack requests */ async threads(_request: DapThreadsRequest): Promise<any> { return await this.sendRequest('threads', {}); } async stackTrace(request: DapStackTraceRequest): Promise<any> { return await this.sendRequest('stackTrace', { threadId: request.arguments.threadId, startFrame: request.arguments.startFrame, levels: request.arguments.levels, }); } async scopes(request: DapScopesRequest): Promise<any> { return await this.sendRequest('scopes', { frameId: request.arguments.frameId, }); } async variables(request: DapVariablesRequest): Promise<any> { return await this.sendRequest('variables', { variablesReference: request.arguments.variablesReference, filter: request.arguments.filter, start: request.arguments.start, count: request.arguments.count, }); } async evaluate(request: DapEvaluateRequest): Promise<any> { return await this.sendRequest('evaluate', { expression: request.arguments.expression, frameId: request.arguments.frameId, context: request.arguments.context, }); } /** * Set breakpoints */ async setBreakpoints(sourcePath: string, lines: number[]): Promise<any> { const source = { path: sourcePath }; const breakpoints = lines.map(line => ({ line, column: 0, verified: false, })); const response = await this.sendRequest('setBreakpoints', { source, breakpoints, }); // Update local breakpoints tracking this.breakpoints.set(sourcePath, lines); this.emit( 'event', this.createDebugEvent('breakpointSet', { sourcePath, lines, verified: response.body?.breakpoints?.map((bp: any) => bp.verified) || [], }) ); return response; } /** * Terminate debugging session */ async terminate(): Promise<void> { this.logger.info('Terminating Node.js debugging session'); try { if (this.transport.connected) { await this.sendRequest('terminate', {}); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for cleanup } await this.cleanup(); this.logger.info('Node.js debugging session terminated'); this.metrics.increment('nodejs.terminate.count'); } catch (error) { this.logger.warn('Error during termination:', error); await this.cleanup(); throw error; } } /** * Disconnect debugging session */ async disconnect(): Promise<void> { this.logger.info('Disconnecting Node.js debugging session'); try { if (this.transport.connected) { await this.sendRequest('disconnect', { restart: false, terminateDebuggee: false, suspendDebuggee: false, }); } await this.cleanup(); this.logger.info('Node.js debugging session disconnected'); this.metrics.increment('nodejs.disconnect.count'); } catch (error) { this.logger.warn('Error during disconnect:', error); await this.cleanup(); throw error; } } /** * Get session information */ getSessionInfo(): { sessionId: string | null; processId: number | null; isAttached: boolean; capabilities: Record<string, any>; } { return { sessionId: this.sessionId, processId: this.processId, isAttached: this.isAttached, capabilities: this.capabilities, }; } /** * Setup transport event handlers */ private setupTransportHandlers(): void { this.transport.on('event', (event: string, body: any) => { this.handleDapEvent(event, body); }); this.transport.on('error', (error: Error) => { this.logger.error('Transport error:', error); this.emit('error', error); }); this.transport.on('disconnected', () => { this.logger.info('Transport disconnected'); this.emit('disconnected'); }); } /** * Setup transport for Node.js debugging */ private async setupTransport(): Promise<void> { // Additional Node.js specific setup if needed this.logger.debug('Setting up Node.js debug transport'); } /** * Negotiate capabilities */ private async negotiateCapabilities(): Promise<Record<string, any>> { const capabilities = { supportsConfigurationDoneRequest: true, supportsEvaluateForHovers: true, supportsSetVariable: true, supportsRestartRequest: true, supportsStepBack: false, supportsDataBreakpoints: false, supportsInstructionBreakpoints: false, supportsCompletionsQuery: true, supportsModulesQuery: true, supportsGotoTargetsRequest: true, supportsLoadedSourcesRequest: true, supportsTerminateDebuggee: true, supportsTerminateRequest: true, supportsDelayedStackTraceLoading: false, supportsConditionalBreakpoints: true, supportsHitConditionalBreakpoints: true, supportsLogPoints: true, supportsExceptionInfoRequest: true, supportsSetExpression: false, supportsTerminateThreadsRequest: false, supportsSetFunctionBreakpoints: false, supportsThreadNamesRequest: true, supportsVariableType: true, supportsVariablePaging: true, supportsRunInTerminalRequest: false, supportsMemoryReferences: false, supportsArgsCanBeInterpretedByShell: false, supportsProgressReporting: false, supportsInvalidatedEvent: true, supportsMemoryEvent: false, }; return capabilities; } /** * Validate launch configuration */ private validateLaunchConfig(config: DapStartRequest): void { if (!config.arguments.program) { throw new Error('program is required for Node.js launch debugging'); } // Validate program path const fs = require('fs'); if (!fs.existsSync(config.arguments.program)) { throw new Error(`Program file not found: ${config.arguments.program}`); } // Validate working directory if provided if (config.arguments.cwd) { if (!fs.existsSync(config.arguments.cwd)) { throw new Error(`Working directory not found: ${config.arguments.cwd}`); } } } /** * Validate attach configuration */ private validateAttachConfig(config: DapAttachRequest): void { if (!config.arguments.processId && !config.arguments.address) { throw new Error('processId or address is required for Node.js attach debugging'); } if (config.arguments.processId && typeof config.arguments.processId !== 'number') { throw new Error('processId must be a number'); } } /** * Send initialize request */ private async sendInitializeRequest(): Promise<void> { await this.transport.sendRequest('initialize', { clientID: 'watchtower', clientName: 'Watchtower MCP Server', adapterID: 'nodejs-debug', locale: 'en-us', linesStartAt1: true, columnsStartAt1: true, pathFormat: 'path', supportsVariableType: true, supportsVariablePaging: true, supportsRunInTerminalRequest: true, supportsMemoryReferences: false, supportsArgsCanBeInterpretedByShell: false, supportsProgressReporting: false, supportsInvalidatedEvent: true, supportsMemoryEvent: false, }); } /** * Send launch request */ private async sendLaunchRequest(config: DapStartRequest): Promise<void> { const launchArgs = { ...config.arguments, // Node.js specific arguments runtimeExecutable: config.arguments.runtimeExecutable || 'node', runtimeArgs: config.arguments.runtimeArgs || [], sourceMaps: config.arguments.sourceMaps !== undefined ? config.arguments.sourceMaps : true, outFiles: config.arguments.outFiles || [], resolveSourceMapLocations: config.arguments.resolveSourceMapLocations || [], skipFiles: config.arguments.skipFiles || [], }; await this.transport.sendRequest('launch', launchArgs); } /** * Send attach request */ private async sendAttachRequest(config: DapAttachRequest): Promise<void> { const attachArgs = { ...config.arguments, // Node.js specific arguments sourceMaps: config.arguments.sourceMaps !== undefined ? config.arguments.sourceMaps : true, skipFiles: config.arguments.skipFiles || [], }; await this.transport.sendRequest('attach', attachArgs); } /** * Wait for adapter to be ready */ private async waitForReady(): Promise<void> { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout waiting for Node.js debug adapter to be ready')); }, 10000); const checkReady = () => { if (this.transport.connected) { clearTimeout(timeout); resolve(); } else { setTimeout(checkReady, 100); } }; checkReady(); }); } /** * Send DAP request */ private async sendRequest(command: string, arguments_: any): Promise<any> { if (!this.transport.connected) { throw new Error('Transport not connected'); } return await this.transport.sendRequest(command, arguments_); } /** * Handle DAP events from transport */ private handleDapEvent(event: string, body: any): void { this.logger.debug('Received DAP event:', { event, body }); // Convert to DebugEvent for consistency const debugEvent = this.createDebugEvent(event, body); this.emit('event', debugEvent); // Handle specific events switch (event) { case 'initialized': this.emit('initialized', body); break; case 'stopped': this.handleStoppedEvent(body); break; case 'continued': this.handleContinuedEvent(body); break; case 'thread': this.handleThreadEvent(body); break; case 'output': this.handleOutputEvent(body); break; case 'breakpoint': this.handleBreakpointEvent(body); break; case 'exited': this.handleExitedEvent(body); break; case 'terminated': this.handleTerminatedEvent(body); break; case 'module': this.handleModuleEvent(body); break; } } /** * Handle stopped event */ private handleStoppedEvent(body: any): void { // this._currentThreadId = body.threadId; // Unused for now this.lastEventId++; this.emit('stopped', body); } /** * Handle continued event */ private handleContinuedEvent(body: any): void { // this._currentThreadId = null; // Unused for now this.emit('continued', body); } /** * Handle thread event */ private handleThreadEvent(body: any): void { this.emit('threadEvent', body); } /** * Handle output event */ private handleOutputEvent(body: any): void { this.emit('output', body); } /** * Handle breakpoint event */ private handleBreakpointEvent(body: any): void { this.emit('breakpointEvent', body); } /** * Handle exited event */ private handleExitedEvent(body: any): void { this.emit('exited', body); } /** * Handle terminated event */ private handleTerminatedEvent(body: any): void { this.emit('terminated', body); } /** * Handle module event */ private handleModuleEvent(body: any): void { this.emit('moduleEvent', body); } /** * Create debug event */ private createDebugEvent(event: string, data: any): DebugEvent { return { event_id: `nodejs_${this.lastEventId++}`, session_id: this.sessionId || 'unknown', timestamp: Date.now(), event_type: event as any, // Type assertion to allow string data, }; } /** * Cleanup resources */ private async cleanup(): Promise<void> { this.sessionId = null; this.processId = null; this.isAttached = false; this.breakpoints.clear(); // this._currentThreadId = null; // Unused for now this.lastEventId = 0; if (this.transport.connected) { try { await this.transport.disconnect(); } catch (error) { this.logger.warn('Error during transport cleanup:', error); } } this.removeAllListeners(); } } /** * Create Node.js adapter instance */ export function createNodeJSAdapter(): NodeJSAdapter { return new NodeJSAdapter(); }

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