ricecoder_mcp/
hot_reload.rs

1//! Configuration hot-reload support for MCP
2
3use crate::config::{MCPConfig, MCPConfigLoader};
4use crate::error::Result;
5use crate::registry::ToolRegistry;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tracing::{debug, info, warn};
10
11/// Configuration watcher for hot-reload support
12#[derive(Debug, Clone)]
13pub struct ConfigWatcher {
14    config_dir: PathBuf,
15    current_config: Arc<RwLock<MCPConfig>>,
16    #[allow(dead_code)]
17    tool_registry: Arc<ToolRegistry>,
18}
19
20impl ConfigWatcher {
21    /// Creates a new configuration watcher
22    ///
23    /// # Arguments
24    /// * `config_dir` - Directory to watch for configuration changes
25    /// * `tool_registry` - Tool registry to update on configuration changes
26    pub fn new<P: AsRef<Path>>(config_dir: P, tool_registry: Arc<ToolRegistry>) -> Self {
27        Self {
28            config_dir: config_dir.as_ref().to_path_buf(),
29            current_config: Arc::new(RwLock::new(MCPConfig::new())),
30            tool_registry,
31        }
32    }
33
34    /// Loads initial configuration from the configuration directory
35    pub async fn load_initial_config(&self) -> Result<()> {
36        debug!("Loading initial configuration from: {:?}", self.config_dir);
37
38        let config = MCPConfigLoader::load_from_directory(&self.config_dir)?;
39
40        // Validate configuration
41        MCPConfigLoader::validate(&config)?;
42
43        // Update current configuration
44        let mut current = self.current_config.write().await;
45        *current = config.clone();
46        drop(current);
47
48        // Update tool registry with loaded tools
49        self.update_registry(&config).await?;
50
51        info!("Initial configuration loaded successfully");
52        Ok(())
53    }
54
55    /// Detects configuration file changes
56    ///
57    /// This method should be run in a background task to continuously monitor for changes.
58    pub async fn watch_for_changes(&self) -> Result<()> {
59        debug!("Starting configuration file watcher");
60
61        loop {
62            // Check for configuration file changes
63            if let Err(e) = self.check_for_changes().await {
64                warn!("Error checking for configuration changes: {}", e);
65            }
66
67            // Sleep for a short duration before checking again
68            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
69        }
70    }
71
72    /// Checks for configuration file changes
73    async fn check_for_changes(&self) -> Result<()> {
74        debug!("Checking for configuration file changes");
75
76        // Try to load configuration from directory
77        let new_config = MCPConfigLoader::load_from_directory(&self.config_dir)?;
78
79        // Validate new configuration
80        MCPConfigLoader::validate(&new_config)?;
81
82        // Compare with current configuration
83        let current = self.current_config.read().await;
84        if self.configs_differ(&current, &new_config) {
85            drop(current);
86
87            info!("Configuration changes detected, reloading");
88
89            // Update current configuration
90            let mut current = self.current_config.write().await;
91            *current = new_config.clone();
92            drop(current);
93
94            // Update tool registry with new tools
95            self.update_registry(&new_config).await?;
96
97            info!("Configuration reloaded successfully");
98        }
99
100        Ok(())
101    }
102
103    /// Compares two configurations to detect changes
104    fn configs_differ(&self, config1: &MCPConfig, config2: &MCPConfig) -> bool {
105        // Compare servers
106        if config1.servers.len() != config2.servers.len() {
107            return true;
108        }
109
110        for (server1, server2) in config1.servers.iter().zip(config2.servers.iter()) {
111            if server1.id != server2.id
112                || server1.command != server2.command
113                || server1.timeout_ms != server2.timeout_ms
114            {
115                return true;
116            }
117        }
118
119        // Compare custom tools
120        if config1.custom_tools.len() != config2.custom_tools.len() {
121            return true;
122        }
123
124        for (tool1, tool2) in config1.custom_tools.iter().zip(config2.custom_tools.iter()) {
125            if tool1.id != tool2.id || tool1.handler != tool2.handler {
126                return true;
127            }
128        }
129
130        // Compare permissions
131        if config1.permissions.len() != config2.permissions.len() {
132            return true;
133        }
134
135        for (perm1, perm2) in config1.permissions.iter().zip(config2.permissions.iter()) {
136            if perm1.pattern != perm2.pattern || perm1.level != perm2.level {
137                return true;
138            }
139        }
140
141        false
142    }
143
144    /// Updates the tool registry with new configuration
145    async fn update_registry(&self, config: &MCPConfig) -> Result<()> {
146        debug!("Updating tool registry with new configuration");
147
148        // Note: Tool registry updates would be performed here
149        // In a real implementation, this would convert CustomToolConfig to ToolMetadata
150        // and register them with the tool registry
151
152        info!("Tool registry updated with {} custom tools", config.custom_tools.len());
153        Ok(())
154    }
155
156    /// Gets the current configuration
157    pub async fn get_current_config(&self) -> MCPConfig {
158        self.current_config.read().await.clone()
159    }
160
161    /// Manually reloads configuration
162    pub async fn reload_config(&self) -> Result<()> {
163        debug!("Manually reloading configuration");
164
165        let config = MCPConfigLoader::load_from_directory(&self.config_dir)?;
166
167        // Validate configuration
168        MCPConfigLoader::validate(&config)?;
169
170        // Update current configuration
171        let mut current = self.current_config.write().await;
172        *current = config.clone();
173        drop(current);
174
175        // Update tool registry
176        self.update_registry(&config).await?;
177
178        info!("Configuration reloaded successfully");
179        Ok(())
180    }
181
182    /// Validates new configuration before applying
183    pub async fn validate_new_config<P: AsRef<Path>>(&self, config_path: P) -> Result<MCPConfig> {
184        debug!("Validating new configuration from: {:?}", config_path.as_ref());
185
186        let config = MCPConfigLoader::load_from_file(config_path)?;
187        MCPConfigLoader::validate(&config)?;
188
189        info!("Configuration validation passed");
190        Ok(config)
191    }
192
193    /// Gets the configuration directory
194    pub fn config_dir(&self) -> &Path {
195        &self.config_dir
196    }
197
198    /// Sets the configuration directory
199    pub fn set_config_dir<P: AsRef<Path>>(&mut self, dir: P) {
200        self.config_dir = dir.as_ref().to_path_buf();
201        debug!("Configuration directory changed to: {:?}", self.config_dir);
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::registry::ToolRegistry;
209    use tempfile::TempDir;
210
211    #[tokio::test]
212    async fn test_create_config_watcher() {
213        let temp_dir = TempDir::new().expect("Failed to create temp dir");
214        let registry = Arc::new(ToolRegistry::new());
215        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
216
217        assert_eq!(watcher.config_dir(), temp_dir.path());
218    }
219
220    #[tokio::test]
221    async fn test_load_initial_config() {
222        let temp_dir = TempDir::new().expect("Failed to create temp dir");
223        let config_path = temp_dir.path().join("mcp-servers.yaml");
224
225        let config_content = r#"
226servers:
227  - id: test-server
228    name: Test Server
229    command: test
230    args: []
231    env: {}
232    timeout_ms: 5000
233    auto_reconnect: true
234    max_retries: 3
235custom_tools: []
236permissions: []
237"#;
238
239        std::fs::write(&config_path, config_content).expect("Failed to write config");
240
241        let registry = Arc::new(ToolRegistry::new());
242        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
243
244        let result = watcher.load_initial_config().await;
245        assert!(result.is_ok());
246
247        let config = watcher.get_current_config().await;
248        assert_eq!(config.servers.len(), 1);
249    }
250
251    #[tokio::test]
252    async fn test_configs_differ_servers() {
253        let temp_dir = TempDir::new().expect("Failed to create temp dir");
254        let registry = Arc::new(ToolRegistry::new());
255        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
256
257        let mut config1 = MCPConfig::new();
258        let config2 = MCPConfig::new();
259
260        config1.add_server(crate::config::MCPServerConfig {
261            id: "server1".to_string(),
262            name: "Server 1".to_string(),
263            command: "cmd1".to_string(),
264            args: vec![],
265            env: std::collections::HashMap::new(),
266            timeout_ms: 5000,
267            auto_reconnect: true,
268            max_retries: 3,
269        });
270
271        assert!(watcher.configs_differ(&config1, &config2));
272    }
273
274    #[tokio::test]
275    async fn test_configs_same() {
276        let temp_dir = TempDir::new().expect("Failed to create temp dir");
277        let registry = Arc::new(ToolRegistry::new());
278        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
279
280        let config1 = MCPConfig::new();
281        let config2 = MCPConfig::new();
282
283        assert!(!watcher.configs_differ(&config1, &config2));
284    }
285
286    #[tokio::test]
287    async fn test_validate_new_config() {
288        let temp_dir = TempDir::new().expect("Failed to create temp dir");
289        let config_path = temp_dir.path().join("test-config.yaml");
290
291        let config_content = r#"
292servers:
293  - id: test-server
294    name: Test Server
295    command: test
296    args: []
297    env: {}
298    timeout_ms: 5000
299    auto_reconnect: true
300    max_retries: 3
301custom_tools: []
302permissions: []
303"#;
304
305        std::fs::write(&config_path, config_content).expect("Failed to write config");
306
307        let registry = Arc::new(ToolRegistry::new());
308        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
309
310        let result = watcher.validate_new_config(&config_path).await;
311        assert!(result.is_ok());
312
313        let config = result.unwrap();
314        assert_eq!(config.servers.len(), 1);
315    }
316
317    #[tokio::test]
318    async fn test_validate_invalid_config() {
319        let temp_dir = TempDir::new().expect("Failed to create temp dir");
320        let config_path = temp_dir.path().join("invalid-config.yaml");
321
322        let config_content = r#"
323servers:
324  - id: ""
325    name: Test Server
326    command: test
327    args: []
328    env: {}
329    timeout_ms: 5000
330    auto_reconnect: true
331    max_retries: 3
332custom_tools: []
333permissions: []
334"#;
335
336        std::fs::write(&config_path, config_content).expect("Failed to write config");
337
338        let registry = Arc::new(ToolRegistry::new());
339        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
340
341        let result = watcher.validate_new_config(&config_path).await;
342        assert!(result.is_err());
343    }
344
345    #[tokio::test]
346    async fn test_set_config_dir() {
347        let temp_dir1 = TempDir::new().expect("Failed to create temp dir");
348        let temp_dir2 = TempDir::new().expect("Failed to create temp dir");
349
350        let registry = Arc::new(ToolRegistry::new());
351        let mut watcher = ConfigWatcher::new(temp_dir1.path(), registry);
352
353        assert_eq!(watcher.config_dir(), temp_dir1.path());
354
355        watcher.set_config_dir(temp_dir2.path());
356        assert_eq!(watcher.config_dir(), temp_dir2.path());
357    }
358
359    #[tokio::test]
360    async fn test_reload_config() {
361        let temp_dir = TempDir::new().expect("Failed to create temp dir");
362        let config_path = temp_dir.path().join("mcp-servers.yaml");
363
364        let config_content = r#"
365servers:
366  - id: test-server
367    name: Test Server
368    command: test
369    args: []
370    env: {}
371    timeout_ms: 5000
372    auto_reconnect: true
373    max_retries: 3
374custom_tools: []
375permissions: []
376"#;
377
378        std::fs::write(&config_path, config_content).expect("Failed to write config");
379
380        let registry = Arc::new(ToolRegistry::new());
381        let watcher = ConfigWatcher::new(temp_dir.path(), registry);
382
383        let result = watcher.reload_config().await;
384        assert!(result.is_ok());
385
386        let config = watcher.get_current_config().await;
387        assert_eq!(config.servers.len(), 1);
388    }
389}