Skip to main content
Glama
ooples

MCP Console Automation Server

SSHProtocol.ts13.1 kB
import { BaseProtocol } from '../core/BaseProtocol.js'; import { SSHAdapter, SSHOptions, createSSHSession, createSSHSessionWithHandlers, } from '../core/SSHAdapter.js'; import { ProtocolCapabilities, SessionState } from '../core/IProtocol.js'; import { ConsoleSession, ConsoleOutput, SessionOptions, SSHConnectionOptions, ConsoleType, } from '../types/index.js'; /** * SSH Protocol implementation for secure remote shell access * Uses the robust SSHAdapter for actual SSH connections with retry/recovery */ export class SSHProtocol extends BaseProtocol { public readonly type: ConsoleType = 'ssh'; public readonly capabilities: ProtocolCapabilities; private connectionPool: Map<string, SSHAdapter> = new Map(); private sessionAdapters: Map<string, SSHAdapter> = new Map(); constructor() { super('SSHProtocol'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: true, 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: 20, defaultTimeout: 30000, supportedEncodings: ['utf-8', 'ascii', 'binary'], supportedAuthMethods: ['password', 'publickey', 'keyboard-interactive'], platformSupport: { windows: true, linux: true, macos: true, freebsd: true, }, }; } async initialize(): Promise<void> { if (this.isInitialized) { return; } // SSH functionality available through SSHAdapter this.isInitialized = true; this.logger.info('SSH protocol initialized with session management fixes'); } async createSession(options: SessionOptions): Promise<ConsoleSession> { if (!this.isInitialized) { throw new Error('Protocol not initialized'); } const sessionId = `ssh-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; // Use session management fixes from BaseProtocol return await this.createSessionWithTypeDetection(sessionId, options); } protected async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { // CRITICAL DEBUG: Track execution flow const debugLog = (msg: string) => { try { console.error(`[SSH-DEBUG] ${msg}`); } catch (e) { // Ignore debug errors } }; debugLog('=== SSHProtocol.doCreateSession CALLED ==='); debugLog(`sessionId: ${sessionId}`); debugLog(`sessionState.isOneShot: ${sessionState.isOneShot}`); debugLog(`options.command: "${options.command}"`); debugLog(`options.args: ${JSON.stringify(options.args)}`); debugLog(`has sshOptions: ${!!options.sshOptions}`); try { // Validate SSH options if (!options.sshOptions) { debugLog('ERROR: No SSH options provided!'); throw new Error('SSH connection options required'); } // Convert to SSHAdapter options const sshOptions: SSHOptions = { host: options.sshOptions.host, port: options.sshOptions.port, username: options.sshOptions.username, password: options.sshOptions.password, privateKey: options.sshOptions.privateKey, strictHostKeyChecking: options.sshOptions.strictHostKeyChecking, timeout: options.timeout || this.capabilities.defaultTimeout, }; // Create SSH adapter for this session with immediate handler setup // This pattern eliminates race conditions by ensuring handlers are attached atomically const adapter = createSSHSessionWithHandlers( sshOptions, sessionId, (adapter) => this.setupAdapterHandlers(sessionId, adapter) ); // Store adapter after handlers are guaranteed to be set up this.sessionAdapters.set(sessionId, adapter); // SSH always needs connection, regardless of one-shot or persistent debugLog( `CONNECTING for ${sessionState.isOneShot ? 'one-shot' : 'persistent'} session` ); debugLog( `SSH Options: ${JSON.stringify({ host: sshOptions.host, hasPassword: !!sshOptions.password, hasPrivateKey: !!sshOptions.privateKey, })}` ); await adapter.connect(sshOptions); debugLog('>>> adapter.connect() COMPLETED <<<'); const session: ConsoleSession = { id: sessionId, command: options.command || 'ssh', args: options.args || [], cwd: options.cwd || '~', env: options.env || {}, createdAt: new Date(), status: sessionState.isOneShot ? 'initializing' : 'running', type: this.type, streaming: options.streaming ?? false, sshOptions: options.sshOptions, executionState: 'idle', activeCommands: new Map(), lastActivity: new Date(), pid: undefined, // SSH sessions don't have local PIDs }; this.sessions.set(sessionId, session); this.outputBuffers.set(sessionId, []); debugLog( `SSH session ${sessionId} created successfully (${sessionState.isOneShot ? 'one-shot' : 'persistent'})` ); this.logger.info( `SSH session ${sessionId} created (${sessionState.isOneShot ? 'one-shot' : 'persistent'})` ); return session; } catch (error) { try { console.error(`[SSH-DEBUG] ERROR in doCreateSession: ${error}`); console.error(`[SSH-DEBUG] Error stack: ${(error as Error).stack}`); } catch (e) { // Ignore debug errors } this.logger.error(`Failed to create SSH session: ${error}`); // Cleanup on failure const adapter = this.sessionAdapters.get(sessionId); if (adapter) { adapter.destroy(); this.sessionAdapters.delete(sessionId); } throw error; } } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const adapter = this.sessionAdapters.get(sessionId); const sessionState = await this.getSessionState(sessionId); if (!adapter) { throw new Error(`SSH adapter for session ${sessionId} not found`); } try { // For one-shot sessions, connect first if not already connected if (sessionState.isOneShot && !adapter.isActive()) { const session = this.sessions.get(sessionId); if (session?.sshOptions) { const sshOptions: SSHOptions = { host: session.sshOptions.host, port: session.sshOptions.port, username: session.sshOptions.username, password: session.sshOptions.password, privateKey: session.sshOptions.privateKey, strictHostKeyChecking: session.sshOptions.strictHostKeyChecking, timeout: this.capabilities.defaultTimeout, }; await adapter.connect(sshOptions); } } // Build full command const fullCommand = args ? `${command} ${args.join(' ')}` : command; // Send command via SSH adapter await adapter.sendCommand(fullCommand); // For one-shot sessions, mark as complete when command is sent if (sessionState.isOneShot) { setTimeout(() => { this.markSessionComplete(sessionId, 0); }, 1000); // Give time for output to be captured } this.emit('commandExecuted', { sessionId, command: fullCommand, timestamp: new Date(), }); } catch (error) { this.logger.error(`Failed to execute SSH command: ${error}`); throw error; } } async sendInput(sessionId: string, input: string): Promise<void> { const adapter = this.sessionAdapters.get(sessionId); if (!adapter) { throw new Error(`SSH adapter for session ${sessionId} not found`); } try { // Check if this is a password input if (input.includes('\n') && !input.trim().includes(' ')) { await adapter.sendPassword(input.trim()); } else { await adapter.sendCommand(input); } this.emit('inputSent', { sessionId, input, timestamp: new Date() }); } catch (error) { this.logger.error(`Failed to send SSH input: ${error}`); throw error; } } async closeSession(sessionId: string): Promise<void> { const adapter = this.sessionAdapters.get(sessionId); if (adapter) { adapter.destroy(); this.sessionAdapters.delete(sessionId); } // Remove from base class tracking this.sessions.delete(sessionId); this.outputBuffers.delete(sessionId); this.emit('sessionClosed', sessionId); } async dispose(): Promise<void> { this.logger.info('Disposing SSH protocol'); // Close all SSH adapters for (const sessionId of this.sessionAdapters.keys()) { const adapter = this.sessionAdapters.get(sessionId); if (adapter) { adapter.destroy(); } } this.sessionAdapters.clear(); this.connectionPool.clear(); await this.cleanup(); } /** * Setup event handlers for SSH adapter with error-safe patterns */ private setupAdapterHandlers(sessionId: string, adapter: SSHAdapter): void { // CRITICAL: All handlers must be synchronous and error-safe // Use try-catch to prevent handler errors from breaking the adapter adapter.on('data', (data: string) => { try { const output: ConsoleOutput = { sessionId, type: 'stdout', data, timestamp: new Date(), raw: data, }; this.addToOutputBuffer(sessionId, output); } catch (error) { this.logger.error( `Error handling SSH data for session ${sessionId}:`, error ); } }); adapter.on('error', (error: string) => { try { const output: ConsoleOutput = { sessionId, type: 'stderr', data: error, timestamp: new Date(), raw: error, }; this.addToOutputBuffer(sessionId, output); } catch (handlerError) { this.logger.error( `Error handling SSH error for session ${sessionId}:`, handlerError ); // Log original error too in case handler fails this.logger.error(`Original SSH error:`, error); } }); adapter.on('close', (code: number) => { try { this.markSessionComplete(sessionId, code); } catch (error) { this.logger.error( `Error handling SSH close for session ${sessionId}:`, error ); } }); adapter.on('connected', () => { try { const session = this.sessions.get(sessionId); if (session) { session.status = 'running'; session.executionState = 'idle'; } } catch (error) { this.logger.error( `Error handling SSH connected for session ${sessionId}:`, error ); } }); adapter.on('error-recovered', (data) => { try { this.logger.info( `SSH session ${sessionId} error recovered: ${JSON.stringify(data)}` ); } catch (error) { this.logger.error( `Error logging SSH recovery for session ${sessionId}:`, error ); } }); adapter.on('degradation-enabled', (data) => { try { this.logger.warn( `SSH session ${sessionId} degraded mode: ${JSON.stringify(data)}` ); } catch (error) { this.logger.error( `Error logging SSH degradation for session ${sessionId}:`, error ); } }); // CRITICAL: Add handler for new safety events from improved SSHAdapter adapter.on('connection-failed', (data) => { try { this.logger.error( `SSH session ${sessionId} connection failed: ${JSON.stringify(data)}` ); // Mark session as failed so it can be cleaned up const session = this.sessions.get(sessionId); if (session) { session.status = 'failed'; } } catch (error) { this.logger.error( `Error handling SSH connection failure for session ${sessionId}:`, error ); } }); adapter.on('connection-restored', (data) => { try { this.logger.info( `SSH session ${sessionId} connection restored: ${JSON.stringify(data)}` ); // Update session status const session = this.sessions.get(sessionId); if (session) { session.status = 'running'; session.executionState = 'idle'; } } catch (error) { this.logger.error( `Error handling SSH connection restoration for session ${sessionId}:`, error ); } }); } }

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