use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub disabled: bool,
#[serde(default)]
pub auto_approve: Vec<String>,
#[serde(default)]
pub restart_policy: Option<RestartPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RestartPolicy {
#[serde(default = "default_initial_delay")]
pub initial_delay_ms: u64,
#[serde(default = "default_max_delay")]
pub max_delay_ms: u64,
#[serde(default = "default_backoff_multiplier")]
pub backoff_multiplier: f64,
#[serde(default = "default_max_restart_attempts")]
pub max_restart_attempts: u32,
}
fn default_initial_delay() -> u64 {
1000
}
fn default_max_delay() -> u64 {
30000
}
fn default_backoff_multiplier() -> f64 {
2.0
}
fn default_max_restart_attempts() -> u32 {
10
}
impl Default for RestartPolicy {
fn default() -> Self {
Self {
initial_delay_ms: default_initial_delay(),
max_delay_ms: default_max_delay(),
backoff_multiplier: default_backoff_multiplier(),
max_restart_attempts: default_max_restart_attempts(),
}
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)] pub(crate) struct McpJsonFile {
pub mcp_servers: HashMap<String, McpServerConfig>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_restart_policy() {
let policy = RestartPolicy::default();
assert_eq!(policy.initial_delay_ms, 1000);
assert_eq!(policy.max_delay_ms, 30000);
assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
assert_eq!(policy.max_restart_attempts, 10);
}
#[test]
fn test_mcp_server_config_defaults() {
let json = r#"{"command": "echo"}"#;
let config: McpServerConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.command, "echo");
assert!(config.args.is_empty());
assert!(config.env.is_empty());
assert!(!config.disabled);
assert!(config.auto_approve.is_empty());
assert!(config.restart_policy.is_none());
}
#[test]
fn test_mcp_json_file_parsing() {
let json = r#"{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
"env": {},
"disabled": false,
"autoApprove": ["read_file", "list_directory"]
}
}
}"#;
let file: McpJsonFile = serde_json::from_str(json).unwrap();
assert_eq!(file.mcp_servers.len(), 1);
let fs_config = &file.mcp_servers["filesystem"];
assert_eq!(fs_config.command, "npx");
assert_eq!(fs_config.auto_approve, vec!["read_file", "list_directory"]);
}
#[test]
fn test_restart_policy_serde_defaults() {
let json = r#"{}"#;
let policy: RestartPolicy = serde_json::from_str(json).unwrap();
assert_eq!(policy.initial_delay_ms, 1000);
assert_eq!(policy.max_delay_ms, 30000);
assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
assert_eq!(policy.max_restart_attempts, 10);
}
#[test]
fn test_config_round_trip() {
let config = McpServerConfig {
command: "npx".to_string(),
args: vec!["-y".to_string(), "server".to_string()],
env: HashMap::from([("KEY".to_string(), "value".to_string())]),
disabled: true,
auto_approve: vec!["tool1".to_string()],
restart_policy: Some(RestartPolicy {
initial_delay_ms: 500,
max_delay_ms: 10000,
backoff_multiplier: 1.5,
max_restart_attempts: 3,
}),
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: McpServerConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}
}