use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"debug" => Ok(LogLevel::Debug),
"info" => Ok(LogLevel::Info),
"warn" => Ok(LogLevel::Warn),
"error" => Ok(LogLevel::Error),
_ => Err(format!("Invalid log level: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub host: String,
pub port: u16,
pub debug: bool,
pub log_level: LogLevel,
pub client_init_timeout: Option<u64>,
pub secrets: HashMap<String, String>,
pub include_tags: Vec<String>,
pub exclude_tags: Vec<String>,
}
impl Default for Settings {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3000,
debug: false,
log_level: LogLevel::Info,
client_init_timeout: None,
secrets: HashMap::new(),
include_tags: Vec::new(),
exclude_tags: Vec::new(),
}
}
}
impl Settings {
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Self {
dotenvy::dotenv().ok();
let mut settings = Self::default();
if let Ok(val) = env::var("FASTMCP_HOST") {
settings.host = val;
}
if let Ok(val) = env::var("FASTMCP_PORT")
&& let Ok(port) = val.parse()
{
settings.port = port;
}
if let Ok(val) = env::var("FASTMCP_DEBUG") {
settings.debug = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("FASTMCP_LOG_LEVEL")
&& let Ok(level) = LogLevel::from_str(&val)
{
settings.log_level = level;
}
if let Ok(val) = env::var("FASTMCP_CLIENT_INIT_TIMEOUT")
&& let Ok(timeout) = val.parse()
{
settings.client_init_timeout = Some(timeout);
}
if let Ok(val) = env::var("FASTMCP_INCLUDE_TAGS") {
settings.include_tags = val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
if let Ok(val) = env::var("FASTMCP_EXCLUDE_TAGS") {
settings.exclude_tags = val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
let secrets_dir =
env::var("FASTMCP_SECRETS_DIR").unwrap_or_else(|_| "/run/secrets".to_string());
let secrets_path = Path::new(&secrets_dir);
if secrets_path.exists()
&& secrets_path.is_dir()
&& let Ok(entries) = fs::read_dir(secrets_path)
{
for entry in entries.flatten() {
if let Ok(path) = entry.path().canonicalize()
&& path.is_file()
&& let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& let Ok(content) = fs::read_to_string(&path)
{
settings
.secrets
.insert(file_name.to_string(), content.trim().to_string());
}
}
}
settings
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn test_load_defaults() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
env::remove_var("FASTMCP_HOST");
env::remove_var("FASTMCP_PORT");
env::remove_var("FASTMCP_DEBUG");
env::remove_var("FASTMCP_LOG_LEVEL");
env::remove_var("FASTMCP_CLIENT_INIT_TIMEOUT");
}
let settings = Settings::load();
assert_eq!(settings.host, "127.0.0.1");
assert_eq!(settings.port, 3000);
assert!(!settings.debug);
}
#[test]
fn test_load_from_env() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
env::set_var("FASTMCP_HOST", "0.0.0.0");
env::set_var("FASTMCP_PORT", "8080");
env::set_var("FASTMCP_DEBUG", "true");
env::set_var("FASTMCP_LOG_LEVEL", "debug");
}
let settings = Settings::load();
assert_eq!(settings.host, "0.0.0.0");
assert_eq!(settings.port, 8080);
assert!(settings.debug);
assert!(matches!(settings.log_level, LogLevel::Debug));
unsafe {
env::remove_var("FASTMCP_HOST");
env::remove_var("FASTMCP_PORT");
env::remove_var("FASTMCP_DEBUG");
env::remove_var("FASTMCP_LOG_LEVEL");
}
}
#[test]
fn test_load_secrets() {
use std::io::Write;
let _lock = ENV_LOCK.lock().unwrap();
let temp_dir = std::env::temp_dir().join("fastmcp_secrets_test");
fs::create_dir_all(&temp_dir).unwrap();
let secret_file = temp_dir.join("api_key");
let mut f = fs::File::create(&secret_file).unwrap();
f.write_all(b"super_secret_value").unwrap();
unsafe {
env::set_var("FASTMCP_SECRETS_DIR", temp_dir.to_str().unwrap());
}
let settings = Settings::load();
assert_eq!(
settings.secrets.get("api_key").map(|s| s.as_str()),
Some("super_secret_value")
);
unsafe {
env::remove_var("FASTMCP_SECRETS_DIR");
}
fs::remove_dir_all(temp_dir).unwrap();
}
#[test]
fn test_log_level_invalid_parse() {
let result = LogLevel::from_str("garbage");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid log level"));
}
#[test]
fn test_log_level_valid_parse() {
assert!(matches!(LogLevel::from_str("debug"), Ok(LogLevel::Debug)));
assert!(matches!(LogLevel::from_str("INFO"), Ok(LogLevel::Info)));
assert!(matches!(LogLevel::from_str("Warn"), Ok(LogLevel::Warn)));
assert!(matches!(LogLevel::from_str("ERROR"), Ok(LogLevel::Error)));
}
#[test]
fn test_load_tags_from_env() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
env::set_var("FASTMCP_INCLUDE_TAGS", "alpha, beta, gamma");
env::set_var("FASTMCP_EXCLUDE_TAGS", "internal");
}
let settings = Settings::load();
assert_eq!(settings.include_tags, vec!["alpha", "beta", "gamma"]);
assert_eq!(settings.exclude_tags, vec!["internal"]);
unsafe {
env::remove_var("FASTMCP_INCLUDE_TAGS");
env::remove_var("FASTMCP_EXCLUDE_TAGS");
}
}
}