use std::sync::Arc;
use anyhow::Context;
use bugwarden::audit::{
policy_hash_of, AuditConfig, AuditSink, AuditState, FailMode, TransportKind,
};
use bugwarden::{config, server};
use bugwarden_core::{client::BugzillaClient, guard::Guard, policy::Policy};
use clap::Parser;
use rmcp::{
transport::{
stdio,
streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
},
},
ServiceExt,
};
use tracing_subscriber::EnvFilter;
use config::{Cli, Transport};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.with_writer(std::io::stderr)
.with_ansi(false)
.init();
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(
BugzillaClient::new(&cli.bugzilla_server, cli.use_auth_header)
.context("failed to build Bugzilla client")?,
);
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")?;
let audit = match &cfg.audit_config {
Some(path) => {
let audit_cfg = AuditConfig::load(path)?;
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(Arc::new(AuditState::new(sink, fail_mode, policy_hash)))
}
None => None,
};
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 to enable it"
);
}
let server = match audit {
Some(audit) => server.with_audit(audit),
None => server,
};
match cfg.transport {
Transport::Stdio => {
tracing::info!("Starting Bugzilla MCP server on stdio");
let service = server.serve(stdio()).await.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;
service.waiting().await?;
}
Transport::Http => {
let ct = tokio_util::sync::CancellationToken::new();
let service = StreamableHttpService::new(
move || Ok(server.clone()),
LocalSessionManager::default().into(),
StreamableHttpServerConfig::default().with_cancellation_token(ct.child_token()),
);
let router = axum::Router::new().nest_service("/mcp", service);
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 {
let _ = tokio::signal::ctrl_c().await;
ct.cancel();
})
.await?;
}
}
Ok(())
}