Skip to main content
Glama
ooples

MCP Console Automation Server

SSHInteractiveFix.ts7.04 kB
/** * Complete fix for SSH interactive mode * This bypasses the command queue entirely for interactive sessions */ import { ClientChannel } from 'ssh2'; import { Logger } from '../utils/logger.js'; export class SSHInteractiveFix { private static logger = new Logger('SSHInteractiveFix'); private static interactiveSessions = new Set<string>(); /** * Mark a session as interactive */ static markInteractive(sessionId: string): void { this.interactiveSessions.add(sessionId); this.logger.info(`Session ${sessionId} marked as interactive`); } /** * Check if a session is interactive */ static isInteractive(sessionId: string): boolean { return this.interactiveSessions.has(sessionId); } /** * Clear interactive state */ static clearInteractive(sessionId: string): void { this.interactiveSessions.delete(sessionId); this.logger.info(`Session ${sessionId} cleared from interactive mode`); } /** * Send input directly to SSH channel, bypassing all queues */ static async sendDirectToSSH( channel: ClientChannel, input: string ): Promise<void> { return new Promise((resolve, reject) => { this.logger.debug(`Sending direct to SSH: ${input.substring(0, 50)}...`); channel.write(input, (error) => { if (error) { this.logger.error('Failed to send to SSH channel:', error); reject(error); } else { resolve(); } }); }); } /** * Send a key sequence directly to SSH channel */ static async sendKeyDirect( channel: ClientChannel, key: string ): Promise<void> { const keyMap: Record<string, string> = { enter: '\r', tab: '\t', escape: '\x1b', backspace: '\x7f', delete: '\x1b[3~', 'ctrl+c': '\x03', 'ctrl+d': '\x04', 'ctrl+z': '\x1a', 'ctrl+l': '\x0c', 'ctrl+x': '\x18', 'ctrl+o': '\x0f', 'ctrl+s': '\x13', 'ctrl+q': '\x11', 'ctrl+a': '\x01', 'ctrl+e': '\x05', 'ctrl+k': '\x0b', 'ctrl+u': '\x15', 'ctrl+w': '\x17', 'ctrl+break': '\x03', up: '\x1b[A', down: '\x1b[B', right: '\x1b[C', left: '\x1b[D', home: '\x1b[H', end: '\x1b[F', pageup: '\x1b[5~', pagedown: '\x1b[6~', f1: '\x1bOP', f2: '\x1bOQ', f3: '\x1bOR', f4: '\x1bOS', f5: '\x1b[15~', f6: '\x1b[17~', f7: '\x1b[18~', f8: '\x1b[19~', f9: '\x1b[20~', f10: '\x1b[21~', }; const sequence = keyMap[key.toLowerCase()] || key; return this.sendDirectToSSH(channel, sequence); } /** * Detect interactive programs from command */ static isInteractiveCommand(command: string): boolean { const interactivePrograms = [ 'nano', 'vim', 'vi', 'emacs', 'pico', 'joe', 'less', 'more', 'man', 'top', 'htop', 'iotop', 'iftop', 'mysql', 'psql', 'sqlite3', 'mongo', 'redis-cli', 'python', 'python3', 'node', 'irb', 'php -a', 'julia', 'ssh', 'telnet', 'ftp', 'sftp', 'screen', 'tmux', 'byobu', 'gdb', 'pdb', 'lldb', 'crontab -e', 'visudo', 'passwd', 'su -', 'sudo -i', 'mc', 'ranger', 'nnn', // file managers 'tig', 'gitui', // git interfaces 'ncdu', 'btop', 'glances', // system monitors ]; const cmd = command.toLowerCase().trim(); return interactivePrograms.some( (prog) => cmd === prog || cmd.startsWith(prog + ' ') || cmd.includes('/' + prog + ' ') || cmd.endsWith('/' + prog) ); } /** * Patch ConsoleManager to handle interactive SSH properly */ static patchConsoleManager(consoleManager: any): void { const originalSendInput = consoleManager.sendInput.bind(consoleManager); const originalSendKey = consoleManager.sendKey.bind(consoleManager); const originalExecuteCommand = consoleManager.executeCommand.bind(consoleManager); // Track when interactive commands are executed consoleManager.executeCommand = async function ( sessionId: string, command: string, args?: string[] ): Promise<void> { const fullCommand = args && args.length > 0 ? `${command} ${args.join(' ')}` : command; // Check if this is an interactive command if (SSHInteractiveFix.isInteractiveCommand(fullCommand)) { SSHInteractiveFix.markInteractive(sessionId); SSHInteractiveFix.logger.info( `Executing interactive command in session ${sessionId}: ${fullCommand}` ); } // For SSH sessions with interactive commands, send directly const sshChannel = this.sshChannels?.get(sessionId); if (sshChannel && SSHInteractiveFix.isInteractive(sessionId)) { SSHInteractiveFix.logger.info( `Sending interactive command directly to SSH channel: ${fullCommand}` ); await SSHInteractiveFix.sendDirectToSSH(sshChannel, fullCommand + '\n'); return; } return originalExecuteCommand.call(this, sessionId, command, args); }; // Override sendInput for interactive sessions consoleManager.sendInput = async function ( sessionId: string, input: string ): Promise<void> { const sshChannel = this.sshChannels?.get(sessionId); // If it's an SSH session and it's interactive, bypass everything if (sshChannel && SSHInteractiveFix.isInteractive(sessionId)) { SSHInteractiveFix.logger.info( `Sending input directly to interactive SSH session ${sessionId}` ); await SSHInteractiveFix.sendDirectToSSH(sshChannel, input); return; } return originalSendInput.call(this, sessionId, input); }; // Override sendKey for interactive sessions consoleManager.sendKey = async function ( sessionId: string, key: string ): Promise<void> { const sshChannel = this.sshChannels?.get(sessionId); // If it's an SSH session and it's interactive, send key directly if (sshChannel && SSHInteractiveFix.isInteractive(sessionId)) { SSHInteractiveFix.logger.info( `Sending key '${key}' directly to interactive SSH session ${sessionId}` ); await SSHInteractiveFix.sendKeyDirect(sshChannel, key); return; } return originalSendKey.call(this, sessionId, key); }; // Clean up on session close const originalCleanupSession = consoleManager.cleanupSession?.bind(consoleManager); if (originalCleanupSession) { consoleManager.cleanupSession = function (sessionId: string) { SSHInteractiveFix.clearInteractive(sessionId); return originalCleanupSession.call(this, sessionId); }; } this.logger.info('ConsoleManager patched with SSHInteractiveFix'); } }

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