Skip to main content

rover/cli/
mcp.rs

1//! `rover mcp` subcommand — start the MCP server over stdio.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use anyhow::Context;
7
8use crate::config;
9use crate::fetcher::ssrf::SsrfLevel;
10use crate::mcp;
11use crate::storage::Db;
12
13pub struct Args {
14    pub ignore_robots: bool,
15    pub rate_limit_rpm: Option<u32>,
16    pub per_host_concurrency: Option<u32>,
17    pub global_concurrency: Option<u32>,
18    pub max_retries: Option<u8>,
19}
20
21pub async fn run(args: Args, config_path: Option<&Path>) -> anyhow::Result<()> {
22    let mut cfg = config::load_resolved(config_path).context("loading config")?;
23    cfg.apply_overrides(
24        args.rate_limit_rpm,
25        args.per_host_concurrency,
26        args.global_concurrency,
27        args.max_retries,
28        args.ignore_robots,
29    );
30
31    let ssrf_level = SsrfLevel::parse(&cfg.ssrf.level)
32        .with_context(|| format!("invalid [ssrf] level `{}` in config", cfg.ssrf.level))?;
33    let ssrf_project_root = if ssrf_level == SsrfLevel::Project {
34        let raw = &cfg.ssrf.project_root;
35        let resolved = std::fs::canonicalize(raw)
36            .with_context(|| format!("canonicalizing ssrf.project_root `{}`", raw.display()))?;
37        tracing::info!(
38            target: "rover::ssrf",
39            project_root = %resolved.display(),
40            "ssrf level=project; project_root resolved",
41        );
42        Some(resolved)
43    } else {
44        None
45    };
46
47    // Optional HAR recorder. Created before the server takes over stdio so any
48    // error opening the file surfaces as a normal startup failure. A periodic
49    // flush task batches up exchanges every 5 seconds; final flush happens at
50    // shutdown via `Arc::strong_count` going to 1 (the flush task holds one
51    // clone for the lifetime of the process).
52    let har_recorder: Option<Arc<crate::fetcher::har::HarRecorder>> =
53        if !cfg.debug.har_path.is_empty() {
54            let path = std::path::PathBuf::from(&cfg.debug.har_path);
55            let r = crate::fetcher::har::HarRecorder::new(path, cfg.debug.har_body_cap)
56                .with_context(|| format!("opening har file at {}", cfg.debug.har_path))?;
57            let r = Arc::new(r);
58
59            let r_flush = r.clone();
60            tokio::spawn(async move {
61                let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
62                interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
63                loop {
64                    interval.tick().await;
65                    if let Err(e) = r_flush.flush().await {
66                        tracing::warn!(
67                            target: "rover::fetcher",
68                            error = ?e,
69                            "har periodic flush failed"
70                        );
71                    }
72                }
73            });
74
75            tracing::info!(
76                target: "rover::fetcher",
77                har_path = %cfg.debug.har_path,
78                har_body_cap = cfg.debug.har_body_cap,
79                "har recorder enabled",
80            );
81            Some(r)
82        } else {
83            None
84        };
85
86    let cfg = Arc::new(cfg);
87
88    let data_dir = crate::paths::data_dir();
89    std::fs::create_dir_all(&data_dir).context("creating data dir")?;
90    let db = Db::open(data_dir.join("rover.db"))
91        .await
92        .context("opening cache database")?;
93
94    mcp::serve_stdio(db, cfg, ssrf_level, ssrf_project_root, har_recorder).await
95}