dynamic-mcp 1.5.0

MCP proxy server that reduces LLM context overhead with on-demand tool loading from multiple upstream servers.
use anyhow::Result;
use notify::{
    Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
};
use std::path::Path;
use tokio::sync::mpsc;

pub struct ConfigWatcher {
    _watcher: RecommendedWatcher,
}

impl ConfigWatcher {
    pub fn new(config_path: &Path) -> Result<(Self, mpsc::Receiver<()>)> {
        let (tx, rx) = mpsc::channel(100);

        let config_path_buf = config_path.to_path_buf();
        let watch_path = config_path_buf.clone();

        let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| match res {
            Ok(event) => match event.kind {
                EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) => {
                    if event.paths.iter().any(|p| p == &watch_path) {
                        tracing::info!("Config file changed: {:?}, triggering reload", event.kind);
                        let _ = tx.blocking_send(());
                    }
                }
                _ => {}
            },
            Err(e) => {
                tracing::warn!("File watch error: {}", e);
            }
        })?;

        watcher.configure(Config::default())?;
        watcher.watch(&config_path_buf, RecursiveMode::NonRecursive)?;

        tracing::info!("Watching config file: {:?}", config_path_buf);

        Ok((Self { _watcher: watcher }, rx))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_watcher_creation_with_valid_path() {
        let mut config_file = NamedTempFile::new().unwrap();
        config_file
            .write_all(r#"{"mcpServers": {}}"#.as_bytes())
            .unwrap();
        config_file.flush().unwrap();

        let result = ConfigWatcher::new(config_file.path());
        assert!(
            result.is_ok(),
            "Should successfully create watcher for valid config file"
        );

        let (_watcher, _rx) = result.unwrap();
    }

    #[test]
    fn test_watcher_fails_with_nonexistent_path() {
        let result = ConfigWatcher::new(std::path::Path::new("/nonexistent/path/config.json"));
        assert!(
            result.is_err(),
            "Should fail to create watcher for nonexistent path"
        );
    }
}