sand-mcp-fs 0.1.0

MCP filesystem server with sandbox security based on cap-std
Documentation
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 {
    /// Allowed directories (one or more)
    #[arg(required = true)]
    allowed_dirs: Vec<String>,
    
    /// Maximum file size for read operations
    /// Supports: KB, MB, GB suffixes (default: 50MB)
    #[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()
        }
    }
}