#![cfg_attr(
test,
allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::str_to_string
)
)]
#[cfg(feature = "mcp-tools")]
use std::env;
use std::error::Error;
use std::sync::Arc;
use clap::Parser;
use dravr_tronc::mcp::transport::stdio;
use dravr_tronc::server::tracing_init;
use dravr_tronc::McpServer;
use embacle::types::RunnerError;
use embacle_mcp::ServerState;
use opentelemetry::global;
use opentelemetry_sdk::metrics::SdkMeterProvider;
use tokio::net::TcpListener;
use embacle_server::auth;
#[cfg(feature = "mcp-tools")]
use embacle_server::mcp_client::McpClientPool;
use embacle_server::router;
use embacle_server::runner::{self, parse_runner_type};
use embacle_server::state::{AppState, ServerTools};
#[derive(Parser)]
#[command(name = "embacle-server", version, about)]
struct Cli {
#[arg(long, default_value = "http")]
transport: String,
#[arg(long, default_value_t = 3000)]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(long, default_value = "copilot")]
provider: String,
}
#[cfg(feature = "mcp-tools")]
async fn build_server_tools(config: Option<&embacle::EmbacleConfig>) -> Option<ServerTools> {
let mut servers: Vec<embacle::McpServerConfig> =
config.map(|c| c.mcp_servers.clone()).unwrap_or_default();
if let Ok(json) = env::var("EMBACLE_MCP_SERVERS") {
match serde_json::from_str::<Vec<embacle::McpServerConfig>>(&json) {
Ok(mut from_env) => servers.append(&mut from_env),
Err(e) => tracing::warn!(error = %e, "Failed to parse EMBACLE_MCP_SERVERS as JSON"),
}
}
if servers.is_empty() {
return None;
}
match McpClientPool::connect(&servers).await {
Ok(pool) if !pool.is_empty() => {
let declarations = pool.declarations().to_vec();
tracing::info!(
tools = pool.tool_count(),
"Server-side tool execution enabled"
);
Some(ServerTools {
executor: Arc::new(pool),
declarations,
})
}
Ok(_) => {
tracing::warn!("Configured MCP servers exposed no tools; server-side tools disabled");
None
}
Err(e) => {
tracing::error!(error = %e, "Failed to connect MCP tool servers; server-side tools disabled");
None
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let cli = Cli::parse();
tracing_init::init_with_notifications(&cli.transport);
let meter_provider = SdkMeterProvider::builder().build();
global::set_meter_provider(meter_provider);
let Some(effective_provider) = parse_runner_type(&cli.provider) else {
return Err(RunnerError::config(format!(
"Unknown provider: {}. Valid: {}",
cli.provider,
runner::valid_provider_names()
))
.into());
};
let config = match embacle::load_config() {
Ok(config) => config,
Err(e) => {
tracing::warn!(error = %e, "Failed to load config file, using CLI defaults");
None
}
};
let effective_provider = match &config {
Some(cfg) if cli.provider == "copilot" => cfg
.defaults
.model
.as_deref()
.and_then(parse_runner_type)
.unwrap_or(effective_provider),
_ => effective_provider,
};
let state = Arc::new(ServerState::new(effective_provider));
tracing::info!(
transport = %cli.transport,
provider = %effective_provider,
"Starting embacle server"
);
match cli.transport.as_str() {
"stdio" => {
let server = Arc::new(McpServer::new(
"embacle-mcp",
env!("CARGO_PKG_VERSION"),
embacle_mcp::build_tool_registry(),
Arc::clone(&state),
));
stdio::run(server).await?;
}
"http" => {
match auth::resolve_startup_auth(&cli.host, auth::api_key_configured()) {
Ok(auth::AuthMode::Enforced) => {
tracing::info!("EMBACLE_API_KEY set; bearer authentication enforced");
}
Ok(auth::AuthMode::LoopbackDev) => {
tracing::warn!(
host = %cli.host,
"No EMBACLE_API_KEY set; unauthenticated access allowed on loopback bind only"
);
}
Err(e) => {
return Err(RunnerError::config(e.to_string()).into());
}
}
#[cfg(feature = "mcp-tools")]
let server_tools = build_server_tools(config.as_ref()).await;
#[cfg(not(feature = "mcp-tools"))]
let server_tools: Option<ServerTools> = None;
let app_state = AppState::new(state).with_server_tools(server_tools);
let app = router::build(app_state);
let addr = format!("{}:{}", cli.host, cli.port);
let listener = TcpListener::bind(&addr)
.await
.map_err(|e| RunnerError::internal(format!("Failed to bind {addr}: {e}")))?;
tracing::info!(address = %addr, "HTTP transport listening");
axum::serve(listener, app)
.await
.map_err(|e| RunnerError::internal(format!("Server error: {e}")))?;
}
other => {
return Err(RunnerError::config(format!(
"Unknown transport: {other}. Valid: http, stdio"
))
.into());
}
}
Ok(())
}