Skip to main content

mcp_memory/
config.rs

1use crate::errors::{MCSError, Result};
2use crate::Transport;
3use std::sync::Arc;
4
5/// How aggressively to push WAL writes to durable storage before acknowledging
6/// the client.
7///
8/// The default [`Async`](Durability::Async) flushes to the kernel page cache
9/// and returns immediately; the background sync thread calls `fsync` within
10/// ~1 second. Journal-mode filesystems (ext4, APFS, NTFS) typically absorb a
11/// power loss within that window.
12///
13/// [`Sync`](Durability::Sync) calls `fsync` before returning, confirming the
14/// data is on stable media. Use this when every write must survive an immediate
15/// power failure, at the cost of higher write latency.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Durability {
18    Async,
19    Sync,
20}
21
22impl Durability {
23    pub const fn is_sync(self) -> bool {
24        matches!(self, Durability::Sync)
25    }
26}
27
28impl std::str::FromStr for Durability {
29    type Err = String;
30    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
31        match s {
32            "async" | "Async" => Ok(Durability::Async),
33            "sync" | "Sync" => Ok(Durability::Sync),
34            _ => Err(format!("unknown durability '{s}'; expected 'async' or 'sync'")),
35        }
36    }
37}
38
39#[derive(Debug, Clone)]
40pub struct Config {
41    pub memory_file_path: String,
42    pub transport: Transport,
43    pub bind_addr: String,
44    pub durability: Durability,
45    /// Optional bearer token required on the `tcp` and `http` transports. When
46    /// `None`, those transports accept unauthenticated connections (stdio is
47    /// always local and never authenticated).
48    pub auth_token: Option<Arc<str>>,
49}
50
51impl Config {
52    pub fn from_args(args: &super::Args) -> Result<Self> {
53        let memory_file_path = args
54            .memory_file
55            .clone()
56            .or_else(|| std::env::var("MEMORY_FILE_PATH").ok())
57            .unwrap_or_else(|| "memory.mcpmem".to_string());
58
59        // Resolve the auth token from --auth-token, then --auth-token-file, then
60        // the MCP_MEMORY_AUTH_TOKEN env var. A configured-but-empty token file
61        // is a hard error: fail closed rather than silently disabling auth.
62        let auth_token: Option<Arc<str>> = if let Some(t) = args.auth_token.clone() {
63            Some(Arc::from(t.as_str()))
64        } else if let Some(path) = args.auth_token_file.clone() {
65            let contents = std::fs::read_to_string(&path).map_err(|e| {
66                MCSError::InvalidParams(format!("failed to read --auth-token-file '{path}': {e}"))
67            })?;
68            let token = contents.trim();
69            if token.is_empty() {
70                return Err(MCSError::InvalidParams(format!(
71                    "--auth-token-file '{path}' is empty; refusing to start with auth disabled"
72                )));
73            }
74            Some(Arc::from(token))
75        } else {
76            std::env::var("MCP_MEMORY_AUTH_TOKEN")
77                .ok()
78                .filter(|t| !t.is_empty())
79                .map(|t| Arc::from(t.as_str()))
80        };
81
82        let durability = if let Ok(env) = std::env::var("MCP_MEMORY_DURABILITY") {
83            env.parse().unwrap_or_else(|e| {
84                tracing::warn!("MCP_MEMORY_DURABILITY parse failed: {e}; falling back to Async");
85                Durability::Async
86            })
87        } else {
88            Durability::Async
89        };
90
91        Ok(Config {
92            memory_file_path,
93            transport: args.transport,
94            bind_addr: args.bind.clone(),
95            durability,
96            auth_token,
97        })
98    }
99}
100
101impl Default for Config {
102    fn default() -> Self {
103        Self {
104            memory_file_path: "memory.mcpmem".to_string(),
105            transport: Transport::Stdio,
106            bind_addr: "127.0.0.1:8080".to_string(),
107            durability: Durability::Async,
108            auth_token: None,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use clap::Parser;
117    use crate::Args;
118
119    #[test]
120    fn test_config_defaults() {
121        let args = Args::parse_from(["mcp-memory"]);
122        let cfg = Config::from_args(&args).unwrap();
123        assert_eq!(cfg.memory_file_path, "memory.mcpmem");
124    }
125
126    #[test]
127    fn test_config_custom_path() {
128        let args = Args::parse_from(["mcp-memory", "--memory-file", "/tmp/test.jsonl"]);
129        let cfg = Config::from_args(&args).unwrap();
130        assert_eq!(cfg.memory_file_path, "/tmp/test.jsonl");
131    }
132}