#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rust_mcp_sdk::McpServer;
use rust_mcp_sdk::schema::Root;
use tokio::sync::RwLock;
pub(crate) async fn list_roots(runtime: &Arc<dyn McpServer>) -> anyhow::Result<Vec<Root>> {
if !runtime.client_supports_root_list().unwrap_or(false) {
return Ok(vec![]);
}
let result = runtime
.request_root_list(None)
.await
.map_err(|e| anyhow::anyhow!("roots/list request failed: {e}"))?;
Ok(result.roots)
}
pub(crate) fn validate_path_in_roots(path: &Path, roots: &[Root]) -> bool {
if roots.is_empty() {
return true;
}
roots.iter().any(|root| path_is_under_root(path, &root.uri))
}
#[derive(Clone)]
pub(crate) struct RootsCache {
inner: Arc<RwLock<Vec<Root>>>,
}
impl RootsCache {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(vec![])),
}
}
pub(crate) async fn refresh(&self, runtime: &Arc<dyn McpServer>) {
match list_roots(runtime).await {
Ok(roots) => {
*self.inner.write().await = roots;
}
Err(e) => {
tracing::warn!("Failed to refresh roots cache: {e}");
}
}
}
pub(crate) async fn get(&self) -> Vec<Root> {
self.inner.read().await.clone()
}
pub(crate) async fn is_path_valid(&self, path: &Path) -> bool {
let roots = self.get().await;
validate_path_in_roots(path, &roots)
}
}
fn path_is_under_root(path: &Path, root_uri: &str) -> bool {
file_uri_to_path(root_uri).is_some_and(|root_path| path.starts_with(&root_path))
}
fn file_uri_to_path(uri: &str) -> Option<PathBuf> {
uri.strip_prefix("file://").map(PathBuf::from)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_mcp_sdk::schema::Root;
fn root(uri: &str) -> Root {
Root {
uri: uri.to_string(),
name: None,
meta: None,
}
}
#[test]
fn file_uri_to_path_strips_prefix() {
let path = file_uri_to_path("file:///home/user/projects");
assert_eq!(path, Some(PathBuf::from("/home/user/projects")));
}
#[test]
fn file_uri_to_path_returns_none_for_non_file() {
let path = file_uri_to_path("https://example.com");
assert!(path.is_none());
}
#[test]
fn validate_path_true_when_roots_empty() {
let roots: Vec<Root> = vec![];
assert!(validate_path_in_roots(Path::new("/anywhere"), &roots));
}
#[test]
fn validate_path_true_when_path_under_root() {
let roots = vec![root("file:///home/user/projects")];
let path = Path::new("/home/user/projects/my-app/src/main.rs");
assert!(validate_path_in_roots(path, &roots));
}
#[test]
fn validate_path_false_when_path_outside_all_roots() {
let roots = vec![
root("file:///home/user/projects"),
root("file:///home/user/docs"),
];
let path = Path::new("/tmp/evil");
assert!(!validate_path_in_roots(path, &roots));
}
#[test]
fn validate_path_true_when_any_root_matches() {
let roots = vec![
root("file:///home/user/projects"),
root("file:///home/user/docs"),
];
let path = Path::new("/home/user/docs/report.pdf");
assert!(validate_path_in_roots(path, &roots));
}
#[test]
fn validate_path_false_for_prefix_that_is_not_ancestor() {
let roots = vec![root("file:///home/user/project")];
let path = Path::new("/home/user/project-evil/file.rs");
assert!(!validate_path_in_roots(path, &roots));
}
#[tokio::test]
async fn roots_cache_starts_empty() {
let cache = RootsCache::new();
let roots = cache.get().await;
assert!(roots.is_empty());
}
#[tokio::test]
async fn roots_cache_is_path_valid_true_when_empty() {
let cache = RootsCache::new();
assert!(cache.is_path_valid(Path::new("/anywhere")).await);
}
}