foundry_mcp/core/installation/
paths.rs

1//! Platform-specific path detection for MCP server configuration files
2
3use crate::core::installation::utils::{ensure_directory_exists, get_home_dir};
4use anyhow::{Context, Result};
5use std::env;
6use std::path::{Path, PathBuf};
7
8/// Get the configuration directory path for Claude Code
9///
10/// Claude Code stores user settings in ~/.claude/
11/// Can be overridden with CLAUDE_CONFIG_DIR environment variable for testing
12pub fn get_claude_code_config_dir() -> Result<PathBuf> {
13    if let Ok(test_dir) = env::var("CLAUDE_CONFIG_DIR") {
14        return Ok(PathBuf::from(test_dir));
15    }
16    let home = get_home_dir()?;
17    Ok(home.join(".claude"))
18}
19
20/// Get the MCP configuration file path for Claude Code
21///
22/// Claude Code uses ~/.claude.json for MCP server configurations.
23/// This returns the MCP config file at ~/.claude.json
24pub fn get_claude_code_mcp_config_path() -> Result<PathBuf> {
25    let home = get_home_dir()?;
26    Ok(home.join(".claude.json"))
27}
28
29/// Get the configuration directory path for Cursor
30///
31/// Cursor stores MCP configurations in ./.cursor/ (project-local directory)
32/// Can be overridden with CURSOR_CONFIG_DIR environment variable for testing
33pub fn get_cursor_config_dir() -> Result<PathBuf> {
34    if let Ok(test_dir) = env::var("CURSOR_CONFIG_DIR") {
35        return Ok(PathBuf::from(test_dir));
36    }
37    let current_dir = std::env::current_dir().context("Failed to get current working directory")?;
38    Ok(current_dir.join(".cursor"))
39}
40
41/// Get the MCP configuration file path for Cursor
42pub fn get_cursor_mcp_config_path() -> Result<PathBuf> {
43    let config_dir = get_cursor_config_dir()?;
44    ensure_directory_exists(&config_dir)?;
45    Ok(config_dir.join("mcp.json"))
46}
47
48/// Get all supported MCP configuration paths
49///
50/// Returns the configuration file paths for both Claude Code and Cursor.
51/// Claude Code uses ~/.claude.json for MCP server configurations.
52pub fn get_all_config_paths() -> Vec<(String, PathBuf)> {
53    vec![
54        (
55            "claude-code".to_string(),
56            get_claude_code_mcp_config_path().unwrap_or_default(),
57        ),
58        (
59            "cursor".to_string(),
60            get_cursor_mcp_config_path().unwrap_or_default(),
61        ),
62    ]
63}
64
65/// Validate that a configuration directory is writable
66pub fn validate_config_dir_writable(config_path: &Path) -> Result<()> {
67    let parent_dir = config_path
68        .parent()
69        .context("Configuration path has no parent directory")?;
70
71    // Try to create the parent directory if it doesn't exist
72    ensure_directory_exists(&parent_dir.to_path_buf())?;
73
74    // Test if we can create a temporary file to check write permissions
75    let temp_file = parent_dir.join(".foundry_test_write");
76    match std::fs::write(&temp_file, b"test") {
77        Ok(_) => {
78            // Clean up the test file
79            let _ = std::fs::remove_file(&temp_file);
80            Ok(())
81        }
82        Err(e) => Err(anyhow::anyhow!(
83            "Configuration directory is not writable: {}. Error: {}",
84            parent_dir.display(),
85            e
86        )),
87    }
88}
89
90/// Get platform-specific information for display
91pub fn get_platform_info() -> String {
92    format!(
93        "{} {} ({})",
94        env::consts::OS,
95        env::consts::ARCH,
96        env!("CARGO_PKG_VERSION")
97    )
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_get_claude_code_config_dir() {
106        let result = get_claude_code_config_dir();
107        assert!(
108            result.is_ok(),
109            "Should be able to get Claude Code config dir"
110        );
111        let path = result.unwrap();
112        assert!(path.ends_with(".claude"));
113    }
114
115    #[test]
116    fn test_get_claude_code_mcp_config_path() {
117        let result = get_claude_code_mcp_config_path();
118        assert!(
119            result.is_ok(),
120            "Should be able to get Claude Code MCP config path"
121        );
122        let path = result.unwrap();
123        assert!(path.ends_with(".claude.json"));
124        assert!(path.to_string_lossy().contains(".claude"));
125    }
126
127    #[test]
128    fn test_get_cursor_config_dir() {
129        let result = get_cursor_config_dir();
130        assert!(result.is_ok(), "Should be able to get Cursor config dir");
131        let path = result.unwrap();
132        assert!(
133            path.ends_with(".cursor"),
134            "Path should end with '.cursor', got: {}",
135            path.display()
136        );
137        assert!(path.is_absolute(), "Path should be absolute");
138    }
139
140    #[test]
141    fn test_get_cursor_mcp_config_path() {
142        let result = get_cursor_mcp_config_path();
143        assert!(
144            result.is_ok(),
145            "Should be able to get Cursor MCP config path"
146        );
147        let path = result.unwrap();
148        assert!(path.ends_with("mcp.json"));
149    }
150
151    #[test]
152    fn test_get_all_config_paths() {
153        let paths = get_all_config_paths();
154        assert_eq!(paths.len(), 2, "Should return paths for both environments");
155
156        let environment_names: Vec<&String> = paths.iter().map(|(name, _)| name).collect();
157        assert!(environment_names.contains(&&"claude-code".to_string()));
158        assert!(environment_names.contains(&&"cursor".to_string()));
159    }
160
161    #[test]
162    fn test_get_platform_info() {
163        let info = get_platform_info();
164        assert!(!info.is_empty(), "Platform info should not be empty");
165        assert!(
166            info.contains(env::consts::OS),
167            "Platform info should contain OS"
168        );
169        assert!(
170            info.contains(env::consts::ARCH),
171            "Platform info should contain architecture"
172        );
173    }
174}