Skip to main content
Glama

Watchtower DAP Windows Debugging

by rlaksana
stdio-transport.ts10.3 kB
import { EventEmitter } from 'events'; import { spawn } from 'child_process'; import { Logger } from '../server/logger'; import { MetricsCollector } from '../server/metrics'; import { ConfigManager } from '../server/config'; /** * Debug Adapter Protocol (DAP) Stdio Transport * * Implements the DAP stdio transport with Content-Length framing * as specified in the VS Code Debug Protocol. */ export class DapStdioTransport extends EventEmitter { private logger: Logger; private metrics: MetricsCollector; private config: ConfigManager; private process: any; private messageBuffer: string = ''; private seq: number = 1; private isConnected: boolean = false; private isConnecting: boolean = false; constructor() { super(); this.logger = new Logger('DapStdioTransport'); this.metrics = MetricsCollector.getInstance(); this.config = ConfigManager.getInstance(); } /** * Connect to debug adapter via stdio */ async connect(adapterCommand: string[], adapterEnv?: Record<string, string>): Promise<void> { if (this.isConnected || this.isConnecting) { throw new Error('Already connected or connecting'); } this.isConnecting = true; this.logger.info(`Connecting to debug adapter: ${adapterCommand.join(' ')}`); this.metrics.startTimer('dap.transport.connect'); try { // Start the debug adapter process this.process = spawn(adapterCommand[0] || '', adapterCommand.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ...adapterEnv, }, }); this.setupProcessHandlers(); // Wait for adapter to initialize await this.waitForAdapterInitialization(); this.isConnected = true; this.isConnecting = false; this.metrics.increment('dap.transport.connect.count'); this.metrics.stopTimer('dap.transport.connect'); this.logger.info('Debug adapter connected successfully'); this.emit('connected'); } catch (error) { this.cleanup(); this.isConnecting = false; this.logger.error('Failed to connect to debug adapter:', error); throw error; } } /** * Disconnect from debug adapter */ async disconnect(): Promise<void> { if (!this.isConnected) { this.logger.warn('Not connected to debug adapter'); return; } this.logger.info('Disconnecting from debug adapter'); this.metrics.startTimer('dap.transport.disconnect'); try { // Send terminate request await this.sendRequest('terminate', {}); // Wait a bit for graceful shutdown await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { this.logger.warn('Error during graceful disconnect:', error); } finally { this.cleanup(); this.isConnected = false; this.metrics.increment('dap.transport.disconnect.count'); this.metrics.stopTimer('dap.transport.disconnect'); this.logger.info('Debug adapter disconnected'); this.emit('disconnected'); } } /** * Send DAP request to debug adapter */ async sendRequest(command: string, arguments_: any = {}, seq?: number): Promise<any> { if (!this.isConnected) { throw new Error('Not connected to debug adapter'); } const requestSeq = seq || this.seq++; const request = { type: 'request', seq: requestSeq, command, arguments: arguments_, }; this.logger.debug(`Sending DAP request: ${command}`, { seq: requestSeq }); this.metrics.increment(`dap.${command.toLowerCase()}.count`); const message = this.formatMessage(request); this.process.stdin.write(message); return new Promise((resolve, reject) => { const timeout = setTimeout( () => { reject(new Error(`Timeout waiting for response to ${command}`)); }, this.config.get('debugging.defaultTimeout', 30000) ); const handler = (response: any) => { clearTimeout(timeout); this.off('response', handler); this.off('error', errorHandler); if (response.success) { resolve(response); } else { reject(new Error(response.message || 'DAP request failed')); } }; const errorHandler = (error: Error) => { clearTimeout(timeout); this.off('response', handler); this.off('error', errorHandler); reject(error); }; this.once('response', handler); this.once('error', errorHandler); }); } /** * Send DAP event to debug adapter */ sendEvent(event: string, body: any = {}): void { if (!this.isConnected) { this.logger.warn('Not connected to debug adapter'); return; } const eventMessage = { type: 'event', seq: this.seq++, event, body, }; this.logger.debug(`Sending DAP event: ${event}`); const message = this.formatMessage(eventMessage); this.process.stdin.write(message); } /** * Check if connected */ get connected(): boolean { return this.isConnected; } /** * Setup process event handlers */ private setupProcessHandlers(): void { if (!this.process) return; // Handle stdout data this.process.stdout.on('data', (data: Buffer) => { this.handleStdoutData(data.toString()); }); // Handle stderr data this.process.stderr.on('data', (data: Buffer) => { const message = data.toString().trim(); this.logger.warn('Debug adapter stderr:', message); this.emit('error', new Error(`Debug adapter error: ${message}`)); }); // Handle process exit this.process.on('exit', (code: number) => { this.logger.info(`Debug adapter process exited with code ${code}`); this.cleanup(); this.isConnected = false; this.emit('disconnected'); }); // Handle process error this.process.on('error', (error: Error) => { this.logger.error('Debug adapter process error:', error); this.emit('error', error); }); } /** * Handle incoming data from debug adapter stdout */ private handleStdoutData(data: string): void { this.messageBuffer += data; // Process complete messages while (this.messageBuffer.length > 0) { const contentLengthMatch = this.messageBuffer.match(/^Content-Length: (\d+)\r\n\r\n/); if (!contentLengthMatch) { // Wait for more data to complete the header break; } const contentLength = parseInt(contentLengthMatch[1] || '0', 10); const messageStart = this.messageBuffer.indexOf('\r\n\r\n') + 4; if (this.messageBuffer.length < messageStart + contentLength) { // Wait for more data to complete the message body break; } // Extract complete message const messageBody = this.messageBuffer.substring(messageStart, messageStart + contentLength); const remainder = this.messageBuffer.substring(messageStart + contentLength); // Update buffer this.messageBuffer = remainder; try { const message = JSON.parse(messageBody); this.handleMessage(message); } catch (error) { this.logger.error('Failed to parse DAP message:', error); this.emit('error', new Error('Invalid DAP message format')); } } } /** * Handle incoming DAP message */ private handleMessage(message: any): void { this.logger.debug('Received DAP message:', message); // Handle responses if (message.type === 'response') { this.emit('response', message); return; } // Handle events if (message.type === 'event') { this.emit('event', message.event, message.body); return; } // Handle requests (for bidirectional communication) if (message.type === 'request') { this.logger.warn('Received unexpected DAP request:', message); this.emit('error', new Error('Unexpected DAP request received')); return; } this.logger.warn('Unknown DAP message type:', message.type); } /** * Format message with Content-Length framing */ private formatMessage(message: any): string { const json = JSON.stringify(message); const contentLength = Buffer.byteLength(json, 'utf8'); return `Content-Length: ${contentLength}\r\n\r\n${json}`; } /** * Wait for adapter initialization */ private async waitForAdapterInitialization(): Promise<void> { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Adapter initialization timeout')); }, 10000); const checkInitialized = () => { if (this.process.exitCode !== undefined) { clearTimeout(timeout); reject(new Error('Adapter process exited during initialization')); return; } // Send an initialize request to test the connection this.sendRequest('initialize', { clientID: 'watchtower', clientName: 'Watchtower MCP Server', adapterID: 'watchtower', 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, }) .then(() => { clearTimeout(timeout); resolve(); }) .catch(error => { clearTimeout(timeout); reject(new Error(`Adapter initialization failed: ${error.message}`)); }); }; // Try to initialize immediately checkInitialized(); }); } /** * Cleanup resources */ private cleanup(): void { if (this.process) { this.process.kill('SIGTERM'); this.process = null; } this.messageBuffer = ''; this.seq = 1; this.isConnected = false; this.isConnecting = false; this.removeAllListeners(); } } /** * Create DAP stdio transport instance */ export function createDapStdioTransport(): DapStdioTransport { return new DapStdioTransport(); }

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