use std::net::SocketAddr;
use std::path::PathBuf;
use std::process;
use clap::{Args, Subcommand};
use rsigma_parser::LintConfig;
use crate::exit_code;
#[derive(Subcommand, Debug)]
pub(crate) enum McpCommands {
Serve(McpServeArgs),
}
#[derive(Args, Debug)]
pub(crate) struct McpServeArgs {
#[arg(long = "lint-config", value_name = "PATH")]
pub lint_config: Option<PathBuf>,
#[arg(long = "rules-dir", value_name = "PATH")]
pub rules_dir: Option<PathBuf>,
#[arg(long = "http", value_name = "ADDR")]
pub http: Option<SocketAddr>,
#[arg(
long = "auth-token",
env = "RSIGMA_MCP_AUTH_TOKEN",
value_name = "TOKEN"
)]
pub auth_token: Option<String>,
#[arg(long = "allow-plaintext")]
pub allow_plaintext: bool,
#[arg(long = "tls-cert", value_name = "PATH", requires = "tls_key")]
pub tls_cert: Option<PathBuf>,
#[arg(long = "tls-key", value_name = "PATH", requires = "tls_cert")]
pub tls_key: Option<PathBuf>,
}
pub(crate) fn dispatch_mcp(cmd: McpCommands) {
match cmd {
McpCommands::Serve(args) => cmd_mcp_serve(args),
}
}
fn apply_mcp_config(args: &mut McpServeArgs) {
let base = crate::config::load_and_merge(None);
let Some(mcp) = base.mcp else {
return;
};
if args.http.is_none()
&& let Some(addr) = mcp.http_addr
{
match addr.parse::<SocketAddr>() {
Ok(parsed) => args.http = Some(parsed),
Err(e) => {
eprintln!("Invalid mcp.http_addr '{addr}' in config: {e}");
process::exit(exit_code::CONFIG_ERROR);
}
}
}
if args.lint_config.is_none() {
args.lint_config = mcp.lint_config;
}
if args.rules_dir.is_none() {
args.rules_dir = mcp.rules_dir;
}
}
pub(crate) fn cmd_mcp_serve(mut args: McpServeArgs) {
apply_mcp_config(&mut args);
let lint_config = match &args.lint_config {
Some(path) => match LintConfig::load(path) {
Ok(config) => config,
Err(e) => {
eprintln!("Error loading lint config {}: {e}", path.display());
process::exit(exit_code::CONFIG_ERROR);
}
},
None => LintConfig::default(),
};
let handler = rsigma_mcp::RsigmaMcp::new(args.rules_dir.clone(), lint_config);
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
eprintln!("Failed to create async runtime for the MCP server: {e}");
process::exit(exit_code::CONFIG_ERROR);
}
};
let result = match args.http {
None => runtime.block_on(rsigma_mcp::serve_stdio(handler)),
Some(addr) => runtime.block_on(serve_http_transport(handler, addr, &args)),
};
if let Err(e) = result {
eprintln!("MCP server error: {e}");
process::exit(exit_code::RULE_ERROR);
}
}
async fn serve_http_transport(
handler: rsigma_mcp::RsigmaMcp,
addr: SocketAddr,
args: &McpServeArgs,
) -> anyhow::Result<()> {
let tls_requested = args.tls_cert.is_some();
if !tls_requested && !addr.ip().is_loopback() && !args.allow_plaintext {
anyhow::bail!(
"refusing to bind plaintext on non-loopback address {addr}; \
pass --tls-cert/--tls-key to enable TLS or --allow-plaintext to opt out \
(e.g. when terminating TLS at a sidecar proxy)"
);
}
let listener = tokio::net::TcpListener::bind(addr).await?;
let auth = args.auth_token.clone();
if tls_requested {
return serve_http_tls(handler, listener, auth, addr, args).await;
}
eprintln!("MCP Streamable HTTP server listening on http://{addr}/mcp");
rsigma_mcp::serve_http(handler, listener, auth).await
}
#[cfg(feature = "daemon-tls")]
async fn serve_http_tls(
handler: rsigma_mcp::RsigmaMcp,
listener: tokio::net::TcpListener,
auth: Option<String>,
addr: SocketAddr,
args: &McpServeArgs,
) -> anyhow::Result<()> {
use std::sync::Arc;
use crate::daemon::tls::{RustlsListener, TlsCliConfig, TlsState};
let cli = TlsCliConfig {
cert_path: args.tls_cert.clone().expect("tls_cert present"),
key_path: args.tls_key.clone().expect("tls_key present"),
key_password: None,
client_ca_path: None,
min_version: Default::default(),
};
let tls_state = TlsState::from_paths(cli).map_err(|e| anyhow::anyhow!("{e}"))?;
let gauge = Arc::new(
prometheus::IntGauge::new(
"rsigma_mcp_tls_active_connections",
"Active TLS connections to the MCP HTTP server",
)
.expect("valid gauge"),
);
let tls_listener = RustlsListener::new(listener, tls_state.config, gauge);
eprintln!("MCP Streamable HTTP server listening on https://{addr}/mcp");
let router = rsigma_mcp::http_router(handler, auth);
axum::serve(tls_listener, router).await?;
Ok(())
}
#[cfg(not(feature = "daemon-tls"))]
async fn serve_http_tls(
_handler: rsigma_mcp::RsigmaMcp,
_listener: tokio::net::TcpListener,
_auth: Option<String>,
_addr: SocketAddr,
_args: &McpServeArgs,
) -> anyhow::Result<()> {
anyhow::bail!(
"--tls-cert/--tls-key require a build with the `daemon-tls` feature; \
rebuild with `--features daemon-tls` or terminate TLS at a sidecar proxy \
and use --allow-plaintext"
)
}