1use crate::errors::{MCSError, Result};
2use crate::Transport;
3use std::sync::Arc;
4
5#[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 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 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}