foundry_mcp/core/installation/
json_config.rs

1//! JSON configuration file management utilities
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7
8/// MCP server configuration entry
9#[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/// MCP configuration file structure
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct McpConfig {
19    #[serde(rename = "mcpServers")]
20    pub mcp_servers: HashMap<String, McpServerConfig>,
21}
22
23/// Create a new MCP server configuration entry
24pub 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
35/// Create a new MCP server configuration entry for Cursor using PATH-based command
36pub 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
47/// Read MCP configuration from a JSON file
48pub fn read_config_file(config_path: &Path) -> Result<McpConfig> {
49    if !config_path.exists() {
50        // Return empty config if file doesn't exist
51        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
75/// Write MCP configuration to a JSON file
76pub fn write_config_file(config_path: &Path, config: &McpConfig) -> Result<()> {
77    // Ensure parent directory exists
78    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
96/// Add or update a server in the MCP configuration
97pub 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
108/// Remove a server from the MCP configuration
109pub fn remove_server_from_config(mut config: McpConfig, server_name: &str) -> McpConfig {
110    config.mcp_servers.remove(server_name);
111    config
112}
113
114/// Check if a server is already configured
115pub fn has_server_config(config: &McpConfig, server_name: &str) -> bool {
116    config.mcp_servers.contains_key(server_name)
117}
118
119/// Get server configuration if it exists
120pub 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
127/// Validate MCP configuration
128pub 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        // Check if command path exists (only for absolute paths)
142        // Commands like "foundry" are meant to be found in PATH, so skip validation
143        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
156/// Format configuration for display
157pub 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        // Test that we can create, write, read, and modify JSON configuration
182        let temp_dir = TempDir::new().unwrap();
183        let config_path = temp_dir.path().join("test-mcp.json");
184
185        // Start with empty config
186        let mut config = McpConfig {
187            mcp_servers: std::collections::HashMap::new(),
188        };
189
190        // Add a server
191        let server_config = create_server_config("/usr/bin/foundry");
192        config = add_server_to_config(config, "foundry", server_config);
193
194        // Write to file
195        write_config_file(&config_path, &config).unwrap();
196
197        // Read back
198        let read_config = read_config_file(&config_path).unwrap();
199        assert!(has_server_config(&read_config, "foundry"));
200
201        // Modify config
202        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 modified config
207        write_config_file(&config_path, &modified_config).unwrap();
208
209        // Read and verify
210        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        // Remove a server
216        let cleaned_config = remove_server_from_config(final_config, "foundry");
217
218        // Write and verify removal
219        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        // Create test config
281        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
288        write_config_file(&config_path, &config).unwrap();
289        assert!(config_path.exists());
290
291        // Read config back
292        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        // Empty config
333        let display = format_config_for_display(&config);
334        assert!(display.contains("No MCP servers configured"));
335
336        // Config with server
337        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}