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, Copy)]
44pub struct SqliteTuning {
45 pub mmap_size: i64,
47 pub page_size: i64,
49 pub cache_size_kb: i64,
51 pub busy_timeout_ms: u64,
53 pub journal_size_limit: i64,
55}
56
57impl Default for SqliteTuning {
58 fn default() -> Self {
59 Self {
60 mmap_size: 268_435_456, page_size: 4096, cache_size_kb: 50_000, busy_timeout_ms: 5000,
64 journal_size_limit: 134_217_728, }
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct Config {
71 pub memory_file_path: String,
72 pub transport: Transport,
73 pub bind_addr: String,
74 pub durability: Durability,
75 pub auth_token: Option<Arc<str>>,
79 pub mmap_size: i64,
80 pub page_size: i64,
82 pub cache_size_kb: i64,
84 pub busy_timeout_ms: u64,
86 pub wal_flush_ms: u64,
89 pub lru_cache_size: usize,
90 pub read_pool_size: usize,
92 pub tls_cert: Option<std::path::PathBuf>,
96 pub tls_key: Option<std::path::PathBuf>,
98 pub vectors_enabled: bool,
101 pub code_enabled: bool,
104}
105
106pub fn resolve_read_pool_size(requested: usize) -> usize {
111 if requested == 0 {
112 std::thread::available_parallelism()
113 .map(|n| n.get())
114 .unwrap_or(4)
115 .clamp(1, 32)
116 } else {
117 requested.max(1)
118 }
119}
120
121impl Config {
122 pub fn sqlite_tuning(&self) -> SqliteTuning {
125 SqliteTuning {
126 mmap_size: self.mmap_size,
127 page_size: self.page_size,
128 cache_size_kb: self.cache_size_kb,
129 busy_timeout_ms: self.busy_timeout_ms,
130 ..SqliteTuning::default()
131 }
132 }
133
134 pub fn from_args(args: &super::Args) -> Result<Self> {
135 let memory_file_path = args
136 .memory_file
137 .clone()
138 .or_else(|| std::env::var("MEMORY_FILE_PATH").ok())
139 .unwrap_or_else(|| "memory.mcpmem".to_string());
140
141 let auth_token: Option<Arc<str>> = if let Some(t) = args.auth_token.clone() {
145 Some(Arc::from(t.as_str()))
146 } else if let Some(path) = args.auth_token_file.clone() {
147 let contents = std::fs::read_to_string(&path).map_err(|e| {
148 MCSError::InvalidParams(format!("failed to read --auth-token-file '{path}': {e}"))
149 })?;
150 let token = contents.trim();
151 if token.is_empty() {
152 return Err(MCSError::InvalidParams(format!(
153 "--auth-token-file '{path}' is empty; refusing to start with auth disabled"
154 )));
155 }
156 Some(Arc::from(token))
157 } else {
158 std::env::var("MCP_MEMORY_AUTH_TOKEN")
159 .ok()
160 .filter(|t| !t.is_empty())
161 .map(|t| Arc::from(t.as_str()))
162 };
163
164 let durability = if let Ok(env) = std::env::var("MCP_MEMORY_DURABILITY") {
165 env.parse().unwrap_or_else(|e| {
166 tracing::warn!("MCP_MEMORY_DURABILITY parse failed: {e}; falling back to Async");
167 Durability::Async
168 })
169 } else {
170 Durability::Async
171 };
172
173 let tls_cert = args
176 .tls_cert
177 .clone()
178 .or_else(|| std::env::var("MCP_TLS_CERT").ok())
179 .filter(|s| !s.is_empty())
180 .map(std::path::PathBuf::from);
181 let tls_key = args
182 .tls_key
183 .clone()
184 .or_else(|| std::env::var("MCP_TLS_KEY").ok())
185 .filter(|s| !s.is_empty())
186 .map(std::path::PathBuf::from);
187 if tls_cert.is_some() != tls_key.is_some() {
188 return Err(MCSError::InvalidParams(
189 "--tls-cert and --tls-key must be provided together (or both omitted for plaintext HTTP)"
190 .to_string(),
191 ));
192 }
193
194 Ok(Config {
195 memory_file_path,
196 transport: args.transport,
197 bind_addr: args.bind.clone(),
198 durability,
199 auth_token,
200 mmap_size: args.mmap_size,
201 page_size: args.page_size,
202 cache_size_kb: args.cache_size_mb.saturating_mul(1024),
203 busy_timeout_ms: args.busy_timeout_ms,
204 wal_flush_ms: args.wal_flush_ms,
205 lru_cache_size: args.lru_cache_size,
206 read_pool_size: resolve_read_pool_size(args.read_pool_size),
207 tls_cert,
208 tls_key,
209 vectors_enabled: args.vectors,
210 code_enabled: args.code,
211 })
212 }
213}
214
215impl Default for Config {
216 fn default() -> Self {
217 Self {
218 memory_file_path: "memory.mcpmem".to_string(),
219 transport: Transport::Stdio,
220 bind_addr: "127.0.0.1:8080".to_string(),
221 durability: Durability::Async,
222 auth_token: None,
223 mmap_size: 268435456,
224 page_size: SqliteTuning::default().page_size,
225 cache_size_kb: SqliteTuning::default().cache_size_kb,
226 busy_timeout_ms: SqliteTuning::default().busy_timeout_ms,
227 wal_flush_ms: 250,
228 lru_cache_size: 10000,
229 read_pool_size: 4,
230 tls_cert: None,
231 tls_key: None,
232 vectors_enabled: false,
233 code_enabled: false,
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_resolve_read_pool_size_auto_scales_within_bounds() {
244 let auto = resolve_read_pool_size(0);
245 assert!((1..=32).contains(&auto), "auto pool {auto} out of [1,32]");
246 assert_eq!(
247 auto,
248 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(1, 32)
249 );
250 }
251
252 #[test]
253 fn test_resolve_read_pool_size_honours_explicit_values() {
254 assert_eq!(resolve_read_pool_size(1), 1);
255 assert_eq!(resolve_read_pool_size(8), 8);
256 assert_eq!(resolve_read_pool_size(100), 100);
258 }
259}
260