sqlserver_mcp_catalog/core/
config_manager.rs1use std::path::{Path, PathBuf};
4
5use serde_json::{Map, Value};
6
7use super::config_schema::Config;
8use super::credential_storage::resolve_home_dir;
9use super::errors::McpifyError;
10
11const ENV_PREFIX: &str = "SQLSERVER";
12const CONFIG_DIR_NAME: &str = ".sqlserver-mcp";
13const LOCAL_CONFIG_FILE: &str = "sqlserver-mcp.config.yml";
14
15fn home_dir() -> PathBuf {
20 resolve_home_dir()
21}
22
23fn read_yaml_if_exists(path: &Path) -> Map<String, Value> {
24 let Ok(contents) = std::fs::read_to_string(path) else {
25 return Map::new();
26 };
27 match serde_yaml::from_str::<Value>(&contents) {
28 Ok(Value::Object(map)) => map,
29 _ => Map::new(),
30 }
31}
32
33fn env_overrides() -> Map<String, Value> {
34 let mut overrides = Map::new();
35 for (config_key, env_suffix) in [
36 ("url", "URL"),
37 ("auth_method", "AUTH_METHOD"),
38 ("sql_port", "SQL_PORT"),
39 ("pool_max_size", "POOL_MAX_SIZE"),
40 ("trust_server_cert", "TRUST_SERVER_CERT"),
41 ("api_version", "API_VERSION"),
42 ("log_level", "LOG_LEVEL"),
43 ("transport", "TRANSPORT"),
44 ("host", "HOST"),
45 ("cors_allow", "CORS_ALLOW"),
46 ("rate_limit", "RATE_LIMIT"),
47 ("timeout_ms", "TIMEOUT_MS"),
48 ("cache_size", "CACHE_SIZE"),
49 ("retry_attempts", "RETRY_ATTEMPTS"),
50 ("port", "PORT"),
51 ] {
52 if let Ok(value) = std::env::var(format!("{ENV_PREFIX}_{env_suffix}")) {
53 overrides.insert(config_key.to_string(), Value::String(value));
54 }
55 }
56 overrides
57}
58
59pub fn load_config(cli_flags: Map<String, Value>) -> Result<Config, McpifyError> {
66 let install_dir = std::env::current_exe()
67 .ok()
68 .and_then(|exe| exe.parent().map(Path::to_path_buf))
69 .unwrap_or_else(|| PathBuf::from("."));
70
71 let layers = [
72 cli_flags,
73 env_overrides(),
74 read_yaml_if_exists(&PathBuf::from(LOCAL_CONFIG_FILE)),
75 read_yaml_if_exists(&home_dir().join(CONFIG_DIR_NAME).join("config.yml")),
76 read_yaml_if_exists(&PathBuf::from("/etc/sqlserver-mcp/config.yml")),
77 read_yaml_if_exists(&install_dir.join("config.yml")),
78 ];
79
80 let mut merged = Map::new();
84 for layer in layers.into_iter().rev() {
85 merged.extend(layer);
86 }
87
88 serde_json::from_value(Value::Object(merged))
89 .map_err(|err| McpifyError::Configuration(format!("invalid configuration: {err}")))
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use serde_json::json;
96
97 fn base_flags() -> Map<String, Value> {
98 json!({ "url": "localhost", "auth_method": "sql_server" })
99 .as_object()
100 .unwrap()
101 .clone()
102 }
103
104 #[test]
105 fn applies_built_in_defaults_when_nothing_else_is_set() {
106 let config = load_config(base_flags()).unwrap();
107 assert_eq!(config.log_level, "info");
108 assert_eq!(config.rate_limit, 100);
109 assert_eq!(config.port, 3000);
110 }
111
112 #[test]
113 fn cli_flags_win_over_everything_else() {
114 let mut flags = base_flags();
115 flags.insert("log_level".to_string(), json!("debug"));
116 let config = load_config(flags).unwrap();
117 assert_eq!(config.log_level, "debug");
118 }
119
120 #[test]
121 fn missing_required_fields_report_a_configuration_error() {
122 let err = load_config(Map::new()).unwrap_err();
123 assert_eq!(err.code(), "CONFIGURATION_ERROR");
124 }
125}