use std::sync::{Arc, OnceLock};
use anyhow::Context;
use bugwarden::audit::{
policy_hash_of, select_sinks, AuditConfig, AuditSink, AuditState, FailMode, SinkSelection,
TransportKind,
};
use bugwarden::http_auth::{self, HttpEnv};
use bugwarden::otel::{self, OtelEnv, Pipeline};
use bugwarden::{config, server};
use bugwarden_core::{guard::Guard, policy::Policy};
use clap::Parser;
use rmcp::{
transport::{
stdio,
streamable_http_server::{session::local::LocalSessionManager, StreamableHttpService},
},
ServiceExt,
};
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::EnvFilter;
use config::{Cli, Transport};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let otel_config = otel::resolve(&OtelEnv::from_env())?;
let otel_slot: Arc<OnceLock<Arc<Pipeline>>> = Arc::new(OnceLock::new());
let registry = tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(false),
);
if otel_config.is_some() {
registry
.with(Pipeline::diagnostics_layer(otel_slot.clone()))
.init();
} else {
registry.init();
}
let http_auth =
http_auth::resolve_for(cli.transport, &HttpEnv::from_env(), cli.insecure_no_auth)
.map(|auth| auth.map(Arc::new))?;
if let Some(auth) = &http_auth {
auth.log_startup_mode();
}
let sinks = select_sinks(cli.audit_config.as_deref(), otel_config.is_some())?;
let mut policy = match &cli.policy {
Some(path) => Policy::load(path)
.with_context(|| format!("failed to load guard policy from {}", path.display()))?,
None => Policy::default(),
};
policy.global.read_only |= cli.read_only;
let bz = Arc::new(server::bugzilla_client(&cli)?);
let guard = Arc::new(Guard { policy });
let cfg = Arc::new(cli);
let server =
server::BugWarden::new(cfg.clone(), guard, bz).context("failed to build the MCP server")?;
server.preflight().await?;
let otel = match otel_config {
Some(otel_cfg) => Some(Arc::new(Pipeline::start(otel_cfg)?)),
None => None,
};
if let Some(pipeline) = &otel {
let _ = otel_slot.set(pipeline.clone());
pipeline.probe().await?;
tracing::info!(
"OTLP export enabled: audit records are exported to the configured \
collector and gate serving when delivery fails; diagnostics are \
copied best-effort"
);
}
let audit_sink = match sinks {
SinkSelection::NoAudit => None,
SinkSelection::FileOnly | SinkSelection::Both | SinkSelection::OtlpOnly => {
let audit_cfg = match sinks {
SinkSelection::FileOnly | SinkSelection::Both => {
match cfg.audit_config.as_deref() {
Some(path) => AuditConfig::load(path)?,
None => anyhow::bail!(
"internal error: a file-bearing sink selection has no path"
),
}
}
_ => AuditConfig::fileless(),
};
let transport = match cfg.transport {
Transport::Stdio => TransportKind::Stdio,
Transport::Http => TransportKind::Http,
};
let fail_mode = FailMode::resolve(audit_cfg.fail_mode, transport);
let policy_hash = match &cfg.policy {
Some(policy_path) => {
let bytes = std::fs::read(policy_path).with_context(|| {
format!(
"failed to read guard policy from {} for the audit digest",
policy_path.display()
)
})?;
Some(policy_hash_of(&bytes))
}
None => None,
};
let sink = AuditSink::open(audit_cfg).context("failed to open the audit sink")?;
Some((sink, fail_mode, policy_hash))
}
};
let audit = audit_sink.map(|(sink, fail_mode, policy_hash)| {
let sink = match &otel {
Some(pipeline) => sink.with_export(pipeline.audit_exporter()),
None => sink,
};
Arc::new(AuditState::new(sink, fail_mode, policy_hash))
});
if audit.is_none() && cfg.transport == Transport::Http {
tracing::warn!(
"auditing is OFF: remote tool calls over http will leave no audit \
record — pass --audit-config / BUGWARDEN_AUDIT_CONFIG for a file, \
or set OTEL_EXPORTER_OTLP_ENDPOINT with BUGWARDEN_AUDIT_CONFIG=none \
for a collector-only trail"
);
}
let server = match audit {
Some(audit) => server.with_audit(audit),
None => server,
};
let server =
server.with_scope_enforcement(http_auth.as_ref().is_some_and(|auth| !auth.is_insecure()));
let otel_on_signal = otel.clone();
let served: anyhow::Result<()> = async move {
match cfg.transport {
Transport::Stdio => {
let shutdown = shutdown_signal();
tokio::pin!(shutdown);
tracing::info!("Starting Bugzilla MCP server on stdio");
let service = tokio::select! {
result = server.serve(stdio()) => {
result.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?
}
() = &mut shutdown => {
tracing::info!("received shutdown signal");
flush_otel_and_exit(otel_on_signal.as_deref()).await;
}
};
let cancel = service.cancellation_token();
tokio::select! {
result = service.waiting() => {
result?;
}
() = shutdown => {
tracing::info!("received shutdown signal");
cancel.cancel();
flush_otel_and_exit(otel_on_signal.as_deref()).await;
}
}
}
Transport::Http => {
let ct = tokio_util::sync::CancellationToken::new();
let config = server
.http_server_config()?
.with_cancellation_token(ct.child_token());
let service = StreamableHttpService::new(
move || Ok(server.clone()),
LocalSessionManager::default().into(),
config,
);
let Some(auth) = http_auth else {
anyhow::bail!("internal error: the http transport has no resolved bearer gate");
};
let router = http_auth::guard_router(
axum::Router::new().nest_service("/mcp", service),
auth,
);
let addr = format!("{}:{}", cfg.host, cfg.port);
tracing::info!("Starting Bugzilla MCP server on {addr}");
let tcp_listener = tokio::net::TcpListener::bind(&addr)
.await
.with_context(|| format!("failed to bind {addr}"))?;
axum::serve(
tcp_listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(async move {
shutdown_signal().await;
tracing::info!("received shutdown signal");
ct.cancel();
})
.await?;
}
}
Ok(())
}
.await;
if let Some(pipeline) = &otel {
pipeline.shutdown().await;
}
served
}
async fn flush_otel_and_exit(otel: Option<&Pipeline>) -> ! {
if let Some(pipeline) = otel {
pipeline.shutdown().await;
}
std::process::exit(0);
}
async fn shutdown_signal() {
let ctrl_c = tokio::signal::ctrl_c();
#[cfg(unix)]
{
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut term) => {
tokio::select! {
_ = ctrl_c => {}
_ = term.recv() => {}
}
}
Err(err) => {
tracing::error!(
error = %err,
"failed to install SIGTERM handler; only SIGINT will stop the process"
);
let _ = ctrl_c.await;
}
}
}
#[cfg(not(unix))]
{
let _ = ctrl_c.await;
}
}