use std::sync::Arc;
use tokio::signal;
use tokio::sync::RwLock;
use tracing::{error, info, warn};
use is_terminal::IsTerminal;
use bevy_fairy::server::brp_client::BrpClient;
use bevy_fairy::server::config::Config;
use bevy_fairy::server::error::Result;
use bevy_fairy::server::{mcp_server, mcp_server_v2};
#[cfg(feature = "observability")]
use bevy_fairy::server::observability::ObservabilityService;
#[tokio::main]
async fn main() -> Result<()> {
std::panic::set_hook(Box::new(|panic_info| {
let payload = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic payload".to_string()
};
let location = if let Some(location) = panic_info.location() {
format!("{}:{}:{}", location.file(), location.line(), location.column())
} else {
"Unknown location".to_string()
};
eprintln!("FATAL PANIC in bevy-fairy: {} at {}", payload, location);
eprintln!("This indicates a critical bug that should be reported.");
eprintln!("Stack trace should appear above this message.");
std::io::Write::flush(&mut std::io::stderr()).unwrap_or(());
}));
let args: Vec<String> = std::env::args().collect();
const LEGACY_STDIO_ALIASES: &[&str] = &["stdio", "serve"];
const KNOWN_ARGS: &[&str] = &["--stdio", "--tcp", "--server", "--help", "-h", "--version", "-V"];
if let Some(unknown) = args.iter().skip(1).find(|arg| {
!KNOWN_ARGS.contains(&arg.as_str()) && !LEGACY_STDIO_ALIASES.contains(&arg.as_str())
}) {
eprintln!("bevy-fairy: unknown argument `{unknown}`");
eprintln!("valid arguments: {}", KNOWN_ARGS.join(", "));
eprintln!("try `bevy-fairy --help`");
std::process::exit(2);
}
if args.iter().any(|arg| arg == "--version" || arg == "-V") {
println!("bevy-fairy {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
println!("bevy-fairy {}", env!("CARGO_PKG_VERSION"));
println!("\nUsage: {} [OPTIONS]", args[0]);
println!("\nOptions:");
println!(" --stdio Run in stdio mode (default for Claude Code)");
println!(" --tcp, --server Run as TCP server on port {}", Config::from_env().unwrap_or_default().mcp_port);
println!(" --help, -h Show this help message");
println!(" --version, -V Print the version and exit");
println!("\nEnvironment variables:");
println!(" BEVY_BRP_HOST Bevy Remote Protocol host (default: localhost)");
println!(" BEVY_BRP_PORT Bevy Remote Protocol port (default: 15702)");
println!(" MCP_PORT MCP server port for TCP mode (default: 3001)");
println!(" RUST_LOG Logging level (default: info)");
return Ok(());
}
let is_stdio_mode = args
.iter()
.any(|arg| arg == "--stdio" || LEGACY_STDIO_ALIASES.contains(&arg.as_str())) ||
(!args.iter().any(|arg| arg == "--tcp" || arg == "--server") && !std::io::stdout().is_terminal());
if is_stdio_mode {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.with_ansi(false) .init();
} else {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
}
let config = Config::from_env()?;
let use_tcp = args.iter().any(|arg| arg == "--tcp" || arg == "--server");
let use_stdio = !use_tcp && (
args.iter().any(|arg| arg == "--stdio")
|| !is_terminal::IsTerminal::is_terminal(&std::io::stdin())
|| std::env::var("MCP_TRANSPORT")
.map(|t| t == "stdio")
.unwrap_or(false)
);
if use_stdio {
info!("Starting bevy-fairy in stdio mode for Claude Code");
run_stdio_mode(config).await
} else {
info!(
"Starting bevy-fairy in TCP mode on port {}",
config.mcp_port
);
run_tcp_mode(config).await
}
}
async fn run_stdio_mode(config: Config) -> Result<()> {
let brp_client = Arc::new(RwLock::new(BrpClient::new(&config)));
{
let client = brp_client.read().await;
client.init().await?;
}
#[cfg(feature = "observability")]
let _observability = if config.observability.metrics_enabled || config.observability.tracing_enabled {
let obs_service = ObservabilityService::new(config.clone(), brp_client.clone()).await?;
obs_service.start().await?;
Some(obs_service)
} else {
None
};
let server = mcp_server_v2::McpServerV2::new(config, brp_client)?;
server.run_stdio().await
}
async fn run_tcp_mode(config: Config) -> Result<()> {
let brp_client = Arc::new(RwLock::new(BrpClient::new(&config)));
{
let client = brp_client.read().await;
client.init().await?;
}
#[cfg(feature = "observability")]
let observability = if config.observability.metrics_enabled || config.observability.tracing_enabled {
let obs_service = ObservabilityService::new(config.clone(), brp_client.clone()).await?;
obs_service.start().await?;
if config.observability.health_check_enabled {
let health_service = obs_service.health();
let health_router = health_service.create_router();
tokio::spawn(async move {
let app = health_router.with_state(health_service);
let health_listener = tokio::net::TcpListener::bind(
format!("127.0.0.1:{}", config.observability.health_check_port)
).await.expect("Failed to bind health check port");
info!("Health endpoints available at http://127.0.0.1:{}", config.observability.health_check_port);
axum::serve(health_listener, app).await.expect("Health server failed");
});
}
Some(obs_service)
} else {
None
};
let mcp_server = mcp_server::McpServer::new(config.clone(), brp_client);
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", config.mcp_port))
.await
.map_err(|e| bevy_fairy::server::error::Error::Connection(format!("Failed to bind TCP: {}", e)))?;
info!("MCP server listening on 127.0.0.1:{}", config.mcp_port);
let server_handle = tokio::spawn(async move {
if let Err(e) = mcp_server.run(listener).await {
error!("MCP Server error: {}", e);
}
});
tokio::select! {
_ = server_handle => {
warn!("MCP Server task completed");
}
_ = signal::ctrl_c() => {
info!("Received SIGINT, shutting down gracefully");
#[cfg(feature = "observability")]
if let Some(obs) = observability {
if let Err(e) = obs.shutdown().await {
warn!("Error shutting down observability: {}", e);
}
}
}
}
Ok(())
}