Skip to main content
Glama
ooples

MCP Console Automation Server

XenProtocol.ts27.3 kB
import { ChildProcess, spawn } from 'child_process'; import * as https from 'https'; import * as http from 'http'; 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'; /** * Xen connection configuration */ interface XenConfig { host?: string; port?: number; username?: string; password?: string; sessionId?: string; poolMaster?: string; useSSL?: boolean; apiVersion?: string; xenToolsPath?: string; connectionType?: 'xe' | 'xapi' | 'xl' | 'ssh'; sshKeyFile?: string; timeout?: number; } /** * VM (Virtual Machine) information */ interface XenVM { uuid: string; name: string; powerState: 'Running' | 'Halted' | 'Paused' | 'Suspended'; cpus: number; memory: number; disks: XenVDI[]; networks: XenVIF[]; host?: string; osVersion?: string; pvDriversVersion?: string; metrics?: XenVMMetrics; } /** * Virtual Disk Image */ interface XenVDI { uuid: string; name?: string; size: number; type: 'system' | 'user' | 'ephemeral' | 'suspend' | 'crashdump'; sr: string; // Storage Repository } /** * Virtual Interface */ interface XenVIF { uuid: string; device: string; mac: string; network: string; ipv4?: string[]; ipv6?: string[]; } /** * VM metrics */ interface XenVMMetrics { cpuUsage: number; memoryUsage: number; diskRead: number; diskWrite: number; networkRead: number; networkWrite: number; uptime: number; } /** * Host information */ interface XenHost { uuid: string; name: string; address: string; cpuInfo: { count: number; speed: number; model: string; }; memory: { total: number; free: number; }; software: { version: string; build: string; }; } /** * Xen session state */ interface XenSession extends ConsoleSession { config: XenConfig; process?: ChildProcess; apiSession?: string; isConnected: boolean; lastCommand?: string; vmConsoles: Map<string, ChildProcess>; // VM UUID -> console process cachedHosts?: XenHost[]; cachedVMs?: XenVM[]; commandQueue: string[]; interactive: boolean; } /** * Xen Hypervisor Protocol Implementation * Supports XenServer, Xen Cloud Platform (XCP), and Xen open source hypervisor */ export class XenProtocol extends BaseProtocol { public readonly type: ConsoleType = 'xen'; public readonly capabilities: ProtocolCapabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: false, supportsPortForwarding: true, supportsAuthentication: true, supportsEncryption: true, supportsCompression: true, supportsMultiplexing: true, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: false, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: false, supportsPTY: true, maxConcurrentSessions: 20, defaultTimeout: 30000, supportedEncodings: ['utf-8'], supportedAuthMethods: ['password', 'session', 'key'], platformSupport: { windows: false, linux: true, macos: false, freebsd: true, }, }; private xenSessions: Map<string, XenSession> = new Map(); private defaultConfig: Partial<XenConfig> = { port: 443, useSSL: true, apiVersion: '2.0', connectionType: 'xe', timeout: 30000, }; constructor() { super('XenProtocol'); } async initialize(): Promise<void> { this.logger.info('Initializing Xen protocol'); try { await this.checkDependencies(); this.isInitialized = true; this.logger.info('Xen protocol initialized successfully'); } catch (error) { this.logger.error('Failed to initialize Xen protocol:', error); throw error; } } private async checkDependencies(): Promise<void> { const dependencies: Record<string, boolean> = {}; // Check for xe CLI tool try { await this.executeSystemCommand('xe', ['--version']); dependencies.xe = true; } catch { dependencies.xe = false; this.logger.debug('xe CLI not found'); } // Check for xl tool (Xen management tool) try { await this.executeSystemCommand('xl', ['info']); dependencies.xl = true; } catch { dependencies.xl = false; this.logger.debug('xl tool not found'); } // Check for xm tool (legacy) try { await this.executeSystemCommand('xm', ['info']); dependencies.xm = true; } catch { dependencies.xm = false; } // Check for virsh with Xen support try { const result = await this.executeSystemCommand('virsh', ['--version']); if (result.includes('Xen')) { dependencies.virsh = true; } } catch { dependencies.virsh = false; } // Store dependencies for later use (this as any)._dependencies = dependencies; } /** * 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 `xen-${Date.now()}-${Math.random().toString(36).substring(7)}`; } protected async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { const config = this.parseXenOptions(options); const session: XenSession = { id: sessionId, type: 'xen', status: 'initializing', command: options.command || 'xe', args: options.args || [], cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.env }, createdAt: new Date(), streaming: options.streaming ?? true, config, isConnected: false, vmConsoles: new Map(), commandQueue: [], interactive: !sessionState.isOneShot, executionState: 'idle' as const, activeCommands: new Map(), }; this.xenSessions.set(sessionId, session); this.sessions.set(sessionId, session); try { // Connect to Xen based on connection type await this.connectToXen(sessionId, session); session.status = 'running'; sessionState.status = 'running'; return session; } catch (error) { session.status = 'stopped'; session.exitCode = 1; sessionState.status = 'stopped'; throw error; } } private parseXenOptions(options: SessionOptions): XenConfig { const config: XenConfig = { ...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 '-s': case '--server': case '--host': config.host = nextArg; i++; break; case '-p': case '--port': config.port = parseInt(nextArg); i++; break; case '-u': case '--username': config.username = nextArg; i++; break; case '--password': case '-pw': config.password = nextArg; i++; break; case '--session': config.sessionId = nextArg; i++; break; case '--nossl': config.useSSL = false; break; case '--key': config.sshKeyFile = nextArg; i++; break; } } } // Parse from environment variables if (options.env) { if (options.env.XEN_HOST) config.host = options.env.XEN_HOST; if (options.env.XEN_USER) config.username = options.env.XEN_USER; if (options.env.XEN_PASSWORD) config.password = options.env.XEN_PASSWORD; if (options.env.XEN_PORT) config.port = parseInt(options.env.XEN_PORT); } // Determine connection type based on available tools const deps = (this as any)._dependencies || {}; if (!config.connectionType) { if (deps.xe) { config.connectionType = 'xe'; } else if (deps.xl) { config.connectionType = 'xl'; } else { config.connectionType = 'xapi'; // Try XAPI } } return config; } private async connectToXen( sessionId: string, session: XenSession ): Promise<void> { const config = session.config; switch (config.connectionType) { case 'xe': await this.connectWithXE(sessionId, session); break; case 'xl': await this.connectWithXL(sessionId, session); break; case 'xapi': await this.connectWithXAPI(sessionId, session); break; case 'ssh': await this.connectWithSSH(sessionId, session); break; default: throw new Error(`Unknown connection type: ${config.connectionType}`); } } private async connectWithXE( sessionId: string, session: XenSession ): Promise<void> { const config = session.config; if (session.interactive) { // Interactive xe shell const args: string[] = []; // Add server connection if remote if (config.host) { args.push('-s', config.host); if (config.port && config.port !== 443) { args.push('-p', config.port.toString()); } } // Add credentials if (config.username) { args.push('-u', config.username); } if (config.password) { args.push('-pw', config.password); } // Start xe process session.process = spawn('xe', args, { cwd: session.cwd, env: session.env, }); session.isConnected = true; // Handle process output session.process.stdout?.on('data', (data: Buffer) => { this.handleXEOutput(sessionId, data.toString()); }); session.process.stderr?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }); }); session.process.on('exit', (code, signal) => { session.isConnected = false; session.exitCode = code ?? undefined; session.status = 'stopped'; this.markSessionComplete(sessionId, code ?? 0); }); // Verify connection await this.executeXECommand(sessionId, 'host-list', [ 'params=uuid,name-label', ]); } else { // One-shot xe command const command = session.args[0] || 'host-list'; const cmdArgs = session.args.slice(1); const args: string[] = []; // Add server connection if (config.host) { args.push('-s', config.host); } if (config.username) { args.push('-u', config.username); } if (config.password) { args.push('-pw', config.password); } // Add the actual command args.push(command, ...cmdArgs); // Execute command const result = await this.executeSystemCommand('xe', args); this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: result, timestamp: new Date(), }); session.isConnected = false; session.status = 'stopped'; this.markSessionComplete(sessionId, 0); } } private async connectWithXL( sessionId: string, session: XenSession ): Promise<void> { // xl is typically used for local Xen management if (session.config.host && session.config.host !== 'localhost') { throw new Error( 'xl tool only supports local Xen management. Use SSH for remote.' ); } if (session.interactive) { // Start xl shell session.process = spawn('xl', ['shell'], { cwd: session.cwd, env: session.env, }); session.isConnected = true; session.process.stdout?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }); }); session.process.stderr?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }); }); session.process.on('exit', (code, signal) => { session.isConnected = false; session.exitCode = code ?? undefined; session.status = 'stopped'; }); } else { // One-shot xl command const command = session.args[0] || 'list'; const cmdArgs = session.args.slice(1); const result = await this.executeSystemCommand('xl', [ command, ...cmdArgs, ]); this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: result, timestamp: new Date(), }); session.status = 'stopped'; this.markSessionComplete(sessionId, 0); } } private async connectWithXAPI( sessionId: string, session: XenSession ): Promise<void> { const config = session.config; if (!config.host) { throw new Error('XAPI connection requires a host'); } // Login to XAPI const apiSession = await this.xapiLogin(config); session.apiSession = apiSession; session.isConnected = true; this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: `Connected to Xen API at ${config.host}\nSession: ${apiSession}\n`, timestamp: new Date(), }); // If one-shot, mark as complete if (!session.interactive) { session.status = 'stopped'; this.markSessionComplete(sessionId, 0); } } private async connectWithSSH( sessionId: string, session: XenSession ): Promise<void> { const config = session.config; if (!config.host) { throw new Error('SSH connection requires a host'); } const args: string[] = []; // SSH options if (config.sshKeyFile) { args.push('-i', config.sshKeyFile); } if (config.port && config.port !== 22) { args.push('-p', config.port.toString()); } // User@host const target = config.username ? `${config.username}@${config.host}` : config.host; args.push(target); // If one-shot, add the command if (!session.interactive && session.args.length > 0) { args.push('xe', ...session.args); } // Start SSH process session.process = spawn('ssh', args, { cwd: session.cwd, env: session.env, }); session.isConnected = true; session.process.stdout?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }); }); session.process.stderr?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }); }); session.process.on('exit', (code, signal) => { session.isConnected = false; session.exitCode = code ?? undefined; session.status = 'stopped'; this.markSessionComplete(sessionId, code ?? 0); }); } private async xapiLogin(config: XenConfig): Promise<string> { return new Promise((resolve, reject) => { const protocol = config.useSSL ? https : http; const postData = JSON.stringify({ jsonrpc: '2.0', method: 'session.login_with_password', params: [config.username || 'root', config.password || ''], id: 1, }); const options = { hostname: config.host, port: config.port || 443, path: '/jsonrpc', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), }, rejectUnauthorized: false, // Allow self-signed certificates }; const req = protocol.request(options, (res) => { let data = ''; res.on('data', (chunk) => (data += chunk)); res.on('end', () => { try { const response = JSON.parse(data); if (response.result && response.result.Status === 'Success') { resolve(response.result.Value); } else { reject(new Error(response.error?.message || 'Login failed')); } } catch (error) { reject(error); } }); }); req.on('error', reject); req.write(postData); req.end(); }); } private async executeXECommand( sessionId: string, command: string, args: string[] ): Promise<string> { const session = this.xenSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } if (session.process && session.process.stdin) { // Send command to xe process const fullCommand = `${command} ${args.join(' ')}\n`; session.process.stdin.write(fullCommand); session.lastCommand = command; return ''; } else { // Execute as separate command const config = session.config; const xeArgs: string[] = []; if (config.host) { xeArgs.push('-s', config.host); } if (config.username) { xeArgs.push('-u', config.username); } if (config.password) { xeArgs.push('-pw', config.password); } xeArgs.push(command, ...args); return await this.executeSystemCommand('xe', xeArgs); } } private handleXEOutput(sessionId: string, output: string): void { const session = this.xenSessions.get(sessionId); if (!session) return; // Parse xe output for special cases if (output.includes('uuid')) { // Parse VM or host information this.parseXenObjects(sessionId, output); } this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: output, timestamp: new Date(), }); } private parseXenObjects(sessionId: string, output: string): void { const session = this.xenSessions.get(sessionId); if (!session) return; // Simple parsing of xe output format const lines = output.split('\n'); let currentObject: any = {}; for (const line of lines) { if (line.trim() === '') { // End of object if (currentObject.uuid) { this.emit('xen-object', { sessionId, object: currentObject }); } currentObject = {}; } else { const match = line.match(/^\s*([^:]+)\s*:\s*(.*)$/); if (match) { const [, key, value] = match; currentObject[key.trim().replace(/-/g, '_')] = value.trim(); } } } } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const session = this.xenSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } // Handle Xen-specific commands switch (command) { case 'vm-list': await this.executeXECommand(sessionId, 'vm-list', args || []); break; case 'vm-start': if (!args || args.length === 0) { throw new Error('vm-start requires VM UUID or name'); } await this.executeXECommand(sessionId, 'vm-start', [`uuid=${args[0]}`]); break; case 'vm-shutdown': if (!args || args.length === 0) { throw new Error('vm-shutdown requires VM UUID or name'); } await this.executeXECommand(sessionId, 'vm-shutdown', [ `uuid=${args[0]}`, ]); break; case 'vm-reboot': if (!args || args.length === 0) { throw new Error('vm-reboot requires VM UUID or name'); } await this.executeXECommand(sessionId, 'vm-reboot', [ `uuid=${args[0]}`, ]); break; case 'vm-console': if (!args || args.length === 0) { throw new Error('vm-console requires VM UUID or name'); } await this.attachVMConsole(sessionId, args[0]); break; case 'host-list': await this.executeXECommand(sessionId, 'host-list', args || []); break; case 'sr-list': // Storage repositories await this.executeXECommand(sessionId, 'sr-list', args || []); break; case 'network-list': await this.executeXECommand(sessionId, 'network-list', args || []); break; case 'task-list': await this.executeXECommand(sessionId, 'task-list', args || []); break; default: // Pass through to xe await this.executeXECommand(sessionId, command, args || []); } } private async attachVMConsole( sessionId: string, vmId: string ): Promise<void> { const session = this.xenSessions.get(sessionId); if (!session) return; // Get VM console using xe const consoleProcess = spawn( 'xe', [ 'console', `uuid=${vmId}`, ...(session.config.host ? ['-s', session.config.host] : []), ...(session.config.username ? ['-u', session.config.username] : []), ...(session.config.password ? ['-pw', session.config.password] : []), ], { cwd: session.cwd, env: session.env, } ); session.vmConsoles.set(vmId, consoleProcess); consoleProcess.stdout?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: `[VM ${vmId}] ${data.toString()}`, timestamp: new Date(), }); }); consoleProcess.stderr?.on('data', (data: Buffer) => { this.addToOutputBuffer(sessionId, { sessionId, type: 'stderr', data: `[VM ${vmId}] ${data.toString()}`, timestamp: new Date(), }); }); consoleProcess.on('exit', () => { session.vmConsoles.delete(vmId); this.addToOutputBuffer(sessionId, { sessionId, type: 'stdout', data: `Console disconnected from VM ${vmId}\n`, timestamp: new Date(), }); }); } async sendInput(sessionId: string, input: string): Promise<void> { const session = this.xenSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } // Check if input is for a VM console if (input.startsWith('vm:')) { const match = input.match(/^vm:([^:]+):(.*)$/); if (match) { const [, vmId, vmInput] = match; const consoleProcess = session.vmConsoles.get(vmId); if (consoleProcess && consoleProcess.stdin) { consoleProcess.stdin.write(vmInput + '\n'); return; } } } // Send to main process if (session.process && session.process.stdin) { session.process.stdin.write(input); if (!input.endsWith('\n')) { session.process.stdin.write('\n'); } } else { // Queue command for XAPI session.commandQueue.push(input); } } async closeSession(sessionId: string): Promise<void> { const session = this.xenSessions.get(sessionId); if (!session) { return; } // Close all VM consoles for (const [vmId, process] of session.vmConsoles) { if (!process.killed) { process.kill('SIGTERM'); } } session.vmConsoles.clear(); // Logout from XAPI if connected if (session.apiSession && session.config.host) { try { await this.xapiLogout(session.config, session.apiSession); } catch (error) { this.logger.debug('Failed to logout from XAPI:', error); } } // Kill main process if (session.process && !session.process.killed) { session.process.kill('SIGTERM'); // Wait a bit, then force kill if needed setTimeout(() => { if (session.process && !session.process.killed) { session.process.kill('SIGKILL'); } }, 2000); } session.status = 'stopped'; this.xenSessions.delete(sessionId); this.sessions.delete(sessionId); } private async xapiLogout( config: XenConfig, sessionId: string ): Promise<void> { return new Promise((resolve) => { const protocol = config.useSSL ? https : http; const postData = JSON.stringify({ jsonrpc: '2.0', method: 'session.logout', params: [sessionId], id: 1, }); const options = { hostname: config.host, port: config.port || 443, path: '/jsonrpc', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), }, rejectUnauthorized: false, }; const req = protocol.request(options, (res) => { res.on('data', () => {}); // Drain response res.on('end', resolve); }); req.on('error', () => resolve()); // Ignore errors req.write(postData); req.end(); }); } 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 dispose(): Promise<void> { this.logger.info('Disposing Xen protocol'); // Close all sessions const sessionIds = Array.from(this.xenSessions.keys()); await Promise.all(sessionIds.map((id) => this.closeSession(id))); await this.cleanup(); } async getHealthStatus(): Promise<ProtocolHealthStatus> { const baseStatus = await super.getHealthStatus(); const deps = (this as any)._dependencies || {}; // Check if any Xen tool is available const hasXenTool = deps.xe || deps.xl || deps.xm || deps.virsh; return { ...baseStatus, isHealthy: this.isInitialized && hasXenTool, warnings: [ ...baseStatus.warnings, !hasXenTool ? 'No Xen management tools available' : null, !deps.xe ? 'xe CLI not available (required for XenServer/XCP)' : null, ].filter(Boolean) as string[], }; } } export default XenProtocol;

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