Skip to main content
Glama
ooples

MCP Console Automation Server

TTYDProtocol.ts19.5 kB
import { spawn, ChildProcess } from 'child_process'; import { BaseProtocol } from '../core/BaseProtocol.js'; import { ConsoleSession, SessionOptions, ConsoleType, ConsoleOutput, } from '../types/index.js'; import { ProtocolCapabilities, SessionState, ErrorContext, ProtocolHealthStatus, ErrorRecoveryResult, ResourceUsage, } from '../core/IProtocol.js'; // TTYD Protocol connection options interface TTYDConnectionOptions extends SessionOptions { // Basic TTYD Configuration executable?: string; port?: number; interface?: string; indexFile?: string; socketPath?: string; terminalType?: string; // Authentication and Security credential?: string; userid?: string; gid?: string; enableBasicAuth?: boolean; sslCert?: string; sslKey?: string; sslCaCert?: string; verifySsl?: boolean; enableSsl?: boolean; forceSsl?: boolean; maxClients?: number; clientTimeOut?: number; // Terminal Configuration enableWriteable?: boolean; onceMode?: boolean; checkOrigin?: boolean; allowWriteableConnections?: boolean; maxConnectionsPerIp?: number; signalList?: string; reconnect?: number; // Logging and Debug debugLevel?: number; enableSyslog?: boolean; logFile?: string; accessLog?: string; // TTY Settings columns?: number; rows?: number; closeSignal?: string; titleFixed?: string; ptyPath?: string; // Advanced Options baseUrl?: string; urlArg?: boolean; ipv6Support?: boolean; keepAliveInterval?: number; pingInterval?: number; pongTimeout?: number; startSessionTimeout?: number; // Command and Shell shell?: string; shellCommand?: string; loginCommand?: string; homeDir?: string; loginShell?: boolean; // Environment Variables termEnv?: Record<string, string>; processEnvironment?: Record<string, string>; // Browser and Client browserCommand?: string; enableBrowser?: boolean; enableFullscreen?: boolean; terminalTheme?: string; fontFamily?: string; fontSize?: number; // WebSocket Configuration wsOrigin?: string; enableCors?: boolean; corsOrigin?: string; // Process Management enableKillAllOnExit?: boolean; gracefulShutdown?: boolean; processCleanup?: boolean; // Network and Performance bufferSize?: number; outputBufferSize?: number; enableCompression?: boolean; compressionLevel?: number; enableZlibDeflate?: boolean; enablePermessageDeflate?: boolean; // Monitoring and Health enableHealthCheck?: boolean; healthCheckPath?: string; enableMetrics?: boolean; metricsPath?: string; enableStatus?: boolean; statusPath?: string; // Security Headers enableSecurityHeaders?: boolean; cspDirectives?: string; enableHsts?: boolean; hstsMaxAge?: number; enableXssProtection?: boolean; enableFrameOptions?: boolean; enableContentTypeNosniff?: boolean; // Session Management enableSessionTimeout?: boolean; sessionTimeout?: number; enableIdleTimeout?: boolean; idleTimeout?: number; enableMaxSessions?: boolean; maxSessions?: number; // File and Directory Access enableFileAccess?: boolean; allowedDirectories?: string[]; restrictedDirectories?: string[]; enableUpload?: boolean; uploadPath?: string; enableDownload?: boolean; // Integration Options enableWebdav?: boolean; webdavPath?: string; enableFtp?: boolean; ftpPort?: number; enableSftp?: boolean; sftpPort?: number; // Customization customCss?: string; customJs?: string; customTitle?: string; customFavicon?: string; customLogo?: string; enableCustomization?: boolean; // Plugin and Extension Support enablePlugins?: boolean; pluginDirectory?: string; enableExtensions?: boolean; extensionPath?: string; // Proxy and Load Balancing enableProxy?: boolean; proxyUrl?: string; proxyTimeout?: number; enableLoadBalancing?: boolean; loadBalancerConfig?: string; // Backup and Recovery enableBackup?: boolean; backupPath?: string; backupInterval?: number; enableAutoRestore?: boolean; // Advanced Terminal Features enableScrollback?: boolean; scrollbackLines?: number; enableCopyPaste?: boolean; enableRightClick?: boolean; enableQuickEdit?: boolean; enableInsertMode?: boolean; // WebRTC and P2P enableWebrtc?: boolean; webrtcConfig?: string; enableP2p?: boolean; p2pConfig?: string; // Docker and Container Integration enableDocker?: boolean; dockerImage?: string; dockerContainer?: string; dockerNetwork?: string; enableKubernetes?: boolean; kubernetesConfig?: string; } /** * TTYD Protocol Implementation * * Provides web-based terminal access through ttyd server * Supports HTTP/HTTPS terminal servers, authentication, real-time terminal streaming, and web terminal management */ export class TTYDProtocol extends BaseProtocol { public readonly type: ConsoleType = 'web-terminal'; public readonly capabilities: ProtocolCapabilities; private ttydProcesses = new Map<string, ChildProcess>(); // Compatibility property for old ProtocolFactory interface public get healthStatus(): ProtocolHealthStatus { return { isHealthy: this.isInitialized, lastChecked: new Date(), errors: [], warnings: [], metrics: { activeSessions: this.sessions.size, totalSessions: this.sessions.size, averageLatency: 0, successRate: 100, uptime: 0, }, dependencies: {}, }; } constructor() { super('ttyd'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: false, supportsPortForwarding: true, supportsAuthentication: true, supportsEncryption: true, supportsCompression: true, supportsMultiplexing: true, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: true, supportsPTY: true, maxConcurrentSessions: 1000, // TTYD can handle many web terminal sessions defaultTimeout: 30000, // Web terminal operations supportedEncodings: ['utf-8'], supportedAuthMethods: ['basic', 'bearer', 'custom'], platformSupport: { windows: true, // TTYD supports Windows through WSL/MinGW linux: true, macos: true, freebsd: true, }, }; } async initialize(): Promise<void> { if (this.isInitialized) return; try { // Check if TTYD is available await this.checkTtydAvailability(); this.isInitialized = true; this.logger.info('TTYD protocol initialized with web terminal features'); } catch (error: any) { this.logger.error('Failed to initialize TTYD protocol', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { const sessionId = `ttyd-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; return await this.createSessionWithTypeDetection(sessionId, options); } async dispose(): Promise<void> { await this.cleanup(); } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const fullCommand = args && args.length > 0 ? `${command} ${args.join(' ')}` : command; await this.sendInput(sessionId, fullCommand + '\n'); } async sendInput(sessionId: string, input: string): Promise<void> { const ttydProcess = this.ttydProcesses.get(sessionId); const session = this.sessions.get(sessionId); if (!ttydProcess || !ttydProcess.stdin || !session) { throw new Error(`No active TTYD session: ${sessionId}`); } ttydProcess.stdin.write(input); session.lastActivity = new Date(); this.emit('input-sent', { sessionId, input, timestamp: new Date(), }); this.logger.debug( `Sent input to TTYD session ${sessionId}: ${input.substring(0, 100)}` ); } async closeSession(sessionId: string): Promise<void> { try { const ttydProcess = this.ttydProcesses.get(sessionId); if (ttydProcess) { // Try graceful shutdown first ttydProcess.kill('SIGTERM'); // Force kill after timeout setTimeout(() => { if (ttydProcess && !ttydProcess.killed) { ttydProcess.kill('SIGKILL'); } }, 10000); this.ttydProcesses.delete(sessionId); } // Clean up base protocol session this.sessions.delete(sessionId); this.logger.info(`TTYD session ${sessionId} closed`); this.emit('session-closed', sessionId); } catch (error) { this.logger.error(`Error closing TTYD session ${sessionId}:`, error); throw error; } } async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const ttydOptions = options as TTYDConnectionOptions; // Build TTYD command const ttydCommand = this.buildTtydCommand(ttydOptions); // Spawn TTYD process const ttydProcess = spawn(ttydCommand[0], ttydCommand.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(ttydOptions), ...options.env, }, }); // Set up output handling ttydProcess.stdout?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); ttydProcess.stderr?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); ttydProcess.on('error', (error) => { this.logger.error(`TTYD process error for session ${sessionId}:`, error); this.emit('session-error', { sessionId, error }); }); ttydProcess.on('close', (code) => { this.logger.info( `TTYD process closed for session ${sessionId} with code ${code}` ); this.markSessionComplete(sessionId, code || 0); }); // Store the process this.ttydProcesses.set(sessionId, ttydProcess); // Create session object const session: ConsoleSession = { id: sessionId, command: ttydCommand[0], args: ttydCommand.slice(1), cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(ttydOptions), ...options.env, }, createdAt: new Date(), lastActivity: new Date(), status: 'running', type: this.type, streaming: options.streaming, executionState: 'idle', activeCommands: new Map(), pid: ttydProcess.pid, }; this.sessions.set(sessionId, session); this.logger.info( `TTYD session ${sessionId} created on port ${ttydOptions.port || 7681}` ); this.emit('session-created', { sessionId, type: 'web-terminal', session }); return session; } // Override getOutput to satisfy old ProtocolFactory interface (returns string) async getOutput(sessionId: string, since?: Date): Promise<any> { const outputs = await super.getOutput(sessionId, since); return outputs.map((output) => output.data).join(''); } // Missing IProtocol methods for compatibility getAllSessions(): ConsoleSession[] { return Array.from(this.sessions.values()); } getActiveSessions(): ConsoleSession[] { return Array.from(this.sessions.values()).filter( (session) => session.status === 'running' ); } getSessionCount(): number { return this.sessions.size; } async getSessionState(sessionId: string): Promise<SessionState> { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } return { sessionId, status: session.status, isOneShot: false, // TTYD sessions are typically persistent isPersistent: true, createdAt: session.createdAt, lastActivity: session.lastActivity, pid: session.pid, metadata: {}, }; } async handleError( error: Error, context: ErrorContext ): Promise<ErrorRecoveryResult> { this.logger.error( `Error in TTYD session ${context.sessionId}: ${error.message}` ); return { recovered: false, strategy: 'none', attempts: 0, duration: 0, error: error.message, }; } async recoverSession(sessionId: string): Promise<boolean> { const ttydProcess = this.ttydProcesses.get(sessionId); return (ttydProcess && !ttydProcess.killed) || false; } getResourceUsage(): ResourceUsage { const memUsage = process.memoryUsage(); const cpuUsage = process.cpuUsage(); return { memory: { used: memUsage.heapUsed, available: memUsage.heapTotal, peak: memUsage.heapTotal, }, cpu: { usage: cpuUsage.user + cpuUsage.system, load: [0, 0, 0], }, network: { bytesIn: 0, bytesOut: 0, connectionsActive: this.ttydProcesses.size, }, storage: { bytesRead: 0, bytesWritten: 0, }, sessions: { active: this.sessions.size, total: this.sessions.size, peak: this.sessions.size, }, }; } async getHealthStatus(): Promise<ProtocolHealthStatus> { const baseStatus = await super.getHealthStatus(); try { await this.checkTtydAvailability(); return { ...baseStatus, dependencies: { ttyd: { available: true }, }, }; } catch (error) { return { ...baseStatus, isHealthy: false, errors: [...baseStatus.errors, `TTYD not available: ${error}`], dependencies: { ttyd: { available: false }, }, }; } } private async checkTtydAvailability(): Promise<void> { return new Promise((resolve, reject) => { const testProcess = spawn('ttyd', ['--version'], { stdio: 'pipe' }); testProcess.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error('TTYD not found. Please install ttyd.')); } }); testProcess.on('error', () => { reject(new Error('TTYD not found. Please install ttyd.')); }); }); } private buildTtydCommand(options: TTYDConnectionOptions): string[] { const command = []; // TTYD executable if (options.executable) { command.push(options.executable); } else { command.push('ttyd'); } // Port configuration if (options.port) { command.push('-p', options.port.toString()); } // Interface binding if (options.interface) { command.push('-i', options.interface); } // Authentication if (options.credential) { command.push('-c', options.credential); } if (options.userid) { command.push('-u', options.userid); } if (options.gid) { command.push('-g', options.gid); } // SSL/TLS configuration if (options.enableSsl) { if (options.sslCert) { command.push('-C', options.sslCert); } if (options.sslKey) { command.push('-K', options.sslKey); } if (options.sslCaCert) { command.push('-A', options.sslCaCert); } } // Terminal configuration if (options.enableWriteable) { command.push('-W'); } if (options.onceMode) { command.push('-o'); } if (options.checkOrigin) { command.push('-O'); } if (options.maxClients) { command.push('-m', options.maxClients.toString()); } if (options.clientTimeOut) { command.push('-t', options.clientTimeOut.toString()); } // Terminal settings if (options.indexFile) { command.push('-I', options.indexFile); } if (options.socketPath) { command.push('-s', options.socketPath); } if (options.terminalType) { command.push('-T', options.terminalType); } // Debug and logging if (options.debugLevel) { command.push('-d', options.debugLevel.toString()); } if (options.enableSyslog) { command.push('-S'); } if (options.logFile) { command.push('-L', options.logFile); } if (options.accessLog) { command.push('-l', options.accessLog); } // Signal configuration if (options.signalList) { command.push('-a', options.signalList); } if (options.closeSignal) { command.push('-k', options.closeSignal); } // Reconnection support if (options.reconnect) { command.push('-r', options.reconnect.toString()); } // Base URL if (options.baseUrl) { command.push('-b', options.baseUrl); } // URL arguments if (options.urlArg) { command.push('-R'); } // IPv6 support if (options.ipv6Support) { command.push('-6'); } // Keep alive settings if (options.keepAliveInterval) { command.push('-P', options.keepAliveInterval.toString()); } // Terminal title if (options.titleFixed) { command.push('-T', options.titleFixed); } // PTY path if (options.ptyPath) { command.push('-E', options.ptyPath); } // Shell command if (options.shell) { command.push(options.shell); } else if (options.shellCommand) { command.push(options.shellCommand); } else { // Default shell if (process.platform === 'win32') { command.push('cmd'); } else { command.push('bash'); } } // Command arguments if (options.args) { command.push(...options.args); } return command; } private buildEnvironment( options: TTYDConnectionOptions ): Record<string, string> { const env: Record<string, string> = {}; // Terminal environment if (options.termEnv) { Object.assign(env, options.termEnv); } // Process environment if (options.processEnvironment) { Object.assign(env, options.processEnvironment); } // Terminal type if (options.terminalType) { env.TERM = options.terminalType; } // Terminal size if (options.columns) { env.COLUMNS = options.columns.toString(); } if (options.rows) { env.LINES = options.rows.toString(); } // Home directory if (options.homeDir) { env.HOME = options.homeDir; } // Shell configuration if (options.shell) { env.SHELL = options.shell; } // TTYD specific environment env.TTYD_SESSION = 'true'; return env; } async cleanup(): Promise<void> { this.logger.info('Cleaning up TTYD protocol'); // Close all TTYD processes for (const [sessionId, process] of Array.from(this.ttydProcesses)) { try { process.kill(); } catch (error) { this.logger.error( `Error killing TTYD process for session ${sessionId}:`, error ); } } // Clear all data this.ttydProcesses.clear(); // Call parent cleanup await super.cleanup(); } } export default TTYDProtocol;

Latest Blog Posts

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/ooples/mcp-console-automation'

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