Skip to main content

mcp_execution_cli/commands/
setup.rs

1//! Setup command implementation.
2//!
3//! Validates the runtime environment for MCP tool execution:
4//! - Checks Node.js 18+ is installed
5//! - Verifies generated files are executable
6//! - Provides helpful error messages and suggestions
7
8use anyhow::{Context, Result};
9use mcp_execution_core::cli::ExitCode;
10use std::path::PathBuf;
11use std::process::Stdio;
12use tokio::process::Command;
13
14/// Runs the setup command.
15///
16/// Validates that the runtime environment is ready for MCP tool execution.
17///
18/// # Checks Performed
19///
20/// 1. **Node.js version**: Ensures Node.js 18.0.0 or higher is installed
21/// 2. **File permissions**: Makes TypeScript files executable (Unix only)
22/// 3. **Configuration**: Checks if ~/.claude/mcp.json exists
23///
24/// # Examples
25///
26/// ```bash
27/// # Run setup validation
28/// mcp-execution-cli setup
29///
30/// # Output:
31/// # ✓ Node.js v20.10.0 detected
32/// # ✓ Runtime setup complete
33/// # Claude Code can now execute MCP tools via:
34/// #   node ~/.claude/servers/<server>/<tool>.ts '{"param":"value"}'
35/// ```
36///
37/// # Errors
38///
39/// Returns an error if:
40/// - Node.js is not installed
41/// - Node.js version is less than 18.0.0
42/// - Home directory cannot be determined
43pub async fn run() -> Result<ExitCode> {
44    println!("Checking runtime environment...\n");
45
46    // Check Node.js installation
47    check_node_version().await?;
48
49    // Check for MCP configuration
50    check_mcp_config()?;
51
52    // Make files executable (Unix only)
53    #[cfg(unix)]
54    make_files_executable().await?;
55
56    println!("\n✓ Runtime setup complete");
57    println!("  Claude Code can now execute MCP tools via:");
58    println!("  node ~/.claude/servers/<server>/<tool>.ts '{{\"param\":\"value\"}}'");
59    println!("\nNext steps:");
60    println!("  1. Generate tools: mcp-execution-cli generate <server>");
61    println!("  2. Configure servers in ~/.claude/mcp.json");
62    println!("  3. Execute tools autonomously via Node.js");
63
64    Ok(ExitCode::SUCCESS)
65}
66
67/// Checks Node.js version requirement.
68///
69/// Verifies that Node.js 18.0.0 or higher is installed and accessible.
70///
71/// # Errors
72///
73/// Returns error if:
74/// - Node.js command not found in PATH
75/// - Node.js version cannot be determined
76/// - Node.js version is less than 18.0.0
77async fn check_node_version() -> Result<()> {
78    // Check if node command exists
79    let output = Command::new("node")
80        .arg("--version")
81        .stdout(Stdio::piped())
82        .stderr(Stdio::piped())
83        .output()
84        .await
85        .context(
86            "Node.js not found in PATH.\n\
87             \n\
88             Node.js 18+ is required for MCP tool execution.\n\
89             Install from: https://nodejs.org\n\
90             \n\
91             Or use a version manager:\n\
92             - nvm: https://github.com/nvm-sh/nvm\n\
93             - fnm: https://github.com/Schniz/fnm",
94        )?;
95
96    if !output.status.success() {
97        anyhow::bail!("Node.js is installed but not working correctly");
98    }
99
100    // Parse version
101    let version_str = String::from_utf8_lossy(&output.stdout);
102    let version_str = version_str.trim().trim_start_matches('v');
103
104    // Extract major version
105    let major_version = version_str
106        .split('.')
107        .next()
108        .and_then(|s| s.parse::<u32>().ok())
109        .context("Failed to parse Node.js version")?;
110
111    if major_version < 18 {
112        anyhow::bail!(
113            "Node.js version {version_str} is too old.\n\
114             \n\
115             Required: Node.js 18.0.0 or higher\n\
116             Current:  Node.js {version_str}\n\
117             \n\
118             Please upgrade Node.js:\n\
119             - Download: https://nodejs.org\n\
120             - Or use nvm: nvm install 18"
121        );
122    }
123
124    println!("✓ Node.js v{version_str} detected");
125    Ok(())
126}
127
128/// Checks if MCP configuration exists.
129///
130/// Validates that ~/.claude/mcp.json exists and is readable.
131/// Provides helpful guidance if not found.
132///
133/// # Errors
134///
135/// Returns error if home directory cannot be determined.
136/// Warns if config file doesn't exist but doesn't fail.
137fn check_mcp_config() -> Result<()> {
138    let config_path = get_mcp_config_path()?;
139
140    if config_path.exists() {
141        println!("✓ MCP configuration found: {}", config_path.display());
142    } else {
143        println!("⚠ MCP configuration not found");
144        println!("  Expected location: {}", config_path.display());
145        println!("  Create it with your server configurations:");
146        println!();
147        println!("  {{");
148        println!("    \"mcpServers\": {{");
149        println!("      \"github\": {{");
150        println!("        \"command\": \"docker\",");
151        println!("        \"args\": [\"run\", \"-i\", \"--rm\", \"...\"]");
152        println!("      }}");
153        println!("    }}");
154        println!("  }}");
155        println!();
156        println!("  See examples/mcp.json.example for more details.");
157    }
158
159    Ok(())
160}
161
162/// Makes TypeScript files executable (Unix only).
163///
164/// Sets executable permissions (0755) on all .ts files in ~/.claude/servers/
165/// This allows files to be executed with shebang: `./tool.ts`
166///
167/// # Platform Support
168///
169/// - Unix/Linux/macOS: Sets permissions
170/// - Windows: No-op (not needed)
171///
172/// # Errors
173///
174/// Returns error if:
175/// - Home directory cannot be determined
176/// - Permission changes fail
177#[cfg(unix)]
178async fn make_files_executable() -> Result<()> {
179    use std::os::unix::fs::PermissionsExt;
180    use tokio::fs;
181
182    let servers_dir = get_servers_dir()?;
183
184    // Check if servers directory exists
185    if !servers_dir.exists() {
186        println!("⚠ No servers directory found");
187        println!("  Run 'mcp-execution-cli generate <server>' to create tools");
188        return Ok(());
189    }
190
191    // Walk through all .ts files and make them executable
192    let mut count = 0;
193    let mut entries = fs::read_dir(&servers_dir).await?;
194
195    while let Some(entry) = entries.next_entry().await? {
196        let path = entry.path();
197
198        if path.is_dir() {
199            // Recurse into server directories
200            if let Ok(mut server_entries) = fs::read_dir(&path).await {
201                while let Some(server_entry) = server_entries.next_entry().await? {
202                    let file_path = server_entry.path();
203
204                    if file_path.extension().and_then(|s| s.to_str()) == Some("ts") {
205                        let metadata = fs::metadata(&file_path).await?;
206                        let mut perms = metadata.permissions();
207                        perms.set_mode(0o755); // rwxr-xr-x
208                        fs::set_permissions(&file_path, perms).await?;
209                        count += 1;
210                    }
211                }
212            }
213        }
214    }
215
216    if count > 0 {
217        println!("✓ Made {count} TypeScript files executable");
218    }
219
220    Ok(())
221}
222
223/// Gets the path to ~/.claude/mcp.json
224fn get_mcp_config_path() -> Result<PathBuf> {
225    let home = dirs::home_dir().context("Failed to get home directory")?;
226    Ok(home.join(".claude").join("mcp.json"))
227}
228
229/// Gets the path to ~/.claude/servers/
230fn get_servers_dir() -> Result<PathBuf> {
231    let home = dirs::home_dir().context("Failed to get home directory")?;
232    Ok(home.join(".claude").join("servers"))
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[tokio::test]
240    async fn test_check_node_version() {
241        // This test will pass if Node.js 18+ is installed
242        // Otherwise it will fail, which is the expected behavior
243        let result = check_node_version().await;
244
245        // We can't assert success because Node.js might not be installed
246        // in CI environment, but we can verify error messages are helpful
247        if let Err(e) = result {
248            let error_msg = e.to_string();
249            assert!(
250                error_msg.contains("Node.js") || error_msg.contains("version"),
251                "Error message should be helpful: {error_msg}"
252            );
253        }
254    }
255
256    #[test]
257    fn test_get_mcp_config_path() {
258        let path = get_mcp_config_path();
259        assert!(path.is_ok());
260
261        let path = path.unwrap();
262        assert!(path.to_string_lossy().contains(".claude"));
263        assert!(path.to_string_lossy().contains("mcp.json"));
264    }
265
266    #[test]
267    fn test_get_servers_dir() {
268        let path = get_servers_dir();
269        assert!(path.is_ok());
270
271        let path = path.unwrap();
272        assert!(path.to_string_lossy().contains(".claude"));
273        assert!(path.to_string_lossy().contains("servers"));
274    }
275
276    #[test]
277    fn test_check_mcp_config_no_panic() {
278        // Should not panic even if config doesn't exist
279        let result = check_mcp_config();
280        assert!(result.is_ok());
281    }
282}