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 {
#[arg(long, default_value = "all", value_delimiter = ',')]
toolsets: Vec<String>,
#[arg(long, value_delimiter = ',')]
tools: Option<Vec<String>>,
},
#[command(name = "analyze", about = "Analyze Terraform configurations")]
Analyze,
}
#[tokio::main]
async fn main() {
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 => {
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();
}