1use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::path::PathBuf;
24
25pub type McpServers = HashMap<String, McpServerConfig>;
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct McpServerConfig {
31 pub command: String,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
35 pub args: Vec<String>,
36 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
38 pub env: HashMap<String, String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub cwd: Option<PathBuf>,
42}
43
44impl McpServerConfig {
45 pub fn new(command: impl Into<String>) -> Self {
47 Self {
48 command: command.into(),
49 args: Vec::new(),
50 env: HashMap::new(),
51 cwd: None,
52 }
53 }
54
55 pub fn with_args<I, S>(mut self, args: I) -> Self
57 where
58 I: IntoIterator<Item = S>,
59 S: Into<String>,
60 {
61 self.args.extend(args.into_iter().map(Into::into));
62 self
63 }
64
65 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
67 self.env.insert(key.into(), value.into());
68 self
69 }
70
71 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
73 self.cwd = Some(cwd.into());
74 self
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn builder_pattern() {
84 let config = McpServerConfig::new("npx")
85 .with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
86 .with_env("DEBUG", "true")
87 .with_cwd("/home/user");
88
89 assert_eq!(config.command, "npx");
90 assert_eq!(config.args.len(), 3);
91 assert_eq!(config.env.get("DEBUG"), Some(&"true".into()));
92 assert_eq!(config.cwd, Some(PathBuf::from("/home/user")));
93 }
94
95 #[test]
96 fn json_round_trip() {
97 let config = McpServerConfig::new("node")
98 .with_args(["server.js"])
99 .with_env("PORT", "3000");
100
101 let json = serde_json::to_string(&config).unwrap();
102 let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
103 assert_eq!(config, parsed);
104 }
105
106 #[test]
107 fn json_round_trip_minimal() {
108 let config = McpServerConfig::new("sqlite-mcp");
109 let json = serde_json::to_string(&config).unwrap();
110 let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
111 assert_eq!(config, parsed);
112 assert!(!json.contains("args"));
114 assert!(!json.contains("env"));
115 assert!(!json.contains("cwd"));
116 }
117
118 #[test]
119 fn servers_map() {
120 let mut servers = McpServers::new();
121 servers.insert(
122 "fs".into(),
123 McpServerConfig::new("npx").with_args(["-y", "fs-server"]),
124 );
125 servers.insert("db".into(), McpServerConfig::new("sqlite-mcp"));
126
127 assert_eq!(servers.len(), 2);
128 assert_eq!(servers["fs"].command, "npx");
129 assert_eq!(servers["db"].command, "sqlite-mcp");
130
131 let json = serde_json::to_string(&servers).unwrap();
133 let parsed: McpServers = serde_json::from_str(&json).unwrap();
134 assert_eq!(servers, parsed);
135 }
136}