Skip to main content
Glama

tfmcp

by nwiizo
use crate::shared::security::SecurityManager; use crate::terraform::model::{ DetailedValidationResult, TerraformAnalysis, TerraformValidateOutput, }; use crate::terraform::parser::TerraformParser; use std::path::{Path, PathBuf}; use std::process::Command; use thiserror::Error; #[derive(Error, Debug)] #[allow(dead_code)] pub enum TerraformError { #[error("Terraform command failed: {0}")] CommandError(String), #[error("Terraform binary not found at path: {0}")] BinaryNotFound(String), #[error("Invalid JSON output: {0}")] JsonParseError(String), #[error("IO error: {0}")] IoError(#[from] std::io::Error), #[error("Terraform init required")] InitRequired, } pub struct TerraformService { terraform_path: PathBuf, project_directory: PathBuf, security_manager: SecurityManager, } impl TerraformService { pub fn new(terraform_path: PathBuf, project_directory: PathBuf) -> Self { eprintln!( "[DEBUG] TerraformService initialized with terraform path: {} and project directory: {}", terraform_path.display(), project_directory.display() ); let security_manager = SecurityManager::new().unwrap_or_else(|e| { eprintln!("[WARN] Failed to initialize security manager: {}", e); // Create a default security manager with basic settings SecurityManager { policy: crate::shared::security::SecurityPolicy::default(), audit_log: None, } }); Self { terraform_path, project_directory, security_manager, } } #[allow(dead_code)] pub fn set_project_directory(&mut self, directory: PathBuf) { eprintln!( "[DEBUG] Setting project directory to: {}", directory.display() ); self.project_directory = directory; } pub fn change_project_directory(&mut self, directory: PathBuf) -> anyhow::Result<()> { eprintln!( "[DEBUG] Changing project directory to: {}", directory.display() ); self.project_directory = directory; Ok(()) } pub fn get_project_directory(&self) -> &PathBuf { &self.project_directory } pub async fn get_version(&self) -> anyhow::Result<String> { let output = Command::new(&self.terraform_path) .arg("version") .arg("-json") .output()?; let output_str = String::from_utf8_lossy(&output.stdout); // Parse JSON output if let Ok(json) = serde_json::from_str::<serde_json::Value>(&output_str) { if let Some(version) = json.get("terraform_version") { return Ok(version.to_string().trim_matches('"').to_string()); } } // Fallback to non-JSON output let output = Command::new(&self.terraform_path).arg("version").output()?; let version_output = String::from_utf8_lossy(&output.stdout); let version_line = version_output .lines() .find(|line| line.starts_with("Terraform") || line.starts_with("OpenTofu")) .unwrap_or("Unknown version"); Ok(version_line.to_string()) } pub async fn init(&self) -> anyhow::Result<String> { let output = Command::new(&self.terraform_path) .arg("init") .current_dir(&self.project_directory) .output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(anyhow::anyhow!( "Terraform init failed: {}", String::from_utf8_lossy(&output.stderr) )) } } pub async fn get_plan(&self) -> anyhow::Result<String> { let output = Command::new(&self.terraform_path) .arg("plan") .arg("-json") .current_dir(&self.project_directory) .output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { let stderr = String::from_utf8_lossy(&output.stderr); if stderr.contains("terraform init") { Err(anyhow::anyhow!( "Terraform initialization required. Please run 'terraform init' first." )) } else { Err(anyhow::anyhow!("Terraform plan failed: {}", stderr)) } } } pub async fn apply(&self, auto_approve: bool) -> anyhow::Result<String> { // Security checks if !self.security_manager.is_command_allowed("apply") { return Err(anyhow::anyhow!( "Apply operation blocked by security policy. Set TFMCP_ALLOW_DANGEROUS_OPS=true to enable." )); } if auto_approve && !self.security_manager.is_auto_approve_allowed("apply") { return Err(anyhow::anyhow!( "Auto-approve for apply operation blocked by security policy. Set TFMCP_ALLOW_AUTO_APPROVE=true to enable." )); } // Validate directory security self.security_manager .validate_directory(&self.project_directory)?; // Check resource limits if let Ok(resources) = self.list_resources().await { self.security_manager .check_resource_limit(resources.len())?; } let mut cmd = Command::new(&self.terraform_path); cmd.arg("apply"); if auto_approve { cmd.arg("-auto-approve"); } let command_args = vec!["terraform".to_string(), "apply".to_string()]; let output = cmd.current_dir(&self.project_directory).output()?; let success = output.status.success(); // Log audit entry let error_msg = if !success { Some(String::from_utf8_lossy(&output.stderr).to_string()) } else { None }; let resource_count = if success { self.list_resources().await.ok().map(|r| r.len()) } else { None }; let audit_entry = self.security_manager.create_audit_entry( "apply", &self.project_directory.to_string_lossy(), &command_args, success, error_msg.clone(), resource_count, ); if let Err(e) = self.security_manager.log_audit_entry(audit_entry) { eprintln!("[WARN] Failed to log audit entry: {}", e); } if success { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(anyhow::anyhow!( "Terraform apply failed: {}", String::from_utf8_lossy(&output.stderr) )) } } pub async fn get_state(&self) -> anyhow::Result<String> { let output = Command::new(&self.terraform_path) .arg("state") .arg("list") .current_dir(&self.project_directory) .output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(anyhow::anyhow!( "Failed to get Terraform state: {}", String::from_utf8_lossy(&output.stderr) )) } } #[allow(dead_code)] pub async fn refresh(&self) -> anyhow::Result<String> { let output = Command::new(&self.terraform_path) .arg("refresh") .current_dir(&self.project_directory) .output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(anyhow::anyhow!( "Terraform refresh failed: {}", String::from_utf8_lossy(&output.stderr) )) } } #[allow(dead_code)] pub async fn create_terraform_configuration(&self, content: &str) -> anyhow::Result<String> { // Write the content to a main.tf file in the project directory let file_path = self.project_directory.join("main.tf"); std::fs::write(&file_path, content)?; Ok(format!( "Terraform configuration created at: {}", file_path.display() )) } #[allow(dead_code)] pub async fn read_terraform_file(&self, filename: &str) -> anyhow::Result<String> { let file_path = self.project_directory.join(filename); match std::fs::read_to_string(&file_path) { Ok(content) => Ok(content), Err(e) => Err(anyhow::anyhow!( "Failed to read file {}: {}", file_path.display(), e )), } } pub async fn list_resources(&self) -> anyhow::Result<Vec<String>> { let output = Command::new(&self.terraform_path) .arg("state") .arg("list") .current_dir(&self.project_directory) .output()?; if !output.status.success() { return Err(anyhow::anyhow!( "Failed to list resources: {}", String::from_utf8_lossy(&output.stderr) )); } let resources = String::from_utf8_lossy(&output.stdout) .lines() .map(|s| s.to_string()) .collect(); Ok(resources) } pub async fn validate(&self) -> anyhow::Result<String> { let output = Command::new(&self.terraform_path) .arg("validate") .arg("-json") .current_dir(&self.project_directory) .output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(anyhow::anyhow!( "Terraform validate failed: {}", String::from_utf8_lossy(&output.stderr) )) } } pub async fn validate_detailed(&self) -> anyhow::Result<DetailedValidationResult> { // Run terraform validate with JSON output let validate_json = self.validate().await?; let validate_output: TerraformValidateOutput = serde_json::from_str(&validate_json)?; // Additional validation checks let mut warnings = Vec::new(); let mut suggestions = Vec::new(); // Check for .tf files in the directory let tf_files = self.find_terraform_files().await?; if tf_files.is_empty() { warnings.push("No Terraform configuration files found in the directory".to_string()); } // Analyze configuration for best practices if !tf_files.is_empty() { let analysis = self.analyze_configurations().await?; // Check for missing descriptions for var in &analysis.variables { if var.description.is_none() || var .description .as_ref() .map(|d| d.is_empty()) .unwrap_or(false) { suggestions.push(format!("Variable '{}' is missing a description", var.name)); } } // Check for hardcoded values that should be variables for resource in &analysis.resources { if resource.provider == "aws" && resource.resource_type.contains("instance") { suggestions.push(format!( "Consider using variables for AWS instance configurations in resource '{}'", resource.name )); } } // Check for missing output descriptions for output in &analysis.outputs { if output.description.is_none() || output .description .as_ref() .map(|d| d.is_empty()) .unwrap_or(false) { suggestions.push(format!("Output '{}' is missing a description", output.name)); } } // Check for provider version constraints if analysis.providers.iter().any(|p| p.version.is_none()) { warnings.push("Some providers are missing version constraints".to_string()); } } Ok(DetailedValidationResult { valid: validate_output.valid, error_count: validate_output.error_count, warning_count: validate_output.warning_count + warnings.len() as i32, diagnostics: validate_output.diagnostics, additional_warnings: warnings, suggestions, checked_files: tf_files.len(), }) } async fn find_terraform_files(&self) -> anyhow::Result<Vec<String>> { let mut tf_files = Vec::new(); let entries = std::fs::read_dir(&self.project_directory)?; for entry in entries { let entry = entry?; let path = entry.path(); if path.is_file() { if let Some(ext) = path.extension() { if ext == "tf" || ext == "tf.json" { if let Some(name) = path.file_name() { tf_files.push(name.to_string_lossy().to_string()); } } } } } Ok(tf_files) } pub async fn destroy(&self, auto_approve: bool) -> anyhow::Result<String> { // Security checks if !self.security_manager.is_command_allowed("destroy") { return Err(anyhow::anyhow!( "Destroy operation blocked by security policy. Set TFMCP_ALLOW_DANGEROUS_OPS=true to enable." )); } if auto_approve && !self.security_manager.is_auto_approve_allowed("destroy") { return Err(anyhow::anyhow!( "Auto-approve for destroy operation blocked by security policy. Set TFMCP_ALLOW_AUTO_APPROVE=true to enable." )); } // Validate directory security self.security_manager .validate_directory(&self.project_directory)?; let mut cmd = Command::new(&self.terraform_path); cmd.arg("destroy"); if auto_approve { cmd.arg("-auto-approve"); } let command_args = vec!["terraform".to_string(), "destroy".to_string()]; let output = cmd.current_dir(&self.project_directory).output()?; let success = output.status.success(); // Log audit entry let error_msg = if !success { Some(String::from_utf8_lossy(&output.stderr).to_string()) } else { None }; let audit_entry = self.security_manager.create_audit_entry( "destroy", &self.project_directory.to_string_lossy(), &command_args, success, error_msg.clone(), None, // Resource count not applicable for destroy ); if let Err(e) = self.security_manager.log_audit_entry(audit_entry) { eprintln!("[WARN] Failed to log audit entry: {}", e); } if success { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(anyhow::anyhow!( "Terraform destroy failed: {}", String::from_utf8_lossy(&output.stderr) )) } } pub async fn analyze_configurations(&self) -> anyhow::Result<TerraformAnalysis> { eprintln!( "[DEBUG] Analyzing Terraform configurations in {}", self.project_directory.display() ); // Check if the directory exists if !self.project_directory.exists() { return Err(anyhow::anyhow!( "Project directory does not exist: {}", self.project_directory.display() )); } // Find all .tf files in the project directory let entries = std::fs::read_dir(&self.project_directory)?; let mut tf_files = Vec::new(); for entry in entries.flatten() { let path = entry.path(); if path.is_file() && path.extension().is_some_and(|ext| ext == "tf") { eprintln!("[DEBUG] Found Terraform file: {}", path.display()); tf_files.push(path); } } if tf_files.is_empty() { eprintln!( "[WARN] No Terraform (.tf) files found in {}", self.project_directory.display() ); return Err(anyhow::anyhow!( "No Terraform (.tf) files found in {}", self.project_directory.display() )); } let mut analysis = TerraformAnalysis { project_directory: self.project_directory.to_string_lossy().to_string(), file_count: tf_files.len(), resources: Vec::new(), variables: Vec::new(), outputs: Vec::new(), providers: Vec::new(), }; // Parse each file to identify resources, variables, outputs for file_path in tf_files { eprintln!("[DEBUG] Analyzing file: {}", file_path.display()); match self.analyze_file(&file_path, &mut analysis) { Ok(_) => eprintln!("[DEBUG] Successfully analyzed {}", file_path.display()), Err(e) => eprintln!("[ERROR] Failed to analyze {}: {}", file_path.display(), e), } } eprintln!("[INFO] Terraform analysis complete: found {} resources, {} variables, {} outputs, {} providers", analysis.resources.len(), analysis.variables.len(), analysis.outputs.len(), analysis.providers.len()); Ok(analysis) } fn analyze_file( &self, file_path: &Path, analysis: &mut TerraformAnalysis, ) -> anyhow::Result<()> { eprintln!("[DEBUG] Reading file: {}", file_path.display()); let content = match std::fs::read_to_string(file_path) { Ok(content) => content, Err(e) => { eprintln!("[ERROR] Failed to read file {}: {}", file_path.display(), e); return Err(anyhow::anyhow!("Failed to read file: {}", e)); } }; let file_name = file_path.file_name().unwrap_or_default().to_string_lossy(); let parser = TerraformParser::new(content); // Parse resources eprintln!("[DEBUG] Parsing resources in {}", file_path.display()); let resources = parser.parse_resources(&file_name); for resource in &resources { eprintln!( "[DEBUG] Found resource: {} ({})", resource.name, resource.resource_type ); } analysis.resources.extend(resources); // Parse variables eprintln!("[DEBUG] Parsing variables in {}", file_path.display()); let variables = parser.parse_variables(); for variable in &variables { eprintln!("[DEBUG] Found variable: {}", variable.name); } analysis.variables.extend(variables); // Parse outputs eprintln!("[DEBUG] Parsing outputs in {}", file_path.display()); let outputs = parser.parse_outputs(); for output in &outputs { eprintln!("[DEBUG] Found output: {}", output.name); } analysis.outputs.extend(outputs); // Parse providers eprintln!("[DEBUG] Parsing providers in {}", file_path.display()); let providers = parser.parse_providers(); for provider in providers { // Check if provider already exists if !analysis.providers.iter().any(|p| p.name == provider.name) { eprintln!("[DEBUG] Found provider: {}", provider.name); analysis.providers.push(provider); } } eprintln!("[DEBUG] Completed analysis of {}", file_path.display()); Ok(()) } /// Get current security policy for debugging/reporting #[allow(dead_code)] pub fn get_security_policy(&self) -> &crate::shared::security::SecurityPolicy { self.security_manager.get_policy() } /// Check if a specific operation is allowed by security policy #[allow(dead_code)] pub fn is_operation_allowed(&self, operation: &str) -> bool { self.security_manager.is_command_allowed(operation) } }

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/nwiizo/tfmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server