Skip to main content
Glama
ooples

MCP Console Automation Server

BMCProtocol.ts16.1 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'; // BMC Protocol connection options interface BMCConnectionOptions extends SessionOptions { bmcHost: string; bmcPort?: number; username?: string; password?: string; ipmiPath?: string; ipmiVersion?: '1.5' | '2.0'; interface?: 'lan' | 'lanplus' | 'open' | 'imb' | 'smic' | 'ssif'; privilege?: 'CALLBACK' | 'USER' | 'OPERATOR' | 'ADMINISTRATOR'; authType?: 'NONE' | 'STRAIGHT_PASSWORD_KEY' | 'MD2' | 'MD5' | 'OEM'; cipherSuite?: number; sessionTimeout?: number; retries?: number; enableSol?: boolean; // Serial Over LAN solBaudRate?: 9600 | 19200 | 38400 | 57600 | 115200; solEscapeChar?: string; enableKvm?: boolean; // Keyboard Video Mouse redirection enableFru?: boolean; // Field Replaceable Unit enableSdr?: boolean; // Sensor Data Records enableSel?: boolean; // System Event Log enablePef?: boolean; // Platform Event Filtering powerControl?: 'on' | 'off' | 'cycle' | 'reset' | 'diag' | 'soft'; bootDevice?: 'pxe' | 'disk' | 'safe' | 'diag' | 'cdrom' | 'floppy'; enableWatchdog?: boolean; watchdogTimeout?: number; fanControl?: boolean; thermalManagement?: boolean; environment?: Record<string, string>; } /** * BMC Protocol Implementation * * Provides Baseboard Management Controller console access through IPMI command * Supports server hardware management, SOL, power control, sensor monitoring, and system administration */ export class BMCProtocol extends BaseProtocol { public readonly type: ConsoleType = 'bmc'; public readonly capabilities: ProtocolCapabilities; private bmcProcesses = 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('bmc'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: false, supportsX11Forwarding: false, supportsPortForwarding: false, supportsAuthentication: true, supportsEncryption: true, supportsCompression: false, supportsMultiplexing: false, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: false, supportsPTY: true, maxConcurrentSessions: 5, defaultTimeout: 30000, // BMC operations can be slow supportedEncodings: ['utf-8'], supportedAuthMethods: ['password', 'certificate'], platformSupport: { windows: true, linux: true, macos: true, freebsd: true, }, }; } async initialize(): Promise<void> { if (this.isInitialized) return; try { // Check if IPMI tool is available await this.checkIpmiAvailability(); this.isInitialized = true; this.logger.info('BMC protocol initialized with production features'); } catch (error: any) { this.logger.error('Failed to initialize BMC protocol', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { const sessionId = `bmc-${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 bmcProcess = this.bmcProcesses.get(sessionId); const session = this.sessions.get(sessionId); if (!bmcProcess || !bmcProcess.stdin || !session) { throw new Error(`No active BMC session: ${sessionId}`); } bmcProcess.stdin.write(input); session.lastActivity = new Date(); this.emit('input-sent', { sessionId, input, timestamp: new Date(), }); this.logger.debug( `Sent input to BMC session ${sessionId}: ${input.substring(0, 100)}` ); } async closeSession(sessionId: string): Promise<void> { try { const bmcProcess = this.bmcProcesses.get(sessionId); if (bmcProcess) { // Try graceful shutdown first bmcProcess.kill('SIGTERM'); // Force kill after timeout setTimeout(() => { if (bmcProcess && !bmcProcess.killed) { bmcProcess.kill('SIGKILL'); } }, 5000); this.bmcProcesses.delete(sessionId); } // Clean up base protocol session this.sessions.delete(sessionId); this.logger.info(`BMC session ${sessionId} closed`); this.emit('session-closed', sessionId); } catch (error) { this.logger.error(`Error closing BMC session ${sessionId}:`, error); throw error; } } async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const bmcOptions = options as BMCConnectionOptions; // Validate required BMC connection parameters if (!bmcOptions.bmcHost) { throw new Error('BMC host is required for BMC protocol'); } // Build IPMI command const ipmiCommand = this.buildIpmiCommand(bmcOptions); // Spawn IPMI process const bmcProcess = spawn(ipmiCommand[0], ipmiCommand.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(bmcOptions), ...options.env, }, }); // Set up output handling bmcProcess.stdout?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); bmcProcess.stderr?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); bmcProcess.on('error', (error) => { this.logger.error(`BMC process error for session ${sessionId}:`, error); this.emit('session-error', { sessionId, error }); }); bmcProcess.on('close', (code) => { this.logger.info( `BMC process closed for session ${sessionId} with code ${code}` ); this.markSessionComplete(sessionId, code || 0); }); // Store the process this.bmcProcesses.set(sessionId, bmcProcess); // Create session object const session: ConsoleSession = { id: sessionId, command: ipmiCommand[0], args: ipmiCommand.slice(1), cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(bmcOptions), ...options.env, }, createdAt: new Date(), lastActivity: new Date(), status: 'running', type: this.type, streaming: options.streaming, executionState: 'idle', activeCommands: new Map(), pid: bmcProcess.pid, }; this.sessions.set(sessionId, session); this.logger.info( `BMC session ${sessionId} created for ${bmcOptions.bmcHost}` ); this.emit('session-created', { sessionId, type: 'bmc', 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: true, // BMC commands are typically one-shot isPersistent: false, createdAt: session.createdAt, lastActivity: session.lastActivity, pid: session.pid, metadata: {}, }; } async handleError( error: Error, context: ErrorContext ): Promise<ErrorRecoveryResult> { this.logger.error( `Error in BMC session ${context.sessionId}: ${error.message}` ); return { recovered: false, strategy: 'none', attempts: 0, duration: 0, error: error.message, }; } async recoverSession(sessionId: string): Promise<boolean> { const bmcProcess = this.bmcProcesses.get(sessionId); return (bmcProcess && !bmcProcess.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.bmcProcesses.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.checkIpmiAvailability(); return { ...baseStatus, dependencies: { ipmi: { available: true }, }, }; } catch (error) { return { ...baseStatus, isHealthy: false, errors: [...baseStatus.errors, `IPMI not available: ${error}`], dependencies: { ipmi: { available: false }, }, }; } } private async checkIpmiAvailability(): Promise<void> { return new Promise((resolve, reject) => { const testProcess = spawn('ipmitool', ['--version'], { stdio: 'pipe' }); testProcess.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error('IPMI tool not found. Please install ipmitool.')); } }); testProcess.on('error', () => { reject(new Error('IPMI tool not found. Please install ipmitool.')); }); }); } private buildIpmiCommand(options: BMCConnectionOptions): string[] { const command = []; // IPMI executable if (options.ipmiPath) { command.push(options.ipmiPath); } else { command.push('ipmitool'); } // Interface type if (options.interface) { command.push('-I', options.interface); } else { command.push('-I', 'lanplus'); // Default to lanplus for better security } // BMC host and port command.push('-H', options.bmcHost); if (options.bmcPort) { command.push('-p', options.bmcPort.toString()); } // Authentication if (options.username) { command.push('-U', options.username); } if (options.password) { command.push('-P', options.password); } // Privilege level if (options.privilege) { command.push('-L', options.privilege); } // Authentication type if (options.authType) { command.push('-A', options.authType); } // IPMI version if (options.ipmiVersion === '1.5') { command.push('-v'); } // Cipher suite for encryption if (options.cipherSuite !== undefined) { command.push('-C', options.cipherSuite.toString()); } // Session timeout if (options.sessionTimeout) { command.push('-N', options.sessionTimeout.toString()); } // Retries if (options.retries) { command.push('-R', options.retries.toString()); } // Command based on options if (options.enableSol) { // Serial Over LAN command.push('sol', 'activate'); if (options.solBaudRate) { command.push('baud', options.solBaudRate.toString()); } if (options.solEscapeChar) { command.push('escape-char', options.solEscapeChar); } } else if (options.powerControl) { // Power control command.push('power', options.powerControl); } else if (options.bootDevice) { // Boot device selection command.push('chassis', 'bootdev', options.bootDevice); } else if (options.enableSdr) { // Sensor Data Records command.push('sdr', 'list'); } else if (options.enableSel) { // System Event Log command.push('sel', 'list'); } else if (options.enableFru) { // Field Replaceable Unit command.push('fru', 'list'); } else if (options.enableWatchdog) { // Watchdog timer command.push('mc', 'watchdog', 'get'); if (options.watchdogTimeout) { command.push('timeout', options.watchdogTimeout.toString()); } } else if (options.fanControl) { // Fan control command.push('sensor', 'list', 'Fan'); } else if (options.thermalManagement) { // Thermal management command.push('sensor', 'list', 'Temperature'); } else { // Default to chassis status command.push('chassis', 'status'); } // Application arguments if (options.args) { command.push(...options.args); } return command; } private buildEnvironment( options: BMCConnectionOptions ): Record<string, string> { const env: Record<string, string> = {}; // IPMI environment variables if (options.bmcHost) { env.IPMI_BMC_HOST = options.bmcHost; } if (options.username) { env.IPMI_USERNAME = options.username; } if (options.password) { env.IPMI_PASSWORD = options.password; } // Interface settings if (options.interface) { env.IPMI_INTERFACE = options.interface; } // Privilege level if (options.privilege) { env.IPMI_PRIVILEGE_LEVEL = options.privilege; } // Timeout settings if (options.sessionTimeout) { env.IPMI_SESSION_TIMEOUT = options.sessionTimeout.toString(); } // Custom environment variables if (options.environment) { Object.assign(env, options.environment); } return env; } async cleanup(): Promise<void> { this.logger.info('Cleaning up BMC protocol'); // Close all BMC processes for (const [sessionId, process] of Array.from(this.bmcProcesses)) { try { process.kill(); } catch (error) { this.logger.error( `Error killing BMC process for session ${sessionId}:`, error ); } } // Clear all data this.bmcProcesses.clear(); // Call parent cleanup await super.cleanup(); } } export default BMCProtocol;

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