use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub transport: Option<mcp_probe_core::transport::TransportConfig>,
pub client: ClientConfig,
pub debug: DebugConfig,
pub logging: LoggingConfig,
pub tui: TuiConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
pub name: String,
pub version: String,
pub metadata: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebugConfig {
pub show_raw_messages: bool,
pub auto_save_sessions: bool,
pub session_directory: PathBuf,
pub max_saved_sessions: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub file: Option<PathBuf>,
pub stderr: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuiConfig {
pub color_scheme: String,
pub key_bindings: std::collections::HashMap<String, String>,
pub refresh_rate_ms: u64,
pub show_help: bool,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
name: "mcp-probe".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
metadata: std::collections::HashMap::new(),
}
}
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
show_raw_messages: false,
auto_save_sessions: true,
session_directory: dirs::home_dir()
.unwrap_or_else(|| ".".into())
.join(".mcp-probe")
.join("sessions"),
max_saved_sessions: 100,
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: "pretty".to_string(),
file: None,
stderr: true,
}
}
}
impl Default for TuiConfig {
fn default() -> Self {
let mut key_bindings = std::collections::HashMap::new();
key_bindings.insert("quit".to_string(), "q".to_string());
key_bindings.insert("help".to_string(), "h".to_string());
key_bindings.insert("refresh".to_string(), "r".to_string());
Self {
color_scheme: "default".to_string(),
key_bindings,
refresh_rate_ms: 100,
show_help: true,
}
}
}
impl Config {
#[allow(dead_code)]
pub fn load_from_file(path: &std::path::Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::default());
}
let content = std::fs::read_to_string(path)?;
let config: Self = toml::from_str(&content)?;
Ok(config)
}
#[allow(dead_code)]
pub fn save_to_file(&self, path: &std::path::Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
#[allow(dead_code)]
pub fn merge(&mut self, other: &Config) {
if other.transport.is_some() {
self.transport = other.transport.clone();
}
if other.debug.show_raw_messages {
self.debug.show_raw_messages = true;
}
if other.logging.level != "info" {
self.logging.level = other.logging.level.clone();
}
if other.logging.format != "pretty" {
self.logging.format = other.logging.format.clone();
}
if other.logging.file.is_some() {
self.logging.file = other.logging.file.clone();
}
}
#[allow(dead_code)]
pub fn default_path() -> PathBuf {
use crate::paths::get_mcp_probe_paths;
match get_mcp_probe_paths() {
Ok(paths) => paths.default_config_file(),
Err(_) => {
dirs::config_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| ".".into()))
.join("mcp-probe")
.join("config.toml")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.client.name, "mcp-probe");
assert_eq!(config.logging.level, "info");
assert!(!config.debug.show_raw_messages);
}
#[test]
fn test_config_serialization() -> Result<()> {
let config = Config::default();
let toml_str = toml::to_string(&config)?;
let parsed: Config = toml::from_str(&toml_str)?;
assert_eq!(config.client.name, parsed.client.name);
assert_eq!(config.logging.level, parsed.logging.level);
Ok(())
}
#[test]
fn test_config_file_operations() -> Result<()> {
let config = Config::default();
let temp_file = NamedTempFile::new()?;
config.save_to_file(temp_file.path())?;
let loaded = Config::load_from_file(temp_file.path())?;
assert_eq!(config.client.name, loaded.client.name);
Ok(())
}
#[test]
fn test_config_merge() {
let mut base = Config::default();
let mut other = Config::default();
other.debug.show_raw_messages = true;
other.logging.level = "debug".to_string();
base.merge(&other);
assert!(base.debug.show_raw_messages);
assert_eq!(base.logging.level, "debug");
}
}