mcp-tools 0.1.0

Rust MCP tools library
Documentation
//! Utility functions for MCP Tools

use crate::Result;
use std::path::Path;
use tracing::info;

/// Validate server configuration
pub fn validate_server_config(config: &crate::common::ServerConfig) -> Result<()> {
    if config.name.is_empty() {
        return Err(crate::McpToolsError::Config(
            "Server name cannot be empty".to_string(),
        ));
    }

    if config.port == 0 {
        return Err(crate::McpToolsError::Config(
            "Server port must be greater than 0".to_string(),
        ));
    }

    Ok(())
}

/// Validate client configuration
pub fn validate_client_config(config: &crate::common::ClientConfig) -> Result<()> {
    if config.name.is_empty() {
        return Err(crate::McpToolsError::Config(
            "Client name cannot be empty".to_string(),
        ));
    }

    if config.server_url.is_empty() {
        return Err(crate::McpToolsError::Config(
            "Server URL cannot be empty".to_string(),
        ));
    }

    Ok(())
}

/// Format file size in human-readable format
pub fn format_file_size(size: u64) -> String {
    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
    let mut size = size as f64;
    let mut unit_index = 0;

    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
        size /= 1024.0;
        unit_index += 1;
    }

    if unit_index == 0 {
        format!("{} {}", size as u64, UNITS[unit_index])
    } else {
        format!("{:.1} {}", size, UNITS[unit_index])
    }
}

/// Check if path is safe for file operations
pub fn is_safe_path<P: AsRef<Path>>(path: P) -> bool {
    let path = path.as_ref();

    // Check for path traversal attempts
    if path.to_string_lossy().contains("..") {
        return false;
    }

    // Check for absolute paths to system directories
    if let Ok(canonical) = path.canonicalize() {
        let path_str = canonical.to_string_lossy().to_lowercase();

        // Unix system paths
        if path_str.starts_with("/etc")
            || path_str.starts_with("/sys")
            || path_str.starts_with("/proc")
            || path_str.starts_with("/dev")
            || path_str.starts_with("/boot")
            || path_str.starts_with("/root")
        {
            return false;
        }

        // Windows system paths
        if path_str.starts_with("c:\\windows")
            || path_str.starts_with("c:\\system32")
            || path_str.starts_with("c:\\program files")
        {
            return false;
        }
    }

    true
}

/// Generate session ID
pub fn generate_session_id() -> String {
    format!(
        "session-{}-{}",
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis(),
        uuid::Uuid::new_v4().to_string()[..8].to_string()
    )
}

/// Log server startup information
pub fn log_server_startup(config: &crate::common::ServerConfig) {
    info!("🚀 Starting MCP Server");
    info!("   Name: {}", config.name);
    info!("   Description: {}", config.description);
    info!("   Version: {}", config.version);
    info!("   Address: {}:{}", config.host, config.port);
    info!("   Max Connections: {}", config.max_connections);
    info!("   Request Timeout: {}s", config.request_timeout_secs);
}

/// Log client startup information
pub fn log_client_startup(config: &crate::common::ClientConfig) {
    info!("🔌 Starting MCP Client");
    info!("   Name: {}", config.name);
    info!("   Version: {}", config.version);
    info!("   Server URL: {}", config.server_url);
    info!("   Connect Timeout: {}s", config.connect_timeout_secs);
    info!("   Request Timeout: {}s", config.request_timeout_secs);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::common::{ClientConfig, ServerConfig};

    #[test]
    fn test_validate_server_config() {
        let mut config = ServerConfig::default();
        assert!(validate_server_config(&config).is_ok());

        config.name = "".to_string();
        assert!(validate_server_config(&config).is_err());

        config.name = "Test".to_string();
        config.port = 0;
        assert!(validate_server_config(&config).is_err());
    }

    #[test]
    fn test_validate_client_config() {
        let mut config = ClientConfig::default();
        assert!(validate_client_config(&config).is_ok());

        config.name = "".to_string();
        assert!(validate_client_config(&config).is_err());

        config.name = "Test".to_string();
        config.server_url = "".to_string();
        assert!(validate_client_config(&config).is_err());
    }

    #[test]
    fn test_format_file_size() {
        assert_eq!(format_file_size(0), "0 B");
        assert_eq!(format_file_size(1023), "1023 B");
        assert_eq!(format_file_size(1024), "1.0 KB");
        assert_eq!(format_file_size(1536), "1.5 KB");
        assert_eq!(format_file_size(1048576), "1.0 MB");
    }

    #[test]
    fn test_is_safe_path() {
        assert!(is_safe_path("./test.txt"));
        assert!(is_safe_path("test/file.txt"));
        assert!(!is_safe_path("../../../etc/passwd"));
        assert!(!is_safe_path("test/../../../etc/passwd"));
    }

    #[test]
    fn test_generate_session_id() {
        let id1 = generate_session_id();
        let id2 = generate_session_id();

        assert_ne!(id1, id2);
        assert!(id1.starts_with("session-"));
        assert!(id2.starts_with("session-"));
    }
}