github_mcp/core/
config_manager.rs1use std::path::{Path, PathBuf};
4
5use serde_json::{Map, Value};
6
7use super::config_schema::Config;
8use super::errors::McpifyError;
9
10const ENV_PREFIX: &str = "GITHUB_MCP";
11const CONFIG_DIR_NAME: &str = ".github-mcp";
12const LOCAL_CONFIG_FILE: &str = "github-mcp.config.yml";
13
14fn resolve_home_dir() -> PathBuf {
20 std::env::var_os("HOME")
21 .or_else(|| std::env::var_os("USERPROFILE"))
22 .map(PathBuf::from)
23 .unwrap_or_else(|| PathBuf::from("."))
24}
25
26fn read_yaml_if_exists(path: &Path) -> Map<String, Value> {
27 let Ok(contents) = std::fs::read_to_string(path) else {
28 return Map::new();
29 };
30 match serde_yaml::from_str::<Value>(&contents) {
31 Ok(Value::Object(map)) => map,
32 _ => Map::new(),
33 }
34}
35
36fn env_overrides() -> Map<String, Value> {
37 let mut overrides = Map::new();
38 for (config_key, env_suffix) in [
39 ("url", "URL"),
40 ("auth_method", "AUTH_METHOD"),
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(&resolve_home_dir().join(CONFIG_DIR_NAME).join("config.yml")),
76 read_yaml_if_exists(&PathBuf::from("/etc/github-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": "https://api.example.com", "auth_method": "pat" })
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}