use std::path::{Path, PathBuf};
use serde_json::{Map, Value};
use super::config_schema::Config;
use super::errors::McpifyError;
const ENV_PREFIX: &str = "GITHUB_MCP";
const CONFIG_DIR_NAME: &str = ".github-mcp";
const LOCAL_CONFIG_FILE: &str = "github-mcp.config.yml";
fn home_dir() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
}
fn read_yaml_if_exists(path: &Path) -> Map<String, Value> {
let Ok(contents) = std::fs::read_to_string(path) else {
return Map::new();
};
match serde_yaml::from_str::<Value>(&contents) {
Ok(Value::Object(map)) => map,
_ => Map::new(),
}
}
fn env_overrides() -> Map<String, Value> {
let mut overrides = Map::new();
for (config_key, env_suffix) in [
("url", "URL"),
("auth_method", "AUTH_METHOD"),
("api_version", "API_VERSION"),
("log_level", "LOG_LEVEL"),
("transport", "TRANSPORT"),
("host", "HOST"),
("cors_allow", "CORS_ALLOW"),
("rate_limit", "RATE_LIMIT"),
("timeout_ms", "TIMEOUT_MS"),
("cache_size", "CACHE_SIZE"),
("retry_attempts", "RETRY_ATTEMPTS"),
("port", "PORT"),
] {
if let Ok(value) = std::env::var(format!("{ENV_PREFIX}_{env_suffix}")) {
overrides.insert(config_key.to_string(), Value::String(value));
}
}
overrides
}
pub fn load_config(cli_flags: Map<String, Value>) -> Result<Config, McpifyError> {
let install_dir = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(Path::to_path_buf))
.unwrap_or_else(|| PathBuf::from("."));
let layers = [
cli_flags,
env_overrides(),
read_yaml_if_exists(&PathBuf::from(LOCAL_CONFIG_FILE)),
read_yaml_if_exists(&home_dir().join(CONFIG_DIR_NAME).join("config.yml")),
read_yaml_if_exists(&PathBuf::from("/etc/github-mcp/config.yml")),
read_yaml_if_exists(&install_dir.join("config.yml")),
];
let mut merged = Map::new();
for layer in layers.into_iter().rev() {
merged.extend(layer);
}
serde_json::from_value(Value::Object(merged))
.map_err(|err| McpifyError::Configuration(format!("invalid configuration: {err}")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn base_flags() -> Map<String, Value> {
json!({ "url": "https://api.example.com", "auth_method": "pat" })
.as_object()
.unwrap()
.clone()
}
#[test]
fn applies_built_in_defaults_when_nothing_else_is_set() {
let config = load_config(base_flags()).unwrap();
assert_eq!(config.log_level, "info");
assert_eq!(config.rate_limit, 100);
assert_eq!(config.port, 3000);
}
#[test]
fn cli_flags_win_over_everything_else() {
let mut flags = base_flags();
flags.insert("log_level".to_string(), json!("debug"));
let config = load_config(flags).unwrap();
assert_eq!(config.log_level, "debug");
}
#[test]
fn missing_required_fields_report_a_configuration_error() {
let err = load_config(Map::new()).unwrap_err();
assert_eq!(err.code(), "CONFIGURATION_ERROR");
}
}