mcpkill 0.1.0

Universal MCP proxy — semantic cache + chunking to kill token waste
Documentation
use anyhow::Result;
use serde::Deserialize;

/// Settings loaded from `~/.mcpkill.toml`.
/// All fields are optional — unset fields fall back to CLI defaults.
///
/// Example `~/.mcpkill.toml`:
/// ```toml
/// max_chunks  = 6
/// threshold   = 0.80
/// ttl_days    = 14
/// max_db_mb   = 200
/// cache_db    = "~/.cache/mcpkill.db"
/// ```
#[derive(Deserialize, Default, Debug)]
pub struct FileConfig {
    pub max_chunks: Option<usize>,
    pub threshold: Option<f32>,
    pub ttl_days: Option<u64>,
    pub max_db_mb: Option<u64>,
    pub cache_db: Option<String>,
}

impl FileConfig {
    /// Load from `~/.mcpkill.toml`. Returns `Default::default()` if the file
    /// does not exist; returns an error only on parse failures.
    pub fn load() -> Result<Self> {
        let home = std::env::var("HOME").unwrap_or_default();
        let path = format!("{home}/.mcpkill.toml");

        match std::fs::read_to_string(&path) {
            Ok(contents) => {
                let cfg: FileConfig = toml::from_str(&contents)
                    .map_err(|e| anyhow::anyhow!("Failed to parse {path}: {e}"))?;
                Ok(cfg)
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
            Err(e) => Err(anyhow::anyhow!("Cannot read {path}: {e}")),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deserialize_partial_config() {
        let toml = r#"
            max_chunks = 8
            threshold  = 0.90
        "#;
        let cfg: FileConfig = toml::from_str(toml).unwrap();
        assert_eq!(cfg.max_chunks, Some(8));
        assert!((cfg.threshold.unwrap() - 0.90).abs() < 1e-5);
        assert!(cfg.ttl_days.is_none());
    }

    #[test]
    fn empty_toml_is_all_none() {
        let cfg: FileConfig = toml::from_str("").unwrap();
        assert!(cfg.max_chunks.is_none());
        assert!(cfg.threshold.is_none());
    }
}