Skip to main content

smith_config/
mcp.rs

1//! MCP (Model Context Protocol) configuration for Smith
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7/// MCP configuration for Smith platform
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct McpConfig {
10    /// Whether MCP adapter is enabled
11    pub enabled: bool,
12
13    /// Global timeout for MCP operations (default: 30s)
14    pub default_timeout_ms: u64,
15
16    /// Maximum number of concurrent MCP servers
17    pub max_servers: usize,
18
19    /// Maximum number of retries for failed operations
20    pub max_retries: u32,
21
22    /// Retry delay in milliseconds
23    pub retry_delay_ms: u64,
24
25    /// Configured MCP servers
26    pub servers: Vec<McpServerConfig>,
27
28    /// Global environment variables to pass to all MCP servers
29    pub global_env: HashMap<String, String>,
30
31    /// Working directory for MCP server processes
32    pub work_dir: Option<PathBuf>,
33
34    /// Enable detailed MCP protocol logging
35    pub debug_logging: bool,
36}
37
38/// Individual MCP server configuration
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct McpServerConfig {
41    /// Unique identifier for this server
42    pub name: String,
43
44    /// Human-readable description
45    pub description: Option<String>,
46
47    /// Command to execute for stdio transport
48    pub command: String,
49
50    /// Command arguments
51    pub args: Vec<String>,
52
53    /// Working directory for this server
54    pub cwd: Option<PathBuf>,
55
56    /// Whether this server is enabled
57    pub enabled: bool,
58
59    /// Specific timeout for this server (overrides global)
60    pub timeout_ms: Option<u64>,
61
62    /// Environment variables specific to this server
63    pub env: HashMap<String, String>,
64
65    /// Auto-start this server on manager initialization
66    pub auto_start: bool,
67
68    /// Tags for server categorization and discovery
69    pub tags: Vec<String>,
70
71    /// Server priority (higher = more preferred for tool conflicts)
72    pub priority: i32,
73
74    /// Allow filesystem access for this server
75    pub allow_filesystem: bool,
76
77    /// Allow network access for this server
78    pub allow_network: bool,
79
80    /// Maximum execution time per tool call
81    pub max_execution_time_ms: u64,
82
83    /// Trust level (1-5, 5 = highest trust)
84    pub trust_level: u8,
85}
86
87impl Default for McpConfig {
88    fn default() -> Self {
89        Self {
90            enabled: false,
91            default_timeout_ms: 30_000,
92            max_servers: 10,
93            max_retries: 3,
94            retry_delay_ms: 1_000,
95            servers: vec![],
96            global_env: HashMap::new(),
97            work_dir: None,
98            debug_logging: false,
99        }
100    }
101}
102
103impl Default for McpServerConfig {
104    fn default() -> Self {
105        Self {
106            name: String::new(),
107            description: None,
108            command: String::new(),
109            args: vec![],
110            cwd: None,
111            enabled: true,
112            timeout_ms: None,
113            env: HashMap::new(),
114            auto_start: true,
115            tags: vec![],
116            priority: 0,
117            allow_filesystem: false,
118            allow_network: false,
119            max_execution_time_ms: 30_000,
120            trust_level: 1,
121        }
122    }
123}
124
125impl McpConfig {
126    /// Create a development configuration with lenient security
127    pub fn development() -> Self {
128        Self {
129            enabled: true,
130            default_timeout_ms: 30_000,
131            max_servers: 10,
132            max_retries: 3,
133            retry_delay_ms: 1_000,
134            servers: vec![],
135            global_env: HashMap::new(),
136            work_dir: Some(std::env::temp_dir()),
137            debug_logging: true,
138        }
139    }
140
141    /// Create a production configuration with strict security
142    pub fn production() -> Self {
143        Self {
144            enabled: true,
145            default_timeout_ms: 15_000,
146            max_servers: 5,
147            max_retries: 2,
148            retry_delay_ms: 2_000,
149            servers: vec![],
150            global_env: HashMap::new(),
151            work_dir: None,
152            debug_logging: false,
153        }
154    }
155
156    /// Validate the configuration
157    pub fn validate(&self) -> Result<(), String> {
158        if self.max_servers == 0 {
159            return Err("max_servers must be greater than 0".to_string());
160        }
161
162        if self.default_timeout_ms == 0 {
163            return Err("default_timeout_ms must be greater than 0".to_string());
164        }
165
166        // Validate server configurations
167        for server in &self.servers {
168            server.validate()?;
169        }
170
171        // Check for duplicate server names
172        let mut names = std::collections::HashSet::new();
173        for server in &self.servers {
174            if !names.insert(&server.name) {
175                return Err(format!("Duplicate server name: '{}'", server.name));
176            }
177        }
178
179        Ok(())
180    }
181
182    /// Find server configuration by name
183    pub fn find_server(&self, name: &str) -> Option<&McpServerConfig> {
184        self.servers.iter().find(|s| s.name == name)
185    }
186
187    /// Get enabled servers
188    pub fn enabled_servers(&self) -> impl Iterator<Item = &McpServerConfig> {
189        self.servers.iter().filter(|s| s.enabled)
190    }
191}
192
193impl McpServerConfig {
194    /// Validate this server configuration
195    pub fn validate(&self) -> Result<(), String> {
196        if self.name.is_empty() {
197            return Err("Server name cannot be empty".to_string());
198        }
199
200        if self.command.is_empty() {
201            return Err(format!("Server '{}' command cannot be empty", self.name));
202        }
203
204        if let Some(timeout) = self.timeout_ms {
205            if timeout == 0 {
206                return Err(format!(
207                    "Server '{}' timeout must be greater than 0",
208                    self.name
209                ));
210            }
211        }
212
213        if self.trust_level == 0 || self.trust_level > 5 {
214            return Err(format!(
215                "Server '{}' trust_level must be between 1 and 5",
216                self.name
217            ));
218        }
219
220        Ok(())
221    }
222
223    /// Check if server has a specific tag
224    pub fn has_tag(&self, tag: &str) -> bool {
225        self.tags.contains(&tag.to_string())
226    }
227
228    /// Get display name for this server
229    pub fn display_name(&self) -> &str {
230        self.description.as_deref().unwrap_or(&self.name)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_config_defaults() {
240        let config = McpConfig::default();
241        assert!(!config.enabled);
242        assert_eq!(config.max_servers, 10);
243
244        let dev_config = McpConfig::development();
245        assert!(dev_config.enabled);
246        assert!(dev_config.debug_logging);
247
248        let prod_config = McpConfig::production();
249        assert!(prod_config.enabled);
250        assert!(!prod_config.debug_logging);
251        assert_eq!(prod_config.default_timeout_ms, 15_000);
252    }
253
254    #[test]
255    fn test_server_config_validation() {
256        let mut server = McpServerConfig {
257            name: "test".to_string(),
258            command: "python".to_string(),
259            args: vec!["-m".to_string(), "mcp_server".to_string()],
260            ..Default::default()
261        };
262
263        assert!(server.validate().is_ok());
264
265        // Test empty name
266        server.name.clear();
267        assert!(server.validate().is_err());
268
269        // Test empty command
270        server.name = "test".to_string();
271        server.command.clear();
272        assert!(server.validate().is_err());
273
274        // Test invalid trust level
275        server.command = "python".to_string();
276        server.trust_level = 0;
277        assert!(server.validate().is_err());
278
279        server.trust_level = 6;
280        assert!(server.validate().is_err());
281
282        server.trust_level = 3;
283        assert!(server.validate().is_ok());
284    }
285
286    #[test]
287    fn test_config_validation() {
288        let mut config = McpConfig::development();
289        assert!(config.validate().is_ok());
290
291        // Test invalid max_servers
292        config.max_servers = 0;
293        assert!(config.validate().is_err());
294
295        // Test duplicate server names
296        config.max_servers = 10;
297        config.servers = vec![
298            McpServerConfig {
299                name: "duplicate".to_string(),
300                command: "python".to_string(),
301                ..Default::default()
302            },
303            McpServerConfig {
304                name: "duplicate".to_string(),
305                command: "node".to_string(),
306                ..Default::default()
307            },
308        ];
309        assert!(config.validate().is_err());
310    }
311
312    #[test]
313    fn test_server_helpers() {
314        let server = McpServerConfig {
315            name: "test".to_string(),
316            description: Some("Test Server".to_string()),
317            command: "python".to_string(),
318            tags: vec!["development".to_string(), "testing".to_string()],
319            trust_level: 3,
320            ..Default::default()
321        };
322
323        assert!(server.has_tag("development"));
324        assert!(server.has_tag("testing"));
325        assert!(!server.has_tag("production"));
326        assert_eq!(server.display_name(), "Test Server");
327    }
328
329    #[test]
330    fn test_config_server_finding() {
331        let mut config = McpConfig::development();
332        config.servers = vec![
333            McpServerConfig {
334                name: "server1".to_string(),
335                command: "python".to_string(),
336                enabled: true,
337                ..Default::default()
338            },
339            McpServerConfig {
340                name: "server2".to_string(),
341                command: "node".to_string(),
342                enabled: false,
343                ..Default::default()
344            },
345        ];
346
347        assert!(config.find_server("server1").is_some());
348        assert!(config.find_server("server2").is_some());
349        assert!(config.find_server("nonexistent").is_none());
350
351        let enabled_servers: Vec<_> = config.enabled_servers().collect();
352        assert_eq!(enabled_servers.len(), 1);
353        assert_eq!(enabled_servers[0].name, "server1");
354    }
355}