mcp_tools/utils/
mod.rs

1//! Utility functions for MCP Tools
2
3use crate::Result;
4use std::path::Path;
5use tracing::info;
6
7/// Validate server configuration
8pub fn validate_server_config(config: &crate::common::ServerConfig) -> Result<()> {
9    if config.name.is_empty() {
10        return Err(crate::McpToolsError::Config(
11            "Server name cannot be empty".to_string(),
12        ));
13    }
14
15    if config.port == 0 {
16        return Err(crate::McpToolsError::Config(
17            "Server port must be greater than 0".to_string(),
18        ));
19    }
20
21    Ok(())
22}
23
24/// Validate client configuration
25pub fn validate_client_config(config: &crate::common::ClientConfig) -> Result<()> {
26    if config.name.is_empty() {
27        return Err(crate::McpToolsError::Config(
28            "Client name cannot be empty".to_string(),
29        ));
30    }
31
32    if config.server_url.is_empty() {
33        return Err(crate::McpToolsError::Config(
34            "Server URL cannot be empty".to_string(),
35        ));
36    }
37
38    Ok(())
39}
40
41/// Format file size in human-readable format
42pub fn format_file_size(size: u64) -> String {
43    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
44    let mut size = size as f64;
45    let mut unit_index = 0;
46
47    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
48        size /= 1024.0;
49        unit_index += 1;
50    }
51
52    if unit_index == 0 {
53        format!("{} {}", size as u64, UNITS[unit_index])
54    } else {
55        format!("{:.1} {}", size, UNITS[unit_index])
56    }
57}
58
59/// Check if path is safe for file operations
60pub fn is_safe_path<P: AsRef<Path>>(path: P) -> bool {
61    let path = path.as_ref();
62
63    // Check for path traversal attempts
64    if path.to_string_lossy().contains("..") {
65        return false;
66    }
67
68    // Check for absolute paths to system directories
69    if let Ok(canonical) = path.canonicalize() {
70        let path_str = canonical.to_string_lossy().to_lowercase();
71
72        // Unix system paths
73        if path_str.starts_with("/etc")
74            || path_str.starts_with("/sys")
75            || path_str.starts_with("/proc")
76            || path_str.starts_with("/dev")
77            || path_str.starts_with("/boot")
78            || path_str.starts_with("/root")
79        {
80            return false;
81        }
82
83        // Windows system paths
84        if path_str.starts_with("c:\\windows")
85            || path_str.starts_with("c:\\system32")
86            || path_str.starts_with("c:\\program files")
87        {
88            return false;
89        }
90    }
91
92    true
93}
94
95/// Generate session ID
96pub fn generate_session_id() -> String {
97    format!(
98        "session-{}-{}",
99        std::time::SystemTime::now()
100            .duration_since(std::time::UNIX_EPOCH)
101            .unwrap()
102            .as_millis(),
103        uuid::Uuid::new_v4().to_string()[..8].to_string()
104    )
105}
106
107/// Log server startup information
108pub fn log_server_startup(config: &crate::common::ServerConfig) {
109    info!("🚀 Starting MCP Server");
110    info!("   Name: {}", config.name);
111    info!("   Description: {}", config.description);
112    info!("   Version: {}", config.version);
113    info!("   Address: {}:{}", config.host, config.port);
114    info!("   Max Connections: {}", config.max_connections);
115    info!("   Request Timeout: {}s", config.request_timeout_secs);
116}
117
118/// Log client startup information
119pub fn log_client_startup(config: &crate::common::ClientConfig) {
120    info!("🔌 Starting MCP Client");
121    info!("   Name: {}", config.name);
122    info!("   Version: {}", config.version);
123    info!("   Server URL: {}", config.server_url);
124    info!("   Connect Timeout: {}s", config.connect_timeout_secs);
125    info!("   Request Timeout: {}s", config.request_timeout_secs);
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::common::{ClientConfig, ServerConfig};
132
133    #[test]
134    fn test_validate_server_config() {
135        let mut config = ServerConfig::default();
136        assert!(validate_server_config(&config).is_ok());
137
138        config.name = "".to_string();
139        assert!(validate_server_config(&config).is_err());
140
141        config.name = "Test".to_string();
142        config.port = 0;
143        assert!(validate_server_config(&config).is_err());
144    }
145
146    #[test]
147    fn test_validate_client_config() {
148        let mut config = ClientConfig::default();
149        assert!(validate_client_config(&config).is_ok());
150
151        config.name = "".to_string();
152        assert!(validate_client_config(&config).is_err());
153
154        config.name = "Test".to_string();
155        config.server_url = "".to_string();
156        assert!(validate_client_config(&config).is_err());
157    }
158
159    #[test]
160    fn test_format_file_size() {
161        assert_eq!(format_file_size(0), "0 B");
162        assert_eq!(format_file_size(1023), "1023 B");
163        assert_eq!(format_file_size(1024), "1.0 KB");
164        assert_eq!(format_file_size(1536), "1.5 KB");
165        assert_eq!(format_file_size(1048576), "1.0 MB");
166    }
167
168    #[test]
169    fn test_is_safe_path() {
170        assert!(is_safe_path("./test.txt"));
171        assert!(is_safe_path("test/file.txt"));
172        assert!(!is_safe_path("../../../etc/passwd"));
173        assert!(!is_safe_path("test/../../../etc/passwd"));
174    }
175
176    #[test]
177    fn test_generate_session_id() {
178        let id1 = generate_session_id();
179        let id2 = generate_session_id();
180
181        assert_ne!(id1, id2);
182        assert!(id1.starts_with("session-"));
183        assert!(id2.starts_with("session-"));
184    }
185}