mod config;
use anyhow::Result;
use backtrace::Backtrace;
use clap::Parser;
use log::{error, info, warn};
use mcp_stdio_proxy::{
AppConfig, AppState, Cli, get_proxy_manager, get_router, init_locale_from_env,
init_tracer_provider, log_service_info, run_cli, start_schedule_task,
};
use run_code_rmcp::warm_up_all_envs;
use tokio::net::TcpListener;
use tokio::signal;
use tracing_appender::rolling::{Builder, Rotation};
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::{EnvFilter, Layer as _};
#[tokio::main]
async fn main() -> Result<()> {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
let cli = Cli::parse();
if cli.command.is_some() || cli.url.is_some() {
return run_cli_mode(cli).await;
}
run_server_mode().await
}
async fn run_cli_mode(cli: Cli) -> Result<()> {
init_locale_from_env();
let is_convert_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Convert(_)));
let is_proxy_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Proxy(_)));
let is_health_command = matches!(cli.command, Some(mcp_stdio_proxy::Commands::Health(_)));
let has_custom_logging = is_convert_command || is_proxy_command;
if !has_custom_logging && !cli.quiet {
let log_level = if cli.verbose {
"debug"
} else if is_health_command {
"off"
} else {
"error" };
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(false)
.without_time()
.with_ansi(false)
.with_writer(std::io::stderr)
.compact()
.init();
}
run_cli(cli).await
}
async fn run_server_mode() -> Result<()> {
init_locale_from_env();
let app_config = AppConfig::load_config()?;
eprintln!("========================================");
eprintln!("Starting MCP proxy service...");
eprintln!("Version: {}", env!("CARGO_PKG_VERSION"));
eprintln!("Configuration loaded:");
eprintln!(" - Port: {}", app_config.server.port);
eprintln!(" - Log directory: {}", &app_config.log.path);
eprintln!(" - Log level: {}", &app_config.log.level);
eprintln!(" - Log retention days: {}", app_config.log.retain_days);
mcp_stdio_proxy::env_init::init(&app_config);
eprintln!("========================================");
app_config.log_path_init()?;
let log_level = app_config.log.level.clone();
let log_path = app_config.log.path.clone();
let server_port = app_config.server.port;
let retain_days = app_config.log.retain_days;
let log_level_for_console = log_level.clone();
let mut console_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level_for_console));
console_filter = console_filter
.add_directive("rmcp=warn".parse().unwrap())
.add_directive("run_code_rmcp=warn".parse().unwrap());
let console_layer = tracing_subscriber::fmt::layer()
.pretty()
.with_writer(std::io::stdout)
.with_filter(console_filter);
let log_path_for_file = log_path.clone();
let file_appender = Builder::new()
.rotation(Rotation::DAILY) .filename_prefix("log") .max_log_files(retain_days as usize) .build(&log_path_for_file)?;
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let mut log_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
log_filter = log_filter
.add_directive("rmcp=warn".parse().unwrap())
.add_directive("run_code_rmcp=warn".parse().unwrap());
let file_layer = tracing_subscriber::fmt::layer()
.compact()
.with_ansi(false)
.with_writer(non_blocking)
.with_filter(log_filter);
init_tracer_provider("mcp-proxy", "0.1.0")?;
let telemetry_filter = EnvFilter::new("warn")
.add_directive("mcp_proxy=debug".parse().unwrap())
.add_directive("mcp_stdio_proxy=debug".parse().unwrap())
.add_directive("rmcp=error".parse().unwrap())
.add_directive("run_code_rmcp=error".parse().unwrap());
let telemetry_layer = tracing_opentelemetry::layer().with_filter(telemetry_filter);
tracing_subscriber::registry()
.with(console_layer)
.with(file_layer)
.with(telemetry_layer)
.init();
log_service_info("mcp-proxy", "0.1.0")?;
tracing::info!("========================================");
tracing::info!("Starting MCP proxy service...");
tracing::info!("Running in proxy server mode");
tracing::info!("Version: {}", env!("CARGO_PKG_VERSION"));
tracing::info!("Configuration:");
tracing::info!(" - Listen port: {}", server_port);
tracing::info!(" - Log directory: {}", log_path);
tracing::info!(" - Log level: {}", &app_config.log.level);
tracing::info!(" - Log retention days: {}", retain_days);
tracing::info!("Environment overrides:");
if std::env::var("MCP_PROXY_PORT").is_ok() {
tracing::info!(" - MCP_PROXY_PORT override detected: {}", server_port);
}
if let Ok(log_dir) = std::env::var("MCP_PROXY_LOG_DIR") {
tracing::info!(" - MCP_PROXY_LOG_DIR override detected: {}", log_dir);
}
if let Ok(level) = std::env::var("MCP_PROXY_LOG_LEVEL") {
tracing::info!(" - MCP_PROXY_LOG_LEVEL override detected: {}", level);
}
tracing::info!("========================================");
let addr = format!("0.0.0.0:{server_port}");
tracing::info!("Binding to {addr}...");
let listener = TcpListener::bind(&addr).await.map_err(|e| {
tracing::error!("Failed to bind to {addr}: {e}");
e
})?;
tracing::info!("Successfully bound to {addr}");
tracing::info!("Initializing application state...");
let state = AppState::new(app_config.clone()).await;
tracing::info!("Application state initialized");
tracing::info!("Initializing MCP router...");
let app = get_router(state.clone()).await?;
tracing::info!("MCP router initialized");
info!("MCP proxy started: {addr}");
info!("Health endpoint: http://{addr}/health");
info!("MCP list endpoint: http://{addr}/mcp/list");
tokio::spawn(start_schedule_task());
info!("Background schedule task started");
info!("Log rotation configured (keep last {retain_days} files)");
tracing::info!("System info:");
tracing::info!(" - OS: {}", std::env::consts::OS);
tracing::info!(" - Arch: {}", std::env::consts::ARCH);
tracing::info!(
" - Working directory: {:?}",
std::env::current_dir().unwrap_or_default()
);
tokio::spawn(async move {
std::panic::set_hook(Box::new(move |panic_info| {
warn!("Panic hook triggered");
if let Some(s) = panic_info.payload().downcast_ref::<String>() {
error!("Panic reason: {s}");
} else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
error!("Panic reason: {s}");
} else {
error!("Panic reason: <unknown>");
}
if let Some(location) = panic_info.location() {
error!("Panic location: {}:{}", location.file(), location.line());
}
error!("Panic backtrace:");
let backtrace = Backtrace::new();
error!("{backtrace:?}");
}));
});
tokio::spawn(async move {
info!("Warming up uv/deno runtime dependencies...");
match warm_up_all_envs(None, None, None, None).await {
Ok(_) => info!("Runtime dependency warm-up completed"),
Err(e) => error!("Runtime dependency warm-up failed: {e}"),
}
});
info!("Starting HTTP server...");
let server =
axum::serve(listener, app.into_make_service()).with_graceful_shutdown(shutdown_signal());
if let Err(e) = server.await {
error!("HTTP server exited with error: {e}");
}
warn!("Shutdown signal received, starting cleanup...");
match get_proxy_manager().cleanup_all_resources().await {
Ok(_) => info!("Resource cleanup completed"),
Err(e) => error!("Resource cleanup failed: {e}"),
}
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
info!("Shutdown complete");
Ok(())
}
async fn shutdown_signal() {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
}
#[cfg(test)]
mod tests {
#[test]
fn test_crypto_provider_install() {
let _ = rustls::crypto::ring::default_provider().install_default();
let provider = rustls::crypto::CryptoProvider::get_default();
assert!(provider.is_some(), "CryptoProvider should be available");
}
#[test]
fn test_crypto_provider_get_default() {
let _ = rustls::crypto::ring::default_provider().install_default();
let provider = rustls::crypto::CryptoProvider::get_default();
assert!(
provider.is_some(),
"CryptoProvider should be available after installation"
);
}
}