Skip to main content
Glama
ooples

MCP Console Automation Server

SPICEProtocol.ts22.3 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'; // SPICE Protocol connection options interface SPICEConnectionOptions extends SessionOptions { // Basic SPICE Configuration host?: string; port?: number; password?: string; channel?: string; // SPICE Client Configuration spiceClient?: string; spiceGtkClient?: string; remoteViewerClient?: string; // Connection Security spicePassword?: string; spiceTicket?: string; spiceCertSubject?: string; spiceCaCertFile?: string; spiceHostSubject?: string; tlsPort?: number; enableTLS?: boolean; enableAuth?: boolean; insecure?: boolean; // Display Configuration fullscreen?: boolean; monitor?: number; monitorMapping?: string; displayId?: number; enableSpiceDisplays?: boolean; enableSpiceInputs?: boolean; enableSpiceAudio?: boolean; enableSpiceUsb?: boolean; enableSpiceSmartcard?: boolean; // Audio Configuration audioBackend?: 'pulse' | 'alsa' | 'auto' | 'none'; disableAudio?: boolean; audioDelay?: number; // USB Redirection usbRedirection?: boolean; usbFilter?: string; usbAutoConnect?: boolean; spiceGtkUsbAutoConnect?: boolean; // Smartcard Support smartcardSupport?: boolean; smartcardDatabase?: string; smartcardCertificates?: string[]; // Clipboard Integration enableClipboard?: boolean; clipboardSelection?: 'clipboard' | 'primary'; // File Transfer enableFileTransfer?: boolean; sharedDir?: string; // Performance Options colorDepth?: 16 | 24 | 32; enableCompression?: boolean; compressionType?: 'auto' | 'never' | 'always'; imageCompressionType?: 'auto_glz' | 'auto_lz' | 'quic' | 'glz' | 'lz' | 'off'; jpegWanCompressionType?: 'auto' | 'never' | 'always'; zlibGlzWanCompressionType?: 'auto' | 'never' | 'always'; streamingVideoMode?: 'filter' | 'all' | 'off'; playbackCompressionMode?: 'on' | 'off'; // Network Configuration proxyHost?: string; proxyPort?: number; proxyUser?: string; proxyPassword?: string; // Advanced Options spiceAgentFileTransfer?: boolean; spiceAgentCopyPaste?: boolean; spiceGtkGrabSequence?: string; spiceGtkAccelMap?: string; releaseKeysSequence?: string; grabKeyboard?: boolean; grabMouse?: boolean; resizeGuest?: boolean; spiceDisableEffects?: string[]; // Logging and Debugging debugLevel?: 'error' | 'warning' | 'info' | 'debug'; enableLogging?: boolean; logFile?: string; // Connection Management connectionTimeout?: number; reconnectDelay?: number; autoReconnect?: boolean; maxReconnectAttempts?: number; // SPICE Server Configuration (when acting as server) serverMode?: boolean; serverPort?: number; serverPassword?: string; serverTlsPort?: number; serverCertFile?: string; serverKeyFile?: string; serverCaCertFile?: string; serverCertSubject?: string; serverImageCompressionType?: | 'auto_glz' | 'auto_lz' | 'quic' | 'glz' | 'lz' | 'off'; serverJpegWanCompressionType?: 'auto' | 'never' | 'always'; serverZlibGlzWanCompressionType?: 'auto' | 'never' | 'always'; serverStreamingVideoMode?: 'filter' | 'all' | 'off'; serverPlaybackCompressionMode?: 'on' | 'off'; serverAgentMouse?: boolean; serverAgentCopyPaste?: boolean; serverAgentFileTransfer?: boolean; // WebDAV Integration webdavServer?: string; webdavPort?: number; webdavUser?: string; webdavPassword?: string; // Multi-monitor Support monitors?: Array<{ id: number; x: number; y: number; width: number; height: number; primary?: boolean; }>; // Input Configuration keyboardLayout?: string; mouseMode?: 'client' | 'server'; disableInputs?: boolean; // Graphics Configuration renderer?: 'cairo' | 'opengl'; enableAcceleration?: boolean; // Migration Support migrationSupport?: boolean; migrationData?: string; // Custom Arguments customArgs?: string[]; // Environment Variables environment?: Record<string, string>; } /** * SPICE Protocol Implementation * * Provides SPICE (Simple Protocol for Independent Computing Environments) remote computing access * Supports virtual desktop connection, audio, USB redirection, file transfer, and enterprise virtualization features */ export class SPICEProtocol extends BaseProtocol { public readonly type: ConsoleType = 'spice'; public readonly capabilities: ProtocolCapabilities; private spiceProcesses = 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('spice'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: false, supportsPortForwarding: false, supportsAuthentication: true, supportsEncryption: true, supportsCompression: true, supportsMultiplexing: true, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: true, supportsPTY: false, maxConcurrentSessions: 10, // SPICE can handle multiple sessions defaultTimeout: 60000, // Remote desktop connections can take time supportedEncodings: ['utf-8'], supportedAuthMethods: ['password', 'ticket', 'certificate'], platformSupport: { windows: true, linux: true, macos: true, freebsd: true, }, }; } async initialize(): Promise<void> { if (this.isInitialized) return; try { // Check if SPICE client is available await this.checkSpiceAvailability(); this.isInitialized = true; this.logger.info( 'SPICE protocol initialized with production remote computing features' ); } catch (error: any) { this.logger.error('Failed to initialize SPICE protocol', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { const sessionId = `spice-${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 spiceProcess = this.spiceProcesses.get(sessionId); const session = this.sessions.get(sessionId); if (!spiceProcess || !spiceProcess.stdin || !session) { throw new Error(`No active SPICE session: ${sessionId}`); } spiceProcess.stdin.write(input); session.lastActivity = new Date(); this.emit('input-sent', { sessionId, input, timestamp: new Date(), }); this.logger.debug( `Sent input to SPICE session ${sessionId}: ${input.substring(0, 100)}` ); } async closeSession(sessionId: string): Promise<void> { try { const spiceProcess = this.spiceProcesses.get(sessionId); if (spiceProcess) { // Try graceful shutdown first spiceProcess.kill('SIGTERM'); // Force kill after timeout setTimeout(() => { if (spiceProcess && !spiceProcess.killed) { spiceProcess.kill('SIGKILL'); } }, 10000); this.spiceProcesses.delete(sessionId); } // Clean up base protocol session this.sessions.delete(sessionId); this.logger.info(`SPICE session ${sessionId} closed`); this.emit('session-closed', sessionId); } catch (error) { this.logger.error(`Error closing SPICE session ${sessionId}:`, error); throw error; } } async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const spiceOptions = options as SPICEConnectionOptions; // Build SPICE command const spiceCommand = this.buildSpiceCommand(spiceOptions); // Spawn SPICE process const spiceProcess = spawn(spiceCommand[0], spiceCommand.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(spiceOptions), ...options.env, }, }); // Set up output handling spiceProcess.stdout?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); spiceProcess.stderr?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); spiceProcess.on('error', (error) => { this.logger.error(`SPICE process error for session ${sessionId}:`, error); this.emit('session-error', { sessionId, error }); }); spiceProcess.on('close', (code) => { this.logger.info( `SPICE process closed for session ${sessionId} with code ${code}` ); this.markSessionComplete(sessionId, code || 0); }); // Store the process this.spiceProcesses.set(sessionId, spiceProcess); // Create session object const session: ConsoleSession = { id: sessionId, command: spiceCommand[0], args: spiceCommand.slice(1), cwd: options.cwd || process.cwd(), env: { ...process.env, ...this.buildEnvironment(spiceOptions), ...options.env, }, createdAt: new Date(), lastActivity: new Date(), status: 'running', type: this.type, streaming: options.streaming, executionState: 'idle', activeCommands: new Map(), pid: spiceProcess.pid, }; this.sessions.set(sessionId, session); this.logger.info( `SPICE session ${sessionId} created for host ${spiceOptions.host || 'localhost'}:${spiceOptions.port || 5900}` ); this.emit('session-created', { sessionId, type: 'spice', 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, // SPICE 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 SPICE session ${context.sessionId}: ${error.message}` ); return { recovered: false, strategy: 'none', attempts: 0, duration: 0, error: error.message, }; } async recoverSession(sessionId: string): Promise<boolean> { const spiceProcess = this.spiceProcesses.get(sessionId); return (spiceProcess && !spiceProcess.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.spiceProcesses.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.checkSpiceAvailability(); return { ...baseStatus, dependencies: { spice: { available: true }, }, }; } catch (error) { return { ...baseStatus, isHealthy: false, errors: [...baseStatus.errors, `SPICE not available: ${error}`], dependencies: { spice: { available: false }, }, }; } } private async checkSpiceAvailability(): Promise<void> { return new Promise((resolve, reject) => { // Try multiple common SPICE clients const clients = ['remote-viewer', 'spicy', 'virt-viewer']; let attempts = 0; const tryClient = (clientName: string) => { const testProcess = spawn(clientName, ['--version'], { stdio: 'pipe' }); testProcess.on('close', (code) => { if (code === 0) { resolve(); } else { attempts++; if (attempts < clients.length) { tryClient(clients[attempts]); } else { reject( new Error( 'No SPICE client found. Please install virt-viewer, remote-viewer, or spicy.' ) ); } } }); testProcess.on('error', () => { attempts++; if (attempts < clients.length) { tryClient(clients[attempts]); } else { reject( new Error( 'No SPICE client found. Please install virt-viewer, remote-viewer, or spicy.' ) ); } }); }; tryClient(clients[0]); }); } private buildSpiceCommand(options: SPICEConnectionOptions): string[] { const command = []; // SPICE client binary if (options.spiceClient) { command.push(options.spiceClient); } else if (options.remoteViewerClient) { command.push(options.remoteViewerClient); } else if (options.spiceGtkClient) { command.push(options.spiceGtkClient); } else { // Default clients in order of preference command.push('remote-viewer'); } // Connection string if (options.host && options.port) { const protocol = options.enableTLS ? 'spice' : 'spice'; const port = options.enableTLS && options.tlsPort ? options.tlsPort : options.port; command.push(`${protocol}://${options.host}:${port}`); } // Authentication if (options.spicePassword) { command.push('--spice-password', options.spicePassword); } if (options.spiceTicket) { command.push('--spice-ticket', options.spiceTicket); } // TLS Configuration if (options.enableTLS) { command.push('--spice-secure-channels', 'all'); } if (options.spiceCaCertFile) { command.push('--spice-ca-file', options.spiceCaCertFile); } if (options.spiceCertSubject) { command.push('--spice-cert-subject', options.spiceCertSubject); } if (options.spiceHostSubject) { command.push('--spice-host-subject', options.spiceHostSubject); } if (options.insecure) { command.push('--spice-disable-verify'); } // Display Configuration if (options.fullscreen) { command.push('--full-screen'); } if (options.monitor !== undefined) { command.push('--monitor', options.monitor.toString()); } if (options.monitorMapping) { command.push('--monitor-mapping', options.monitorMapping); } // Audio Configuration if (options.disableAudio) { command.push('--spice-disable-audio'); } if (options.audioBackend && options.audioBackend !== 'auto') { command.push('--spice-audio-backend', options.audioBackend); } // USB Redirection if (options.usbRedirection) { command.push('--spice-usb-auto-connect'); } if (options.usbFilter) { command.push('--spice-usb-filter', options.usbFilter); } // Smartcard Support if (options.smartcardSupport) { command.push('--spice-smartcard'); } if (options.smartcardDatabase) { command.push('--spice-smartcard-db', options.smartcardDatabase); } // Clipboard if (!options.enableClipboard) { command.push('--spice-disable-clipboard'); } // File Transfer if (!options.enableFileTransfer) { command.push('--spice-disable-file-transfer'); } if (options.sharedDir) { command.push('--spice-shared-dir', options.sharedDir); } // Performance Options if (options.colorDepth) { command.push('--spice-color-depth', options.colorDepth.toString()); } if (options.imageCompressionType) { command.push('--spice-image-compression', options.imageCompressionType); } if (options.jpegWanCompressionType) { command.push( '--spice-jpeg-wan-compression', options.jpegWanCompressionType ); } if (options.zlibGlzWanCompressionType) { command.push( '--spice-zlib-glz-wan-compression', options.zlibGlzWanCompressionType ); } if (options.streamingVideoMode) { command.push('--spice-streaming-video', options.streamingVideoMode); } if (options.playbackCompressionMode) { command.push( '--spice-playback-compression', options.playbackCompressionMode ); } // Advanced Options if (options.spiceGtkGrabSequence) { command.push('--spice-gtk-grab-sequence', options.spiceGtkGrabSequence); } if (options.releaseKeysSequence) { command.push('--release-cursor', options.releaseKeysSequence); } if (options.resizeGuest) { command.push('--spice-resize-guest'); } if (options.spiceDisableEffects) { options.spiceDisableEffects.forEach((effect) => { command.push('--spice-disable-effects', effect); }); } // Debugging if (options.debugLevel) { command.push('--debug-level', options.debugLevel); } if (options.enableLogging && options.logFile) { command.push('--log-file', options.logFile); } // Network Configuration if (options.proxyHost && options.proxyPort) { let proxyUrl = `${options.proxyHost}:${options.proxyPort}`; if (options.proxyUser && options.proxyPassword) { proxyUrl = `${options.proxyUser}:${options.proxyPassword}@${proxyUrl}`; } command.push('--spice-proxy', proxyUrl); } // Connection Management if (options.connectionTimeout) { command.push( '--spice-connection-timeout', options.connectionTimeout.toString() ); } // Multi-monitor Configuration if (options.monitors) { options.monitors.forEach((monitor, index) => { command.push( '--spice-monitor-config', `${monitor.id}:${monitor.x}:${monitor.y}:${monitor.width}:${monitor.height}` ); }); } // Input Configuration if (options.keyboardLayout) { command.push('--spice-keyboard-layout', options.keyboardLayout); } if (options.disableInputs) { command.push('--spice-disable-inputs'); } // Graphics Configuration if (options.renderer) { command.push('--spice-renderer', options.renderer); } if (options.enableAcceleration) { command.push('--spice-gl'); } // Custom arguments if (options.customArgs) { command.push(...options.customArgs); } return command; } private buildEnvironment( options: SPICEConnectionOptions ): Record<string, string> { const env: Record<string, string> = {}; // SPICE environment variables if (options.debugLevel) { env.SPICE_DEBUG = options.debugLevel; } if (options.enableLogging) { env.SPICE_DEBUG_LEVEL = options.debugLevel || 'info'; } // Graphics environment if (options.renderer) { env.SPICE_RENDERER = options.renderer; } // Custom environment variables if (options.environment) { Object.assign(env, options.environment); } return env; } async cleanup(): Promise<void> { this.logger.info('Cleaning up SPICE protocol'); // Close all SPICE processes for (const [sessionId, process] of Array.from(this.spiceProcesses)) { try { process.kill(); } catch (error) { this.logger.error( `Error killing SPICE process for session ${sessionId}:`, error ); } } // Clear all data this.spiceProcesses.clear(); // Call parent cleanup await super.cleanup(); } } export default SPICEProtocol;

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