Skip to main content

mcp_memory/
config.rs

1use crate::errors::Result;
2use crate::Transport;
3
4/// How aggressively to push WAL writes to durable storage before acknowledging
5/// the client.
6///
7/// The default [`Async`](Durability::Async) flushes to the kernel page cache
8/// and returns immediately; the background sync thread calls `fsync` within
9/// ~1 second. Journal-mode filesystems (ext4, APFS, NTFS) typically absorb a
10/// power loss within that window.
11///
12/// [`Sync`](Durability::Sync) calls `fsync` before returning, confirming the
13/// data is on stable media. Use this when every write must survive an immediate
14/// power failure, at the cost of higher write latency.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Durability {
17    Async,
18    Sync,
19}
20
21impl Durability {
22    pub const fn is_sync(self) -> bool {
23        matches!(self, Durability::Sync)
24    }
25}
26
27impl std::str::FromStr for Durability {
28    type Err = String;
29    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
30        match s {
31            "async" | "Async" => Ok(Durability::Async),
32            "sync" | "Sync" => Ok(Durability::Sync),
33            _ => Err(format!("unknown durability '{s}'; expected 'async' or 'sync'")),
34        }
35    }
36}
37
38#[derive(Debug, Clone)]
39pub struct Config {
40    pub memory_file_path: String,
41    pub transport: Transport,
42    pub bind_addr: String,
43    pub durability: Durability,
44}
45
46impl Config {
47    pub fn from_args(args: &super::Args) -> Result<Self> {
48        let memory_file_path = args
49            .memory_file
50            .clone()
51            .or_else(|| std::env::var("MEMORY_FILE_PATH").ok())
52            .unwrap_or_else(|| "memory.mcpmem".to_string());
53
54        let durability = if let Ok(env) = std::env::var("MCP_MEMORY_DURABILITY") {
55            env.parse().unwrap_or_else(|e| {
56                tracing::warn!("MCP_MEMORY_DURABILITY parse failed: {e}; falling back to Async");
57                Durability::Async
58            })
59        } else {
60            Durability::Async
61        };
62
63        Ok(Config {
64            memory_file_path,
65            transport: args.transport,
66            bind_addr: args.bind.clone(),
67            durability,
68        })
69    }
70}
71
72impl Default for Config {
73    fn default() -> Self {
74        Self {
75            memory_file_path: "memory.mcpmem".to_string(),
76            transport: Transport::Stdio,
77            bind_addr: "127.0.0.1:8080".to_string(),
78            durability: Durability::Async,
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use clap::Parser;
87    use crate::Args;
88
89    #[test]
90    fn test_config_defaults() {
91        let args = Args::parse_from(["mcp-memory"]);
92        let cfg = Config::from_args(&args).unwrap();
93        assert_eq!(cfg.memory_file_path, "memory.mcpmem");
94    }
95
96    #[test]
97    fn test_config_custom_path() {
98        let args = Args::parse_from(["mcp-memory", "--memory-file", "/tmp/test.jsonl"]);
99        let cfg = Config::from_args(&args).unwrap();
100        assert_eq!(cfg.memory_file_path, "/tmp/test.jsonl");
101    }
102}