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