Skip to main content

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}