foundry_mcp/core/installation/
json_config.rs1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct McpServerConfig {
11 pub command: String,
12 pub args: Vec<String>,
13 pub env: Option<HashMap<String, String>>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct McpConfig {
19 #[serde(rename = "mcpServers")]
20 pub mcp_servers: HashMap<String, McpServerConfig>,
21}
22
23pub fn create_server_config(binary_path: &str) -> McpServerConfig {
25 McpServerConfig {
26 command: binary_path.to_string(),
27 args: vec!["serve".to_string()],
28 env: Some(HashMap::from([(
29 "FOUNDRY_LOG_LEVEL".to_string(),
30 "info".to_string(),
31 )])),
32 }
33}
34
35pub fn create_cursor_server_config() -> McpServerConfig {
37 McpServerConfig {
38 command: "foundry".to_string(),
39 args: vec!["serve".to_string()],
40 env: Some(HashMap::from([(
41 "FOUNDRY_LOG_LEVEL".to_string(),
42 "info".to_string(),
43 )])),
44 }
45}
46
47pub fn read_config_file(config_path: &Path) -> Result<McpConfig> {
49 if !config_path.exists() {
50 return Ok(McpConfig {
52 mcp_servers: HashMap::new(),
53 });
54 }
55
56 let content = std::fs::read_to_string(config_path).context(format!(
57 "Failed to read config file: {}",
58 config_path.display()
59 ))?;
60
61 if content.trim().is_empty() {
62 return Ok(McpConfig {
63 mcp_servers: HashMap::new(),
64 });
65 }
66
67 let config: McpConfig = serde_json::from_str(&content).context(format!(
68 "Failed to parse config file: {}",
69 config_path.display()
70 ))?;
71
72 Ok(config)
73}
74
75pub fn write_config_file(config_path: &Path, config: &McpConfig) -> Result<()> {
77 if let Some(parent) = config_path.parent() {
79 std::fs::create_dir_all(parent).context(format!(
80 "Failed to create config directory: {}",
81 parent.display()
82 ))?;
83 }
84
85 let content =
86 serde_json::to_string_pretty(config).context("Failed to serialize config to JSON")?;
87
88 std::fs::write(config_path, content).context(format!(
89 "Failed to write config file: {}",
90 config_path.display()
91 ))?;
92
93 Ok(())
94}
95
96pub fn add_server_to_config(
98 mut config: McpConfig,
99 server_name: &str,
100 server_config: McpServerConfig,
101) -> McpConfig {
102 config
103 .mcp_servers
104 .insert(server_name.to_string(), server_config);
105 config
106}
107
108pub fn remove_server_from_config(mut config: McpConfig, server_name: &str) -> McpConfig {
110 config.mcp_servers.remove(server_name);
111 config
112}
113
114pub fn has_server_config(config: &McpConfig, server_name: &str) -> bool {
116 config.mcp_servers.contains_key(server_name)
117}
118
119pub fn get_server_config<'a>(
121 config: &'a McpConfig,
122 server_name: &str,
123) -> Option<&'a McpServerConfig> {
124 config.mcp_servers.get(server_name)
125}
126
127pub fn validate_config(config: &McpConfig) -> Result<()> {
129 for (server_name, server_config) in &config.mcp_servers {
130 if server_name.trim().is_empty() {
131 return Err(anyhow::anyhow!("Server name cannot be empty"));
132 }
133
134 if server_config.command.trim().is_empty() {
135 return Err(anyhow::anyhow!(
136 "Server '{}' has empty command",
137 server_name
138 ));
139 }
140
141 let command_path = Path::new(&server_config.command);
144 if command_path.is_absolute() && !command_path.exists() {
145 return Err(anyhow::anyhow!(
146 "Server '{}' command does not exist: {}",
147 server_name,
148 server_config.command
149 ));
150 }
151 }
152
153 Ok(())
154}
155
156pub fn format_config_for_display(config: &McpConfig) -> String {
158 if config.mcp_servers.is_empty() {
159 return "No MCP servers configured".to_string();
160 }
161
162 let mut output = format!("Configured MCP servers ({}):\n", config.mcp_servers.len());
163
164 for (name, server_config) in &config.mcp_servers {
165 output.push_str(&format!(
166 "• {}: {} {:?}\n",
167 name, server_config.command, server_config.args
168 ));
169 }
170
171 output
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use tempfile::TempDir;
178
179 #[test]
180 fn test_json_config_roundtrip() {
181 let temp_dir = TempDir::new().unwrap();
183 let config_path = temp_dir.path().join("test-mcp.json");
184
185 let mut config = McpConfig {
187 mcp_servers: std::collections::HashMap::new(),
188 };
189
190 let server_config = create_server_config("/usr/bin/foundry");
192 config = add_server_to_config(config, "foundry", server_config);
193
194 write_config_file(&config_path, &config).unwrap();
196
197 let read_config = read_config_file(&config_path).unwrap();
199 assert!(has_server_config(&read_config, "foundry"));
200
201 let new_server_config = create_server_config("/custom/bin/foundry");
203 let modified_config =
204 add_server_to_config(read_config, "custom-foundry", new_server_config);
205
206 write_config_file(&config_path, &modified_config).unwrap();
208
209 let final_config = read_config_file(&config_path).unwrap();
211 assert!(has_server_config(&final_config, "foundry"));
212 assert!(has_server_config(&final_config, "custom-foundry"));
213 assert_eq!(final_config.mcp_servers.len(), 2);
214
215 let cleaned_config = remove_server_from_config(final_config, "foundry");
217
218 write_config_file(&config_path, &cleaned_config).unwrap();
220 let final_read = read_config_file(&config_path).unwrap();
221 assert!(!has_server_config(&final_read, "foundry"));
222 assert!(has_server_config(&final_read, "custom-foundry"));
223 assert_eq!(final_read.mcp_servers.len(), 1);
224 }
225
226 #[test]
227 fn test_create_server_config() {
228 let config = create_server_config("/usr/bin/foundry");
229 assert_eq!(config.command, "/usr/bin/foundry");
230 assert_eq!(config.args, vec!["serve"]);
231 assert!(config.env.is_some());
232 }
233
234 #[test]
235 fn test_create_cursor_server_config() {
236 let config = create_cursor_server_config();
237 assert_eq!(config.command, "foundry");
238 assert_eq!(config.args, vec!["serve"]);
239 assert!(config.env.is_some());
240 let env = config.env.unwrap();
241 assert_eq!(env.get("FOUNDRY_LOG_LEVEL"), Some(&"info".to_string()));
242 }
243
244 #[test]
245 fn test_add_server_to_config() {
246 let mut config = McpConfig {
247 mcp_servers: HashMap::new(),
248 };
249
250 let server_config = create_server_config("/usr/bin/foundry");
251 config = add_server_to_config(config, "foundry", server_config.clone());
252
253 assert!(has_server_config(&config, "foundry"));
254 assert_eq!(config.mcp_servers.len(), 1);
255
256 let retrieved = get_server_config(&config, "foundry").unwrap();
257 assert_eq!(retrieved.command, server_config.command);
258 }
259
260 #[test]
261 fn test_remove_server_from_config() {
262 let mut config = McpConfig {
263 mcp_servers: HashMap::new(),
264 };
265
266 let server_config = create_server_config("/usr/bin/foundry");
267 config = add_server_to_config(config, "foundry", server_config);
268 assert!(has_server_config(&config, "foundry"));
269
270 config = remove_server_from_config(config, "foundry");
271 assert!(!has_server_config(&config, "foundry"));
272 assert_eq!(config.mcp_servers.len(), 0);
273 }
274
275 #[test]
276 fn test_read_write_config_file() {
277 let temp_dir = TempDir::new().unwrap();
278 let config_path = temp_dir.path().join("mcp.json");
279
280 let mut config = McpConfig {
282 mcp_servers: HashMap::new(),
283 };
284 let server_config = create_server_config("/usr/bin/foundry");
285 config = add_server_to_config(config, "foundry", server_config);
286
287 write_config_file(&config_path, &config).unwrap();
289 assert!(config_path.exists());
290
291 let read_config = read_config_file(&config_path).unwrap();
293 assert!(has_server_config(&read_config, "foundry"));
294 assert_eq!(read_config.mcp_servers.len(), 1);
295 }
296
297 #[test]
298 fn test_validate_config_valid() {
299 let temp_dir = TempDir::new().unwrap();
300 let binary_path = temp_dir.path().join("foundry");
301 std::fs::write(&binary_path, b"test").unwrap();
302
303 let mut config = McpConfig {
304 mcp_servers: HashMap::new(),
305 };
306 let server_config = create_server_config(&binary_path.to_string_lossy());
307 config = add_server_to_config(config, "foundry", server_config);
308
309 let result = validate_config(&config);
310 assert!(result.is_ok());
311 }
312
313 #[test]
314 fn test_validate_config_invalid_command() {
315 let mut config = McpConfig {
316 mcp_servers: HashMap::new(),
317 };
318 let server_config = create_server_config("/nonexistent/command");
319 config = add_server_to_config(config, "foundry", server_config);
320
321 let result = validate_config(&config);
322 assert!(result.is_err());
323 assert!(result.unwrap_err().to_string().contains("does not exist"));
324 }
325
326 #[test]
327 fn test_format_config_for_display() {
328 let mut config = McpConfig {
329 mcp_servers: HashMap::new(),
330 };
331
332 let display = format_config_for_display(&config);
334 assert!(display.contains("No MCP servers configured"));
335
336 let server_config = create_server_config("/usr/bin/foundry");
338 config = add_server_to_config(config, "foundry", server_config);
339
340 let display = format_config_for_display(&config);
341 assert!(display.contains("Configured MCP servers (1)"));
342 assert!(display.contains("foundry"));
343 }
344}