use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub type McpServers = HashMap<String, McpServerConfig>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct McpServerConfig {
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub env: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathBuf>,
}
impl McpServerConfig {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
args: Vec::new(),
env: HashMap::new(),
cwd: None,
}
}
pub fn with_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args.extend(args.into_iter().map(Into::into));
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = Some(cwd.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_pattern() {
let config = McpServerConfig::new("npx")
.with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
.with_env("DEBUG", "true")
.with_cwd("/home/user");
assert_eq!(config.command, "npx");
assert_eq!(config.args.len(), 3);
assert_eq!(config.env.get("DEBUG"), Some(&"true".into()));
assert_eq!(config.cwd, Some(PathBuf::from("/home/user")));
}
#[test]
fn json_round_trip() {
let config = McpServerConfig::new("node")
.with_args(["server.js"])
.with_env("PORT", "3000");
let json = serde_json::to_string(&config).unwrap();
let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, parsed);
}
#[test]
fn json_round_trip_minimal() {
let config = McpServerConfig::new("sqlite-mcp");
let json = serde_json::to_string(&config).unwrap();
let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, parsed);
assert!(!json.contains("args"));
assert!(!json.contains("env"));
assert!(!json.contains("cwd"));
}
#[test]
fn servers_map() {
let mut servers = McpServers::new();
servers.insert(
"fs".into(),
McpServerConfig::new("npx").with_args(["-y", "fs-server"]),
);
servers.insert("db".into(), McpServerConfig::new("sqlite-mcp"));
assert_eq!(servers.len(), 2);
assert_eq!(servers["fs"].command, "npx");
assert_eq!(servers["db"].command, "sqlite-mcp");
let json = serde_json::to_string(&servers).unwrap();
let parsed: McpServers = serde_json::from_str(&json).unwrap();
assert_eq!(servers, parsed);
}
}