mcpr_integrations/store/config.rs
1//! Store configuration — TOML section and validation.
2//!
3//! Maps to the `[store]` section in `mcpr.toml`. Implements [`ModuleConfig`]
4//! so the store owns its own defaults and validation logic.
5
6use mcpr_core::config::{ConfigIssue, ModuleConfig, Severity};
7
8/// `[store]` section in `mcpr.toml`.
9///
10/// Controls the SQLite-based request storage engine. All fields are optional —
11/// sensible defaults are applied based on the platform.
12///
13/// ```toml
14/// [store]
15/// # Enable or disable request storage. Default: true.
16/// # Set to false to run mcpr as a pure proxy with no local persistence.
17/// enabled = true
18///
19/// # Override the database file path. Default: platform-specific.
20/// # Linux: ~/.local/share/mcpr/mcpr.db
21/// # macOS: ~/Library/Application Support/mcpr/mcpr.db
22/// # Windows: %APPDATA%\mcpr\mcpr.db
23/// path = "/var/lib/mcpr/requests.db"
24///
25/// # Proxy name tag written to every row. Default: derived from upstream URL.
26/// # Use this when you run multiple proxies sharing one database file,
27/// # or when you want a human-friendly name in `mcpr proxy logs <name>`.
28/// name = "api-server"
29/// ```
30#[derive(serde::Deserialize, Default, Debug, Clone)]
31#[serde(default)]
32pub struct FileStoreConfig {
33 /// Whether request storage is enabled.
34 ///
35 /// When false, no database is opened and no events are recorded.
36 /// CLI query commands (`mcpr proxy logs`, etc.) will report that
37 /// storage is disabled.
38 ///
39 /// Default: `true` — storage is on by default because observability
40 /// is the primary value proposition of mcpr beyond basic proxying.
41 pub enabled: Option<bool>,
42
43 /// Override the database file path.
44 ///
45 /// When set, the store uses this exact path instead of the platform
46 /// default. Useful for:
47 /// - Placing the DB on a specific disk or partition.
48 /// - Docker/container deployments where the data dir is mounted.
49 /// - Running integration tests with an isolated database.
50 ///
51 /// The parent directory is created automatically if it doesn't exist.
52 /// Default: platform-specific (see [`super::path::resolve_db_path`]).
53 pub path: Option<String>,
54
55 /// Human-readable proxy name, written to every request and session row.
56 ///
57 /// This is how `mcpr proxy logs <name>` identifies which proxy's data
58 /// to query. When multiple proxies share a database, each needs a
59 /// unique name.
60 ///
61 /// Default: derived from the upstream MCP URL hostname (e.g.,
62 /// "localhost-9000" for `http://localhost:9000/mcp`). Set this
63 /// explicitly when the derived name isn't descriptive enough.
64 pub name: Option<String>,
65}
66
67impl FileStoreConfig {
68 /// Whether storage is enabled. Defaults to true.
69 pub fn is_enabled(&self) -> bool {
70 self.enabled.unwrap_or(true)
71 }
72}
73
74impl ModuleConfig for FileStoreConfig {
75 fn name(&self) -> &'static str {
76 "store"
77 }
78
79 fn validate(&self) -> Vec<ConfigIssue> {
80 let mut issues = Vec::new();
81
82 // Path must not be empty if explicitly set.
83 if let Some(ref p) = self.path
84 && p.trim().is_empty()
85 {
86 issues.push(ConfigIssue {
87 severity: Severity::Error,
88 module: "store",
89 message: "store.path cannot be an empty string — remove the key to use the platform default, or set a valid path".into(),
90 });
91 }
92
93 // Name must not be empty if explicitly set.
94 if let Some(ref n) = self.name
95 && n.trim().is_empty()
96 {
97 issues.push(ConfigIssue {
98 severity: Severity::Error,
99 module: "store",
100 message: "store.name cannot be an empty string — remove the key to auto-derive from the upstream URL, or set a valid name".into(),
101 });
102 }
103
104 issues
105 }
106}
107
108#[cfg(test)]
109#[allow(non_snake_case)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn file_store_config__default_is_valid() {
115 let config = FileStoreConfig::default();
116 assert!(config.is_enabled());
117 assert!(config.validate().is_empty());
118 }
119
120 #[test]
121 fn file_store_config__disabled_is_valid() {
122 let config = FileStoreConfig {
123 enabled: Some(false),
124 path: None,
125 name: None,
126 };
127 assert!(!config.is_enabled());
128 assert!(config.validate().is_empty());
129 }
130
131 #[test]
132 fn file_store_config__empty_path_is_error() {
133 let config = FileStoreConfig {
134 enabled: None,
135 path: Some("".into()),
136 name: None,
137 };
138 let issues = config.validate();
139 assert_eq!(issues.len(), 1);
140 assert_eq!(issues[0].severity, Severity::Error);
141 assert!(issues[0].message.contains("store.path"));
142 }
143
144 #[test]
145 fn file_store_config__empty_name_is_error() {
146 let config = FileStoreConfig {
147 enabled: None,
148 path: None,
149 name: Some(" ".into()),
150 };
151 let issues = config.validate();
152 assert_eq!(issues.len(), 1);
153 assert!(issues[0].message.contains("store.name"));
154 }
155
156 #[test]
157 fn file_store_config__parses_from_toml() {
158 let toml_str = r#"
159 enabled = false
160 path = "/tmp/mcpr.db"
161 name = "my-proxy"
162 "#;
163 let config: FileStoreConfig = toml::from_str(toml_str).unwrap();
164 assert_eq!(config.enabled, Some(false));
165 assert_eq!(config.path.as_deref(), Some("/tmp/mcpr.db"));
166 assert_eq!(config.name.as_deref(), Some("my-proxy"));
167 }
168}