use std::{
env,
io::stderr,
sync::{Arc, Mutex as StdMutex},
time::Duration,
};
use clap::{Parser, Subcommand};
use rmcp::{ServiceExt, transport::io::stdio};
use tracing_subscriber::EnvFilter;
use void_crawl_core::{acquire_profile, chrome_user_data_dirs};
use voidcrawl_mcp::{
AppState, VoidCrawlServer,
install::{self, InstallArgs},
sessions::SessionRegistry,
state::PinnedProfile,
tools::session::close_handle,
};
#[derive(Parser, Debug)]
#[command(name = "voidcrawl-mcp", version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
headful: bool,
}
#[derive(Subcommand, Debug)]
enum Command {
Install(InstallArgs),
Uninstall(InstallArgs),
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match &cli.command {
Some(Command::Install(args)) => return install::run(false, args),
Some(Command::Uninstall(args)) => return install::run(true, args),
None => {}
}
tracing_subscriber::fmt()
.with_writer(stderr)
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
let profile_name = cli
.profile
.clone()
.or_else(|| env::var("VOIDCRAWL_PROFILE").ok().filter(|s| !s.is_empty()));
let headful =
cli.headful || matches!(env::var("VOIDCRAWL_HEADFUL").as_deref(), Ok("1" | "true"));
let headless = !headful;
let sessions = Arc::new(SessionRegistry::default());
let state = if let Some(name) = profile_name.as_deref() {
tracing::info!(profile = name, headful, "acquiring Chrome profile");
let mut handle = acquire_profile(name, Duration::from_secs(30), headless).await?;
let session = handle.take_session().ok_or_else(|| {
anyhow::anyhow!("profile handle returned without a session — should be unreachable")
})?;
let user_data_root = chrome_user_data_dirs()
.into_iter()
.find(|b| b.join(name).is_dir())
.unwrap_or_else(|| handle.path().to_path_buf());
tracing::info!(
profile = name,
path = %handle.path().display(),
user_data_root = %user_data_root.display(),
"profile acquired — pool will inherit its Chrome"
);
let pinned = PinnedProfile {
handle,
session: StdMutex::new(Some(session)),
name: name.to_string(),
user_data_root,
};
Arc::new(AppState::with_pinned_profile(Arc::clone(&sessions), pinned))
} else {
Arc::new(AppState::new(Arc::clone(&sessions)))
};
tracing::info!("voidcrawl-mcp starting");
let server = VoidCrawlServer::new(Arc::clone(&state));
let service = server.serve(stdio()).await?;
tracing::info!("voidcrawl-mcp ready");
let quit = service.waiting().await?;
tracing::info!(reason = ?quit, "voidcrawl-mcp shutting down");
for handle in sessions.drain().await {
if let Err(e) = close_handle(handle).await {
tracing::warn!(error = %e, "failed to close dedicated session");
}
}
if let Some(pool) = state.pool_if_initialized() {
if let Err(e) = pool.close().await {
tracing::warn!(error = %e, "failed to close browser pool");
}
}
if let Some(ref pinned) = state.pinned {
tracing::info!(profile = %pinned.name, "releasing pinned profile");
}
drop(state);
Ok(())
}