1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::BTreeMap;
10
11pub type McpServers = BTreeMap<String, McpServerConfig>;
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct McpServerConfig {
33 pub command: String,
35
36 #[serde(default)]
38 pub args: Vec<String>,
39
40 #[serde(default)]
42 pub env: BTreeMap<String, String>,
43
44 #[serde(default)]
46 pub cwd: Option<String>,
47
48 #[serde(flatten)]
51 pub extra: Value,
52}
53
54impl McpServerConfig {
55 pub fn new(command: impl Into<String>) -> Self {
69 Self {
70 command: command.into(),
71 args: Vec::new(),
72 env: BTreeMap::new(),
73 cwd: None,
74 extra: Value::Object(Default::default()),
75 }
76 }
77
78 pub fn with_args(command: impl Into<String>, args: Vec<String>) -> Self {
92 Self {
93 command: command.into(),
94 args,
95 env: BTreeMap::new(),
96 cwd: None,
97 extra: Value::Object(Default::default()),
98 }
99 }
100}
101
102pub(crate) fn mcp_servers_to_wire(servers: &McpServers) -> Vec<Value> {
107 servers
108 .iter()
109 .map(|(name, config)| {
110 let mut obj = serde_json::to_value(config).unwrap_or_default();
111 if let Some(map) = obj.as_object_mut() {
112 map.insert("name".to_string(), Value::String(name.clone()));
113 }
114 obj
115 })
116 .collect()
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_mcp_config_new() {
125 let cfg = McpServerConfig::new("uvx");
126
127 assert_eq!(cfg.command, "uvx");
128 assert!(cfg.args.is_empty(), "args should default to empty");
129 assert!(cfg.env.is_empty(), "env should default to empty");
130 assert!(cfg.cwd.is_none(), "cwd should default to None");
131 assert_eq!(cfg.extra, Value::Object(Default::default()));
133 }
134
135 #[test]
136 fn test_mcp_config_with_args() {
137 let args = vec![
138 "-y".to_string(),
139 "@modelcontextprotocol/server-filesystem".to_string(),
140 "/tmp".to_string(),
141 ];
142 let cfg = McpServerConfig::with_args("npx", args.clone());
143
144 assert_eq!(cfg.command, "npx");
145 assert_eq!(cfg.args, args);
146 assert!(cfg.env.is_empty());
147 assert!(cfg.cwd.is_none());
148 }
149
150 #[test]
151 fn test_mcp_config_serde_roundtrip() {
152 let mut env = BTreeMap::new();
153 env.insert("API_KEY".to_string(), "secret".to_string());
154
155 let original = McpServerConfig {
156 command: "python".to_string(),
157 args: vec!["-m".to_string(), "mcp_server".to_string()],
158 env,
159 cwd: Some("/workspace".to_string()),
160 extra: Value::Object(Default::default()),
161 };
162
163 let json = serde_json::to_string(&original).expect("serialize failed");
164 let decoded: McpServerConfig =
165 serde_json::from_str(&json).expect("deserialize failed");
166
167 assert_eq!(original, decoded);
168 }
169
170 #[test]
171 fn test_mcp_servers_to_wire_with_entries() {
172 let mut servers: McpServers = BTreeMap::new();
173
174 servers.insert(
175 "alpha".to_string(),
176 McpServerConfig::new("cmd-alpha"),
177 );
178 servers.insert(
179 "beta".to_string(),
180 McpServerConfig::with_args(
181 "cmd-beta",
182 vec!["--flag".to_string()],
183 ),
184 );
185
186 let wire = mcp_servers_to_wire(&servers);
187
188 assert_eq!(wire.len(), 2);
190
191 let alpha = &wire[0];
192 assert_eq!(alpha["name"], Value::String("alpha".to_string()));
193 assert_eq!(alpha["command"], Value::String("cmd-alpha".to_string()));
194
195 let beta = &wire[1];
196 assert_eq!(beta["name"], Value::String("beta".to_string()));
197 assert_eq!(beta["command"], Value::String("cmd-beta".to_string()));
198 assert_eq!(
199 beta["args"],
200 serde_json::json!(["--flag"]),
201 "args must survive wire conversion"
202 );
203 }
204
205 #[test]
206 fn test_mcp_servers_to_wire_empty() {
207 let servers: McpServers = BTreeMap::new();
208 let wire = mcp_servers_to_wire(&servers);
209 assert!(wire.is_empty(), "empty map must produce empty vec");
210 }
211}