use std::collections::HashMap;
use crate::cli::mapping::sanitize_cli_name;
use crate::Error;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ServerConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct MCPConfig {
servers: HashMap<String, ServerConfig>,
}
impl MCPConfig {
pub fn from_json(json: &str) -> Result<Self, Error> {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawConfig {
mcp_servers: HashMap<String, ServerConfig>,
}
let raw: RawConfig = serde_json::from_str(json)?;
Ok(Self { servers: raw.mcp_servers })
}
pub fn server_names(&self) -> Vec<String> {
let mut names = self.servers.keys().cloned().collect::<Vec<_>>();
names.sort();
names
}
pub fn server(&self, name: &str) -> Option<&ServerConfig> {
self.servers.get(name)
}
pub fn cli_prefix(&self, server_name: &str) -> String {
sanitize_cli_name(server_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_server() {
let json = r#"{"mcpServers": {"my-server": {"command": "uvx", "args": ["my-server"]}}}"#;
let config = MCPConfig::from_json(json).unwrap();
assert_eq!(config.server_names(), vec!["my-server"]);
}
#[test]
fn single_server_command_and_args() {
let json = r#"{"mcpServers": {"s": {"command": "uvx", "args": ["mcp-fetch"]}}}"#;
let config = MCPConfig::from_json(json).unwrap();
let server = config.server("s").unwrap();
assert_eq!(server.command, "uvx");
assert_eq!(server.args, vec!["mcp-fetch"]);
}
#[test]
fn server_without_args_defaults_to_empty() {
let json = r#"{"mcpServers": {"s": {"command": "uvx"}}}"#;
let config = MCPConfig::from_json(json).unwrap();
let server = config.server("s").unwrap();
assert!(server.args.is_empty());
}
#[test]
fn server_env_vars_parsed() {
let json = r#"{
"mcpServers": {
"s": {
"command": "uvx",
"args": [],
"env": { "API_KEY": "secret", "DEBUG": "1" }
}
}
}"#;
let config = MCPConfig::from_json(json).unwrap();
let server = config.server("s").unwrap();
assert_eq!(server.env.get("API_KEY"), Some(&"secret".to_string()));
assert_eq!(server.env.get("DEBUG"), Some(&"1".to_string()));
}
#[test]
fn server_without_env_defaults_to_empty() {
let json = r#"{"mcpServers": {"s": {"command": "cmd"}}}"#;
let config = MCPConfig::from_json(json).unwrap();
let server = config.server("s").unwrap();
assert!(server.env.is_empty());
}
#[test]
fn parse_multi_server() {
let json = r#"{
"mcpServers": {
"server-a": {"command": "uvx", "args": ["a"]},
"server-b": {"command": "npx", "args": ["-y", "b"]}
}
}"#;
let config = MCPConfig::from_json(json).unwrap();
let names = config.server_names();
assert_eq!(names.len(), 2);
assert!(names.contains(&"server-a".to_string()));
assert!(names.contains(&"server-b".to_string()));
}
#[test]
fn server_names_sorted() {
let json = r#"{
"mcpServers": {
"zebra-server": {"command": "z"},
"alpha-server": {"command": "a"},
"mango-server": {"command": "m"}
}
}"#;
let config = MCPConfig::from_json(json).unwrap();
assert_eq!(config.server_names(), vec!["alpha-server", "mango-server", "zebra-server"]);
}
#[test]
fn empty_server_list() {
let json = r#"{"mcpServers": {}}"#;
let config = MCPConfig::from_json(json).unwrap();
assert_eq!(config.server_names(), Vec::<String>::new());
}
#[test]
fn invalid_json_is_error() {
assert!(MCPConfig::from_json("{invalid}").is_err());
}
#[test]
fn empty_input_is_error() {
assert!(MCPConfig::from_json("").is_err());
}
#[test]
fn missing_mcp_servers_key_is_error() {
assert!(MCPConfig::from_json(r#"{"servers": {}}"#).is_err());
}
#[test]
fn server_lookup_missing_name() {
let json = r#"{"mcpServers": {"s": {"command": "cmd"}}}"#;
let config = MCPConfig::from_json(json).unwrap();
assert!(config.server("nonexistent").is_none());
}
#[test]
fn cli_prefix_returns_sanitized_name() {
let json = r#"{"mcpServers": {"my-server": {"command": "cmd"}}}"#;
let config = MCPConfig::from_json(json).unwrap();
assert_eq!(config.cli_prefix("my-server"), "my-server");
}
#[test]
fn cli_prefix_sanitizes_name() {
let json = r#"{"mcpServers": {"My Server!": {"command": "cmd"}}}"#;
let config = MCPConfig::from_json(json).unwrap();
assert_eq!(config.cli_prefix("My Server!"), "my-server");
}
}