use anyhow::Result;
use camino::Utf8PathBuf;
use crate::config::Config;
use crate::mcp::server::{serve_stdio, CordanceServer};
use crate::mcp::validation::AllowedRoots;
pub fn run(cfg: &Config, default_target: &Utf8PathBuf) -> Result<()> {
let _ = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("CORDANCE_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.try_init();
tracing::info!(
target = %default_target,
"cordance serve: rmcp 1.7 stdio server starting (stderr-only logging)"
);
if !cfg.mcp.allowed_roots.is_empty() {
tracing::warn!(
extra = ?cfg.mcp.allowed_roots,
"ignoring [mcp].allowed_roots from project cordance.toml; allow-list is launch-CWD only"
);
}
let allowed = AllowedRoots::from_config(default_target.as_std_path(), &[]);
tracing::debug!(
target = %default_target,
roots = ?allowed.roots(),
"mcp allow-list resolved (launch CWD only)"
);
let runtime_cfg = cfg.clone();
let server = CordanceServer::new(runtime_cfg, allowed);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
runtime.block_on(async move {
if let Err(e) = serve_stdio(server).await {
tracing::error!(error = %e, "cordance serve: stdio loop exited with error");
return Err::<(), anyhow::Error>(anyhow::anyhow!("serve_stdio: {e}"));
}
Ok(())
})?;
tracing::info!("cordance serve: clean shutdown");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::McpConfig;
use std::path::PathBuf;
#[test]
fn allowed_roots_are_launch_cwd_only_even_with_hostile_config() {
let dir = tempfile::tempdir().expect("tempdir");
let server_cwd = dir.path().to_path_buf();
std::fs::write(
server_cwd.join("cordance.toml"),
"[mcp]\nallowed_roots = [\"/\"]\n",
)
.expect("write hostile cordance.toml");
let hostile_cfg_view = McpConfig {
allowed_roots: vec!["/".to_string()],
};
let _ = hostile_cfg_view; let allowed = AllowedRoots::from_config(server_cwd.as_path(), &[]);
let canon_cwd = dunce::canonicalize(server_cwd.as_path()).expect("canonicalise server cwd");
let roots: Vec<PathBuf> = allowed.roots().to_vec();
assert_eq!(
roots.len(),
1,
"allow-list must contain exactly one entry (launch CWD)"
);
assert_eq!(
roots[0], canon_cwd,
"the only entry must be the canonicalised launch CWD"
);
for root in &roots {
let s = root.to_string_lossy();
assert!(
s.contains(canon_cwd.to_string_lossy().as_ref()),
"resolved root {s:?} must be inside the launch CWD"
);
}
}
#[test]
fn empty_extras_yields_cwd_only_allow_list() {
let dir = tempfile::tempdir().expect("tempdir");
let cwd = dir.path().to_path_buf();
let allowed = AllowedRoots::from_config(cwd.as_path(), &[]);
assert_eq!(allowed.roots().len(), 1);
let canon = dunce::canonicalize(cwd.as_path()).expect("canon");
assert_eq!(allowed.roots()[0], canon);
}
}