Skip to main content
Glama
ooples

MCP Console Automation Server

TerraformProtocol.ts16.6 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'; // Terraform Protocol connection options interface TerraformConnectionOptions extends SessionOptions { terraformPath?: string; terraformVersion?: string; workingDir?: string; configFile?: string; varFile?: string[]; variables?: Record<string, string>; backend?: 'local' | 's3' | 'azurerm' | 'gcs' | 'consul' | 'etcd'; backendConfig?: Record<string, string>; stateFile?: string; workspace?: string; parallelism?: number; enableLogging?: boolean; logLevel?: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'; enableValidation?: boolean; enableRefresh?: boolean; autoApprove?: boolean; target?: string[]; replace?: string[]; environment?: Record<string, string>; cloudProvider?: 'aws' | 'azure' | 'gcp' | 'digitalocean' | 'cloudflare'; credentialsFile?: string; enableRemoteState?: boolean; enableModules?: boolean; moduleSource?: string[]; enableProviders?: boolean; providerSource?: string[]; dryRun?: boolean; destroyMode?: boolean; importMode?: boolean; importAddress?: string; importId?: string; enableGraph?: boolean; graphType?: 'plan' | 'apply' | 'validate' | 'plan-destroy'; } /** * Terraform Protocol Implementation * * Provides Terraform Infrastructure as Code console access through terraform command * Supports planning, applying, destroying, state management, workspaces, modules, and providers */ export class TerraformProtocol extends BaseProtocol { public readonly type: ConsoleType = 'terraform'; public readonly capabilities: ProtocolCapabilities; private terraformProcesses = 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('terraform'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: false, supportsX11Forwarding: false, supportsPortForwarding: false, supportsAuthentication: true, supportsEncryption: false, supportsCompression: false, supportsMultiplexing: false, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: false, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: false, supportsPTY: true, maxConcurrentSessions: 10, defaultTimeout: 300000, // Terraform operations can take very long supportedEncodings: ['utf-8'], supportedAuthMethods: ['api-key', 'service-account'], platformSupport: { windows: true, linux: true, macos: true, freebsd: true, }, }; } async initialize(): Promise<void> { if (this.isInitialized) return; try { // Check if Terraform is available await this.checkTerraformAvailability(); this.isInitialized = true; this.logger.info( 'Terraform protocol initialized with production features' ); } catch (error: any) { this.logger.error('Failed to initialize Terraform protocol', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { const sessionId = `terraform-${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 terraformProcess = this.terraformProcesses.get(sessionId); const session = this.sessions.get(sessionId); if (!terraformProcess || !terraformProcess.stdin || !session) { throw new Error(`No active Terraform session: ${sessionId}`); } terraformProcess.stdin.write(input); session.lastActivity = new Date(); this.emit('input-sent', { sessionId, input, timestamp: new Date(), }); this.logger.debug( `Sent input to Terraform session ${sessionId}: ${input.substring(0, 100)}` ); } async closeSession(sessionId: string): Promise<void> { try { const terraformProcess = this.terraformProcesses.get(sessionId); if (terraformProcess) { // Try graceful shutdown first terraformProcess.kill('SIGTERM'); // Force kill after timeout (longer for Terraform operations) setTimeout(() => { if (terraformProcess && !terraformProcess.killed) { terraformProcess.kill('SIGKILL'); } }, 15000); this.terraformProcesses.delete(sessionId); } // Clean up base protocol session this.sessions.delete(sessionId); this.logger.info(`Terraform session ${sessionId} closed`); this.emit('session-closed', sessionId); } catch (error) { this.logger.error(`Error closing Terraform session ${sessionId}:`, error); throw error; } } async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const terraformOptions = options as TerraformConnectionOptions; // Build Terraform command const terraformCommand = this.buildTerraformCommand(terraformOptions); // Spawn Terraform process const terraformProcess = spawn( terraformCommand[0], terraformCommand.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], cwd: options.cwd || terraformOptions.workingDir || process.cwd(), env: { ...process.env, ...this.buildEnvironment(terraformOptions), ...options.env, }, } ); // Set up output handling terraformProcess.stdout?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); terraformProcess.stderr?.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), }; this.addToOutputBuffer(sessionId, output); }); terraformProcess.on('error', (error) => { this.logger.error( `Terraform process error for session ${sessionId}:`, error ); this.emit('session-error', { sessionId, error }); }); terraformProcess.on('close', (code) => { this.logger.info( `Terraform process closed for session ${sessionId} with code ${code}` ); this.markSessionComplete(sessionId, code || 0); }); // Store the process this.terraformProcesses.set(sessionId, terraformProcess); // Create session object const session: ConsoleSession = { id: sessionId, command: terraformCommand[0], args: terraformCommand.slice(1), cwd: options.cwd || terraformOptions.workingDir || process.cwd(), env: { ...process.env, ...this.buildEnvironment(terraformOptions), ...options.env, }, createdAt: new Date(), lastActivity: new Date(), status: 'running', type: this.type, streaming: options.streaming, executionState: 'idle', activeCommands: new Map(), pid: terraformProcess.pid, }; this.sessions.set(sessionId, session); this.logger.info( `Terraform session ${sessionId} created for ${terraformOptions.configFile || terraformOptions.workingDir || 'Terraform project'}` ); this.emit('session-created', { sessionId, type: 'terraform', 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, // Terraform 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 Terraform session ${context.sessionId}: ${error.message}` ); return { recovered: false, strategy: 'none', attempts: 0, duration: 0, error: error.message, }; } async recoverSession(sessionId: string): Promise<boolean> { const terraformProcess = this.terraformProcesses.get(sessionId); return (terraformProcess && !terraformProcess.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.terraformProcesses.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.checkTerraformAvailability(); return { ...baseStatus, dependencies: { terraform: { available: true }, }, }; } catch (error) { return { ...baseStatus, isHealthy: false, errors: [...baseStatus.errors, `Terraform not available: ${error}`], dependencies: { terraform: { available: false }, }, }; } } private async checkTerraformAvailability(): Promise<void> { return new Promise((resolve, reject) => { const testProcess = spawn('terraform', ['version'], { stdio: 'pipe' }); testProcess.on('close', (code) => { if (code === 0) { resolve(); } else { reject( new Error('Terraform CLI not found. Please install Terraform.') ); } }); testProcess.on('error', () => { reject(new Error('Terraform CLI not found. Please install Terraform.')); }); }); } private buildTerraformCommand(options: TerraformConnectionOptions): string[] { const command = []; // Terraform executable if (options.terraformPath) { command.push(options.terraformPath); } else { command.push('terraform'); } // Determine Terraform subcommand based on options if (options.destroyMode) { command.push('destroy'); } else if ( options.importMode && options.importAddress && options.importId ) { command.push('import'); command.push(options.importAddress); command.push(options.importId); return command; } else if (options.enableGraph) { command.push('graph'); if (options.graphType) { command.push('-type', options.graphType); } } else if (options.dryRun) { command.push('plan'); } else { // Default to plan for safety command.push('plan'); } // Configuration file if (options.configFile) { command.push('-chdir', options.configFile); } // Variable files if (options.varFile) { for (const varFile of options.varFile) { command.push('-var-file', varFile); } } // Variables if (options.variables) { for (const [key, value] of Object.entries(options.variables)) { command.push('-var', `${key}=${value}`); } } // State file if (options.stateFile) { command.push('-state', options.stateFile); } // Backend configuration if (options.backendConfig) { for (const [key, value] of Object.entries(options.backendConfig)) { command.push('-backend-config', `${key}=${value}`); } } // Parallelism if (options.parallelism) { command.push('-parallelism', options.parallelism.toString()); } // Auto approve for apply/destroy if ( options.autoApprove && (command.includes('apply') || command.includes('destroy')) ) { command.push('-auto-approve'); } // Target resources if (options.target) { for (const target of options.target) { command.push('-target', target); } } // Replace resources if (options.replace) { for (const replace of options.replace) { command.push('-replace', replace); } } // Refresh option if (options.enableRefresh === false) { command.push('-refresh=false'); } // Application arguments if (options.args) { command.push(...options.args); } return command; } private buildEnvironment( options: TerraformConnectionOptions ): Record<string, string> { const env: Record<string, string> = {}; // Terraform logging if (options.enableLogging) { env.TF_LOG = options.logLevel || 'INFO'; env.TF_LOG_PATH = './terraform.log'; } // Workspace if (options.workspace) { env.TF_WORKSPACE = options.workspace; } // Cloud provider credentials if (options.cloudProvider === 'aws') { if (options.credentialsFile) { env.AWS_SHARED_CREDENTIALS_FILE = options.credentialsFile; } } else if (options.cloudProvider === 'azure') { if (options.credentialsFile) { env.AZURE_CLIENT_CERTIFICATE_PATH = options.credentialsFile; } } else if (options.cloudProvider === 'gcp') { if (options.credentialsFile) { env.GOOGLE_APPLICATION_CREDENTIALS = options.credentialsFile; } } // Plugin cache env.TF_PLUGIN_CACHE_DIR = process.env.TF_PLUGIN_CACHE_DIR || '~/.terraform.d/plugin-cache'; // Disable telemetry for CI environments env.CHECKPOINT_DISABLE = '1'; // Enable CLI UI env.TF_CLI_ARGS = '-no-color'; // Input handling env.TF_INPUT = '0'; // Custom environment variables if (options.environment) { Object.assign(env, options.environment); } return env; } async cleanup(): Promise<void> { this.logger.info('Cleaning up Terraform protocol'); // Close all Terraform processes for (const [sessionId, process] of Array.from(this.terraformProcesses)) { try { process.kill(); } catch (error) { this.logger.error( `Error killing Terraform process for session ${sessionId}:`, error ); } } // Clear all data this.terraformProcesses.clear(); // Call parent cleanup await super.cleanup(); } } export default TerraformProtocol;

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