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/// Tunable SQLite pragmas applied when opening the database. `page_size` and
40/// `auto_vacuum` only take effect on a freshly-created database (they are fixed
41/// once the file has content); the rest apply on every open. Defaults target a
42/// Linux host (4 KiB pages match the OS page / filesystem block size).
43#[derive(Debug, Clone, Copy)]
44pub struct SqliteTuning {
45    /// `PRAGMA mmap_size` in bytes.
46    pub mmap_size: i64,
47    /// `PRAGMA page_size` in bytes (fresh DB only). Must be a power of two.
48    pub page_size: i64,
49    /// `PRAGMA cache_size` magnitude in KiB (applied as the negative form).
50    pub cache_size_kb: i64,
51    /// `PRAGMA busy_timeout` in milliseconds.
52    pub busy_timeout_ms: u64,
53    /// `PRAGMA journal_size_limit` in bytes.
54    pub journal_size_limit: i64,
55}
56
57impl Default for SqliteTuning {
58    fn default() -> Self {
59        Self {
60            mmap_size: 268_435_456,          // 256 MiB
61            page_size: 4096,                 // 4 KiB — matches Linux page/fs block
62            cache_size_kb: 50_000,           // ~50 MiB
63            busy_timeout_ms: 5000,
64            journal_size_limit: 134_217_728, // 128 MiB
65        }
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    /// Optional bearer token required on the `tcp` and `http` transports. When
76    /// `None`, those transports accept unauthenticated connections (stdio is
77    /// always local and never authenticated).
78    pub auth_token: Option<Arc<str>>,
79    pub mmap_size: i64,
80    /// `PRAGMA page_size` in bytes (fresh DB only).
81    pub page_size: i64,
82    /// `PRAGMA cache_size` magnitude in KiB.
83    pub cache_size_kb: i64,
84    /// `PRAGMA busy_timeout` in milliseconds.
85    pub busy_timeout_ms: u64,
86    /// Interval in milliseconds for the background `wal_checkpoint(PASSIVE)`
87    /// flush. `0` disables it (rely on SQLite auto-checkpoint + maintenance).
88    pub wal_flush_ms: u64,
89    pub lru_cache_size: usize,
90    /// Size of the read-only connection pool (concurrent reads). Always >= 1.
91    pub read_pool_size: usize,
92    /// PEM certificate chain for serving the `http` transport over TLS (HTTPS).
93    /// `None` (the default) keeps the transport plaintext. Engaged only when
94    /// both `tls_cert` and `tls_key` are set.
95    pub tls_cert: Option<std::path::PathBuf>,
96    /// PEM private key matching `tls_cert`.
97    pub tls_key: Option<std::path::PathBuf>,
98    /// Enable the vector / semantic-search subsystem (`vector_*` + `hybrid_search`
99    /// tools backed by a usearch HNSW index). Off by default.
100    pub vectors_enabled: bool,
101    /// Enable the tree-sitter code-symbol subsystem (`code_*` tools). Off by
102    /// default. Only effective when built with the `code` feature.
103    pub code_enabled: bool,
104}
105
106/// Resolve the read-only connection-pool size. `0` means "auto": scale to the
107/// number of available CPUs (clamped to `[1, 32]` so a many-core host doesn't
108/// open an unreasonable number of connections, each carrying its own page
109/// cache). Any explicit value is honoured but floored at 1.
110pub 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    /// Build the SQLite pragma tuning from this config, keeping the fixed
123    /// `journal_size_limit` default.
124    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        // Resolve the auth token from --auth-token, then --auth-token-file, then
142        // the MCP_MEMORY_AUTH_TOKEN env var. A configured-but-empty token file
143        // is a hard error: fail closed rather than silently disabling auth.
144        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        // TLS cert/key for the `http` transport, from CLI flags or env vars.
174        // Both must be supplied together; one without the other is a hard error.
175        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        // A huge explicit value is honoured (only the auto path is clamped).
257        assert_eq!(resolve_read_pool_size(100), 100);
258    }
259}
260