ricecoder_mcp/
hot_reload.rs1use 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#[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 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 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 MCPConfigLoader::validate(&config)?;
42
43 let mut current = self.current_config.write().await;
45 *current = config.clone();
46 drop(current);
47
48 self.update_registry(&config).await?;
50
51 info!("Initial configuration loaded successfully");
52 Ok(())
53 }
54
55 pub async fn watch_for_changes(&self) -> Result<()> {
59 debug!("Starting configuration file watcher");
60
61 loop {
62 if let Err(e) = self.check_for_changes().await {
64 warn!("Error checking for configuration changes: {}", e);
65 }
66
67 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
69 }
70 }
71
72 async fn check_for_changes(&self) -> Result<()> {
74 debug!("Checking for configuration file changes");
75
76 let new_config = MCPConfigLoader::load_from_directory(&self.config_dir)?;
78
79 MCPConfigLoader::validate(&new_config)?;
81
82 let current = self.current_config.read().await;
84 if self.configs_differ(¤t, &new_config) {
85 drop(current);
86
87 info!("Configuration changes detected, reloading");
88
89 let mut current = self.current_config.write().await;
91 *current = new_config.clone();
92 drop(current);
93
94 self.update_registry(&new_config).await?;
96
97 info!("Configuration reloaded successfully");
98 }
99
100 Ok(())
101 }
102
103 fn configs_differ(&self, config1: &MCPConfig, config2: &MCPConfig) -> bool {
105 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 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 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 async fn update_registry(&self, config: &MCPConfig) -> Result<()> {
146 debug!("Updating tool registry with new configuration");
147
148 info!("Tool registry updated with {} custom tools", config.custom_tools.len());
153 Ok(())
154 }
155
156 pub async fn get_current_config(&self) -> MCPConfig {
158 self.current_config.read().await.clone()
159 }
160
161 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 MCPConfigLoader::validate(&config)?;
169
170 let mut current = self.current_config.write().await;
172 *current = config.clone();
173 drop(current);
174
175 self.update_registry(&config).await?;
177
178 info!("Configuration reloaded successfully");
179 Ok(())
180 }
181
182 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 pub fn config_dir(&self) -> &Path {
195 &self.config_dir
196 }
197
198 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}