Skip to main content
Glama
ooples

MCP Console Automation Server

X11VNCProtocol.ts25.9 kB
import { ChildProcess, spawn } from 'child_process'; import * as net from 'net'; import * as fs from 'fs/promises'; import * as path from 'path'; import { BaseProtocol } from '../core/BaseProtocol.js'; import { ProtocolCapabilities, ProtocolHealthStatus, SessionState, } from '../core/IProtocol.js'; import { ConsoleSession, SessionOptions, ConsoleType, ConsoleOutput, } from '../types/index.js'; /** * X11VNC server configuration */ interface X11VNCConfig { display?: string; port?: number; password?: string; passwordFile?: string; viewOnly?: boolean; shared?: boolean; forever?: boolean; loop?: boolean; allowLocal?: boolean; ssl?: boolean; sslOnly?: boolean; sslCertFile?: string; sslKeyFile?: string; stunnel?: boolean; httpPort?: number; httpDir?: string; scale?: string; geometry?: string; cursor?: boolean; noCursor?: boolean; arrow?: boolean; fixScreen?: boolean; noRepeat?: boolean; speeds?: string; wait?: number; defer?: number; readTimeout?: number; acceptMode?: 'once' | 'prompt' | 'gone'; gone?: string; users?: string; clipboardFile?: string; noClipboard?: boolean; selectionSend?: boolean; selectionRecv?: boolean; logFile?: string; ultraDSM?: boolean; msLogon?: string; noxdamage?: boolean; xkb?: boolean; skipKeycodes?: string; modtweak?: boolean; xrandr?: string; padGeom?: string; rotate?: string; reflect?: string; id?: string; sid?: string; dpms?: boolean; noxfixes?: boolean; alphacut?: number; alphafrac?: number; alpharemove?: boolean; noalphablend?: boolean; nocursorshape?: boolean; noremoteresize?: boolean; noserverdpms?: boolean; noultraext?: boolean; chatWindow?: boolean; guiTray?: boolean; rfbAuth?: string; permitFileTransfer?: boolean; tightVNC?: boolean; ultraVNC?: boolean; findDisplay?: boolean; create?: boolean; env?: Record<string, string>; xvfb?: boolean; xdummy?: boolean; xvnc?: boolean; x11vncPath?: string; } /** * X11VNC session state */ interface X11VNCSession extends ConsoleSession { config: X11VNCConfig; serverProcess?: ChildProcess; vncPort?: number; httpPort?: number; passwordFile?: string; logFile?: string; isServerRunning: boolean; clientConnections: Set<string>; displayServer?: 'x11' | 'xvfb' | 'xdummy' | 'xvnc'; virtualDisplay?: string; xvfbProcess?: ChildProcess; performanceMetrics?: { fps: number; bandwidth: number; latency: number; quality: number; }; } /** * X11VNC Protocol Implementation * Provides VNC server functionality for X11 displays with comprehensive configuration */ export class X11VNCProtocol extends BaseProtocol { public readonly type: ConsoleType = 'x11vnc'; public readonly capabilities: ProtocolCapabilities = { 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: false, maxConcurrentSessions: 10, defaultTimeout: 30000, supportedEncodings: [ 'utf-8', 'raw', 'tight', 'zlib', 'hextile', 'zrle', 'ultra', ], supportedAuthMethods: [ 'none', 'vnc', 'unix', 'tls', 'vencrypt', 'sasl', 'md5', 'rfb', ], platformSupport: { windows: false, linux: true, macos: true, freebsd: true, }, }; private x11vncSessions: Map<string, X11VNCSession> = new Map(); private availablePorts: Set<number> = new Set(); private usedPorts: Set<number> = new Set(); private defaultConfig: Partial<X11VNCConfig> = { display: ':0', port: 5900, shared: true, forever: true, noxdamage: true, xkb: true, defer: 10, wait: 10, }; constructor() { super('X11VNCProtocol'); this.initializePortPool(); } private initializePortPool(): void { for (let port = 5900; port <= 5910; port++) { this.availablePorts.add(port); } } async initialize(): Promise<void> { this.logger.info('Initializing X11VNC protocol'); try { await this.checkDependencies(); await this.detectDisplayServers(); this.isInitialized = true; this.logger.info('X11VNC protocol initialized successfully'); } catch (error) { this.logger.error('Failed to initialize X11VNC protocol:', error); throw error; } } private async checkDependencies(): Promise<void> { const dependencies: Record<string, boolean> = {}; // Check for x11vnc try { await this.executeSystemCommand('x11vnc', ['-version']); dependencies.x11vnc = true; } catch { dependencies.x11vnc = false; this.logger.warn('x11vnc not found in PATH'); } // Check for Xvfb (virtual framebuffer) try { await this.executeSystemCommand('Xvfb', ['-help']); dependencies.xvfb = true; } catch { dependencies.xvfb = false; } // Check for Xdummy try { await this.executeSystemCommand('Xdummy', ['-version']); dependencies.xdummy = true; } catch { dependencies.xdummy = false; } // Check for TigerVNC try { await this.executeSystemCommand('vncserver', ['-version']); dependencies.tigervnc = true; } catch { dependencies.tigervnc = false; } // Store dependencies for later use (this as any)._dependencies = dependencies; } private async detectDisplayServers(): Promise<string[]> { const displays: string[] = []; // Check for X11 displays try { const result = await this.executeSystemCommand('ls', ['/tmp/.X11-unix/']); const matches = result.match(/X(\d+)/g); if (matches) { displays.push(...matches.map((m) => `:${m.substring(1)}`)); } } catch { this.logger.debug('No X11 displays found in /tmp/.X11-unix/'); } // Check DISPLAY environment variable const envDisplay = process.env.DISPLAY; if (envDisplay && !displays.includes(envDisplay)) { displays.push(envDisplay); } this.logger.info(`Detected displays: ${displays.join(', ') || 'none'}`); return displays; } /** * Create a new session */ async createSession(options: SessionOptions): Promise<ConsoleSession> { const sessionId = this.generateSessionId(); const sessionState: SessionState = { sessionId: sessionId, status: 'initializing', isOneShot: this.isOneShotCommand(options), isPersistent: !this.isOneShotCommand(options), createdAt: new Date(), lastActivity: new Date(), }; return this.doCreateSession(sessionId, options, sessionState); } private generateSessionId(): string { return `x11vnc-${Date.now()}-${Math.random().toString(36).substring(7)}`; } protected async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { const config = this.parseX11VNCOptions(options); const session: X11VNCSession = { id: sessionId, type: 'x11vnc', status: 'initializing', command: options.command || 'x11vnc', args: options.args || [], cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.env }, createdAt: new Date(), streaming: options.streaming ?? true, config, isServerRunning: false, clientConnections: new Set(), executionState: 'idle', activeCommands: new Map(), }; this.x11vncSessions.set(sessionId, session); this.sessions.set(sessionId, session); try { // Start X11VNC server await this.startX11VNCServer(sessionId, session); session.status = 'running'; sessionState.status = 'running'; this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: `X11VNC server started on port ${session.vncPort}\n`, timestamp: new Date(), }); return session; } catch (error) { session.status = 'stopped'; session.exitCode = 1; sessionState.status = 'stopped'; throw error; } } private parseX11VNCOptions(options: SessionOptions): X11VNCConfig { const config: X11VNCConfig = { ...this.defaultConfig }; // Parse from args if provided if (options.args) { const args = options.args; for (let i = 0; i < args.length; i++) { const arg = args[i]; const nextArg = args[i + 1]; switch (arg) { case '-display': config.display = nextArg; i++; break; case '-rfbport': config.port = parseInt(nextArg); i++; break; case '-passwd': config.password = nextArg; i++; break; case '-passwdfile': config.passwordFile = nextArg; i++; break; case '-viewonly': config.viewOnly = true; break; case '-shared': config.shared = true; break; case '-forever': config.forever = true; break; case '-loop': config.loop = true; break; case '-localhost': config.allowLocal = true; break; case '-ssl': config.ssl = true; break; case '-sslonly': config.sslOnly = true; break; case '-stunnel': config.stunnel = true; break; case '-httpport': config.httpPort = parseInt(nextArg); i++; break; case '-httpdir': config.httpDir = nextArg; i++; break; case '-scale': config.scale = nextArg; i++; break; case '-geometry': case '-geom': config.geometry = nextArg; i++; break; case '-nocursor': config.noCursor = true; break; case '-cursor': config.cursor = true; break; case '-arrow': config.arrow = true; break; case '-fixscreen': config.fixScreen = true; break; case '-norepeat': config.noRepeat = true; break; case '-speeds': config.speeds = nextArg; i++; break; case '-wait': config.wait = parseInt(nextArg); i++; break; case '-defer': config.defer = parseInt(nextArg); i++; break; case '-noxdamage': config.noxdamage = true; break; case '-xkb': config.xkb = true; break; case '-create': config.create = true; break; case '-xvfb': config.xvfb = true; break; case '-xdummy': config.xdummy = true; break; } } } // Parse from environment variables if (options.env) { if (options.env.X11VNC_DISPLAY) config.display = options.env.X11VNC_DISPLAY; if (options.env.X11VNC_PORT) config.port = parseInt(options.env.X11VNC_PORT); if (options.env.X11VNC_PASSWORD) config.password = options.env.X11VNC_PASSWORD; } return config; } private async startX11VNCServer( sessionId: string, session: X11VNCSession ): Promise<void> { const config = session.config; // Allocate port session.vncPort = await this.allocatePort(config.port); // Create virtual display if needed if (config.create || config.xvfb || config.xdummy) { await this.createVirtualDisplay(session); } // Build command arguments const args: string[] = []; // Display args.push('-display', config.display || session.virtualDisplay || ':0'); // Port args.push('-rfbport', session.vncPort.toString()); // Authentication if (config.password) { // Create temporary password file session.passwordFile = path.join('/tmp', `x11vnc_${sessionId}.pwd`); await fs.writeFile(session.passwordFile, config.password, { mode: 0o600, }); args.push('-rfbauth', session.passwordFile); } else if (config.passwordFile) { args.push('-rfbauth', config.passwordFile); } else { args.push('-nopw'); // No password } // Options if (config.viewOnly) args.push('-viewonly'); if (config.shared) args.push('-shared'); if (config.forever) args.push('-forever'); if (config.loop) args.push('-loop'); if (config.allowLocal) args.push('-localhost'); if (config.ssl) { args.push('-ssl'); if (config.sslCertFile) args.push('-ssldir', path.dirname(config.sslCertFile)); } if (config.sslOnly) args.push('-sslonly'); if (config.httpPort) { args.push('-httpport', config.httpPort.toString()); session.httpPort = config.httpPort; } if (config.httpDir) args.push('-httpdir', config.httpDir); if (config.scale) args.push('-scale', config.scale); if (config.geometry) args.push('-geometry', config.geometry); if (config.noCursor) args.push('-nocursor'); else if (config.cursor) args.push('-cursor', 'most'); if (config.arrow) args.push('-arrow'); if (config.fixScreen) args.push('-fixscreen'); if (config.noRepeat) args.push('-norepeat'); if (config.speeds) args.push('-speeds', config.speeds); if (config.wait) args.push('-wait', config.wait.toString()); if (config.defer) args.push('-defer', config.defer.toString()); if (config.noxdamage) args.push('-noxdamage'); if (config.xkb) args.push('-xkb'); if (config.noClipboard) args.push('-noclipboard'); // Performance tuning args.push('-ncache', '10'); // Client-side caching args.push('-ncache_cr'); // Cache cursor rendering // Logging if (config.logFile || (this.logger as any).level === 'debug') { session.logFile = config.logFile || path.join('/tmp', `x11vnc_${sessionId}.log`); args.push('-o', session.logFile); args.push('-verbose'); } // Start x11vnc process const x11vncPath = config.x11vncPath || 'x11vnc'; session.serverProcess = spawn(x11vncPath, args, { cwd: session.cwd, env: session.env, detached: false, }); session.isServerRunning = true; // Handle process output session.serverProcess.stdout?.on('data', (data: Buffer) => { const output = data.toString(); this.parseX11VNCOutput(sessionId, output); this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: output, timestamp: new Date(), }); }); session.serverProcess.stderr?.on('data', (data: Buffer) => { const output = data.toString(); this.parseX11VNCOutput(sessionId, output); this.addToOutputBuffer(sessionId, { sessionId, type: 'stderr', data: output, timestamp: new Date(), }); }); session.serverProcess.on('exit', (code, signal) => { session.isServerRunning = false; session.exitCode = code ?? undefined; session.status = 'stopped'; this.cleanupSession(sessionId); this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: `X11VNC server exited with code ${code} (signal: ${signal})\n`, timestamp: new Date(), }); }); // Wait for server to be ready await this.waitForServerReady(sessionId, session); } private async createVirtualDisplay(session: X11VNCSession): Promise<void> { const displayNum = Math.floor(Math.random() * 100) + 10; session.virtualDisplay = `:${displayNum}`; const geometry = session.config.geometry || '1920x1080'; const depth = '24'; if (session.config.xvfb !== false) { // Try Xvfb first try { const xvfbArgs = [ session.virtualDisplay, '-screen', '0', `${geometry}x${depth}`, '-ac', // Disable access control '+extension', 'GLX', '+render', '-noreset', ]; session.xvfbProcess = spawn('Xvfb', xvfbArgs, { env: session.env, detached: false, }); session.displayServer = 'xvfb'; session.config.display = session.virtualDisplay; // Wait for Xvfb to start await new Promise((resolve) => setTimeout(resolve, 1000)); this.logger.info(`Created Xvfb display ${session.virtualDisplay}`); return; } catch (error) { this.logger.warn('Failed to start Xvfb:', error); } } if (session.config.xdummy) { // Try Xdummy try { const xdummyArgs = [session.virtualDisplay]; session.xvfbProcess = spawn('Xdummy', xdummyArgs, { env: session.env, detached: false, }); session.displayServer = 'xdummy'; session.config.display = session.virtualDisplay; await new Promise((resolve) => setTimeout(resolve, 1000)); this.logger.info(`Created Xdummy display ${session.virtualDisplay}`); return; } catch (error) { this.logger.warn('Failed to start Xdummy:', error); } } throw new Error('Failed to create virtual display'); } private parseX11VNCOutput(sessionId: string, output: string): void { const session = this.x11vncSessions.get(sessionId); if (!session) return; // Parse connection events if (output.includes('Got connection from')) { const match = output.match(/Got connection from (.+)/); if (match) { session.clientConnections.add(match[1]); this.emit('client-connected', { sessionId, client: match[1] }); } } if (output.includes('Client gone')) { const match = output.match(/Client (.+) gone/); if (match) { session.clientConnections.delete(match[1]); this.emit('client-disconnected', { sessionId, client: match[1] }); } } // Parse performance metrics if (output.includes('fb read rate:')) { const match = output.match(/fb read rate:\s*([\d.]+)/); if (match) { if (!session.performanceMetrics) { session.performanceMetrics = { fps: 0, bandwidth: 0, latency: 0, quality: 100, }; } session.performanceMetrics.fps = parseFloat(match[1]); } } } private async waitForServerReady( sessionId: string, session: X11VNCSession, timeout: number = 10000 ): Promise<void> { const startTime = Date.now(); while (Date.now() - startTime < timeout) { if (!session.isServerRunning) { throw new Error('X11VNC server failed to start'); } // Check if port is listening const isListening = await this.isPortListening(session.vncPort!); if (isListening) { this.logger.info(`X11VNC server ready on port ${session.vncPort}`); return; } await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error('X11VNC server startup timeout'); } private async isPortListening(port: number): Promise<boolean> { return new Promise((resolve) => { const client = new net.Socket(); client.once('connect', () => { client.destroy(); resolve(true); }); client.once('error', () => { resolve(false); }); client.connect(port, 'localhost'); }); } private async allocatePort(preferredPort?: number): Promise<number> { if (preferredPort && this.availablePorts.has(preferredPort)) { this.availablePorts.delete(preferredPort); this.usedPorts.add(preferredPort); return preferredPort; } // Find next available port for (const port of this.availablePorts) { const inUse = await this.isPortListening(port); if (!inUse) { this.availablePorts.delete(port); this.usedPorts.add(port); return port; } } // If all preset ports are used, find a random one return this.findRandomPort(); } private async findRandomPort(): Promise<number> { return new Promise((resolve, reject) => { const server = net.createServer(); server.listen(0, '127.0.0.1', () => { const port = (server.address() as net.AddressInfo).port; server.close(() => resolve(port)); }); server.on('error', reject); }); } private async executeSystemCommand( command: string, args: string[] ): Promise<string> { return new Promise((resolve, reject) => { const process = spawn(command, args); let output = ''; let error = ''; process.stdout?.on('data', (data) => { output += data.toString(); }); process.stderr?.on('data', (data) => { error += data.toString(); }); process.on('exit', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(error || `Command failed with code ${code}`)); } }); }); } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const session = this.x11vncSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } // X11VNC server commands if (command === 'refresh') { // Send refresh signal to x11vnc if (session.serverProcess) { session.serverProcess.kill('SIGUSR1'); } } else if (command === 'disconnect-clients') { // Disconnect all clients session.clientConnections.clear(); if (session.serverProcess) { session.serverProcess.kill('SIGUSR2'); } } else if (command === 'get-info') { // Get server information const info = { display: session.config.display, port: session.vncPort, httpPort: session.httpPort, clients: Array.from(session.clientConnections), performance: session.performanceMetrics, uptime: Date.now() - session.createdAt.getTime(), }; this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: JSON.stringify(info, null, 2) + '\n', timestamp: new Date(), }); } } async sendInput(sessionId: string, input: string): Promise<void> { // X11VNC doesn't accept input directly - it's a VNC server // Input would come from VNC clients this.logger.warn('Direct input not supported for X11VNC server sessions'); } async closeSession(sessionId: string): Promise<void> { const session = this.x11vncSessions.get(sessionId); if (!session) { return; } // Stop X11VNC server if (session.serverProcess && !session.serverProcess.killed) { session.serverProcess.kill('SIGTERM'); // Wait a bit, then force kill if needed setTimeout(() => { if (session.serverProcess && !session.serverProcess.killed) { session.serverProcess.kill('SIGKILL'); } }, 2000); } // Stop virtual display if we created one if (session.xvfbProcess && !session.xvfbProcess.killed) { session.xvfbProcess.kill('SIGTERM'); } // Cleanup temporary files await this.cleanupSession(sessionId); // Return port to pool if (session.vncPort) { this.usedPorts.delete(session.vncPort); this.availablePorts.add(session.vncPort); } session.status = 'stopped'; this.x11vncSessions.delete(sessionId); this.sessions.delete(sessionId); } private async cleanupSession(sessionId: string): Promise<void> { const session = this.x11vncSessions.get(sessionId); if (!session) return; // Remove password file if (session.passwordFile) { try { await fs.unlink(session.passwordFile); } catch (error) { this.logger.debug(`Failed to remove password file: ${error}`); } } // Remove log file if in production if (session.logFile && (this.logger as any).level !== 'debug') { try { await fs.unlink(session.logFile); } catch (error) { this.logger.debug(`Failed to remove log file: ${error}`); } } } async dispose(): Promise<void> { this.logger.info('Disposing X11VNC protocol'); // Close all sessions const sessionIds = Array.from(this.x11vncSessions.keys()); await Promise.all(sessionIds.map((id) => this.closeSession(id))); await this.cleanup(); } async getHealthStatus(): Promise<ProtocolHealthStatus> { const baseStatus = await super.getHealthStatus(); // Add X11VNC-specific health checks const x11vncAvailable = ((this as any)._dependencies as any)?.x11vnc || false; const hasDisplayServer = ((this as any)._dependencies as any)?.xvfb || ((this as any)._dependencies as any)?.xdummy || false; return { ...baseStatus, isHealthy: this.isInitialized && x11vncAvailable, warnings: [ ...baseStatus.warnings, !x11vncAvailable ? 'x11vnc not available' : null, !hasDisplayServer ? 'No virtual display server available (Xvfb/Xdummy)' : null, ].filter(Boolean) as string[], }; } } export default X11VNCProtocol;

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