mod error;
mod filesystem;
mod tools;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use filesystem::FilesystemServer;
use rmcp::{
handler::server::ServerHandler,
model::{ServerCapabilities, ServerInfo},
tool_handler,
ServiceExt,
};
use tracing::info;
use tracing_subscriber::fmt;
fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim().to_uppercase();
let (num, mult) = if s.ends_with("GB") {
(s.trim_end_matches("GB"), 1024 * 1024 * 1024)
} else if s.ends_with("MB") {
(s.trim_end_matches("MB"), 1024 * 1024)
} else if s.ends_with("KB") {
(s.trim_end_matches("KB"), 1024)
} else {
(s.as_str(), 1)
};
num.parse::<u64>()
.map(|n| n * mult)
.map_err(|e| format!("Invalid size '{}': {}", s, e))
}
#[derive(Parser)]
#[command(name = "sand-mcp-fs")]
struct Cli {
#[arg(required = true)]
allowed_dirs: Vec<String>,
#[arg(short, long, default_value = "50MB", value_parser = parse_size)]
max_file_size: u64,
}
#[tokio::main]
async fn main() -> Result<()> {
fmt::init();
let args = Cli::parse();
let allowed_dirs: Vec<PathBuf> = args.allowed_dirs
.iter()
.filter_map(|s| {
let path = shellexpand::tilde(s).to_string();
let path = PathBuf::from(&path);
match path.canonicalize() {
Ok(canonical) => {
if canonical.is_dir() {
Some(canonical)
} else {
eprintln!("Warning: {} is not a directory", s);
None
}
}
Err(e) => {
eprintln!("Warning: Cannot access directory {}: {}", s, e);
None
}
}
})
.collect();
if allowed_dirs.is_empty() {
anyhow::bail!("No valid directories provided");
}
info!(
"Starting sand-mcp-fs with {} allowed directories (max file size: {} bytes)",
allowed_dirs.len(),
args.max_file_size
);
let server = FilesystemServer::new(allowed_dirs, args.max_file_size)?;
let _service = server.serve((tokio::io::stdin(), tokio::io::stdout())).await?;
tokio::signal::ctrl_c().await?;
Ok(())
}
#[tool_handler]
impl ServerHandler for FilesystemServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
instructions: Some(
"MCP Filesystem server with sandbox security. All file operations are restricted to allowed directories. Available tools: read_file, write_file, list_directory, create_directory, get_file_info, move_file, search_files, list_allowed_directories".to_string()
),
..Default::default()
}
}
}