tfmcp 0.2.1

Terraform Model Context Protocol Tool - A CLI tool to manage Terraform through MCP
Documentation
use clap::{Parser, Subcommand};
use tfmcp::TfMcp;
use tfmcp::mcp::server::{TfMcpServer, ToolFilter};
use tfmcp::mcp::transport::{HttpTransportConfig, TransportMode};
use tfmcp::shared::logging;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

const APP_VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Parser)]
#[command(
    name = "tfmcp",
    about = "✨ A CLI tool to manage Terraform configurations and operate Terraform through the Model Context Protocol (MCP).",
    version = APP_VERSION,
    disable_version_flag(true)
)]
pub struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    #[arg(
        long,
        short = 'c',
        value_name = "PATH",
        help = "Path to the configuration file"
    )]
    pub config: Option<String>,

    #[arg(
        long,
        short = 'd',
        value_name = "PATH",
        help = "Terraform project directory"
    )]
    pub dir: Option<String>,

    #[arg(long, short = 'V', help = "Print version")]
    pub version: bool,
}

#[derive(Subcommand)]
enum Commands {
    #[command(name = "mcp", about = "Launch tfmcp as an MCP server")]
    Mcp {
        /// Tool categories to enable (comma-separated): default, terraform, registry, analysis, operations, all
        #[arg(long, default_value = "all", value_delimiter = ',')]
        toolsets: Vec<String>,
        /// Individual tools to enable (comma-separated, overrides --toolsets)
        #[arg(long, value_delimiter = ',')]
        tools: Option<Vec<String>>,
    },

    #[command(name = "analyze", about = "Analyze Terraform configurations")]
    Analyze,
}

#[tokio::main]
async fn main() {
    // Initialize tracing/logging
    init_logging();

    let cli = Cli::parse();

    if cli.version {
        println!("{APP_VERSION}");
        std::process::exit(0);
    }

    match &cli.command {
        Some(cmd) => match cmd {
            Commands::Mcp { toolsets, tools } => {
                logging::info("Starting tfmcp in MCP server mode");
                let tool_filter = ToolFilter::from_cli(toolsets, tools.as_deref());
                match init_tfmcp(&cli).await {
                    Ok(tfmcp) => {
                        let result = match TransportMode::from_env() {
                            Ok(TransportMode::Stdio) => {
                                TfMcpServer::serve_stdio(tfmcp, tool_filter).await
                            }
                            Ok(TransportMode::StreamableHttp) => {
                                match HttpTransportConfig::from_env() {
                                    Ok(config) => {
                                        TfMcpServer::serve_streamable_http(
                                            tfmcp,
                                            tool_filter,
                                            config,
                                        )
                                        .await
                                    }
                                    Err(err) => Err(err),
                                }
                            }
                            Err(err) => Err(err),
                        };

                        if let Err(err) = result {
                            logging::error(&format!("Error launching MCP server: {err:?}"));
                            std::process::exit(1);
                        }
                    }
                    Err(e) => {
                        logging::error(&format!("Failed to initialize tfmcp: {e}"));
                        std::process::exit(1);
                    }
                }
            }
            Commands::Analyze => {
                logging::info("Starting Terraform configuration analysis");
                match init_tfmcp(&cli).await {
                    Ok(mut tfmcp) => {
                        if let Err(err) = tfmcp.analyze_terraform().await {
                            logging::error(&format!("Error analyzing Terraform: {err:?}"));
                            std::process::exit(1);
                        }
                    }
                    Err(e) => {
                        logging::error(&format!("Failed to initialize tfmcp: {e}"));
                        std::process::exit(1);
                    }
                }
            }
        },
        None => {
            // Default behavior if no command is specified
            println!("No command specified. Use --help for usage information.");
        }
    };
}

async fn init_tfmcp(cli: &Cli) -> anyhow::Result<TfMcp> {
    let config_path = cli.config.clone();
    let dir_path = cli.dir.clone();

    logging::info(&format!(
        "Initializing tfmcp with config: {config_path:?}, dir: {dir_path:?}"
    ));
    TfMcp::new(config_path, dir_path)
}

fn init_logging() {
    let log_level = std::env::var("TFMCP_LOG_LEVEL")
        .unwrap_or_else(|_| "info".to_string())
        .to_lowercase();

    let filter = match log_level.as_str() {
        "trace" => "trace",
        "debug" => "debug",
        "info" => "info",
        "warn" | "warning" => "warn",
        "error" => "error",
        _ => "info",
    };

    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| format!("tfmcp={filter},reqwest=warn,hyper=warn").into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();
}