mcp_execution_cli/commands/
setup.rs1use anyhow::{Context, Result};
9use mcp_execution_core::cli::{ExitCode, OutputFormat};
10use serde::Serialize;
11use std::path::PathBuf;
12use std::process::Stdio;
13use tokio::process::Command;
14
15#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
37pub struct SetupResult {
38 pub node_version: String,
40 pub mcp_config_path: String,
42 pub mcp_config_found: bool,
44 pub servers_dir_found: bool,
47 pub files_made_executable: usize,
50}
51
52pub async fn run(output_format: OutputFormat) -> Result<ExitCode> {
87 if output_format == OutputFormat::Pretty {
88 println!("Checking runtime environment...\n");
89 }
90
91 let node_version = check_node_version().await?;
92
93 let mcp_config_path = get_mcp_config_path()?;
94 let mcp_config_found = mcp_config_path.exists();
95
96 let (servers_dir_found, files_made_executable) = check_files_executable().await?;
97
98 let result = SetupResult {
99 node_version,
100 mcp_config_path: mcp_config_path.display().to_string(),
101 mcp_config_found,
102 servers_dir_found,
103 files_made_executable,
104 };
105
106 if output_format == OutputFormat::Pretty {
107 print_pretty_summary(&result);
108 } else {
109 let formatted = crate::formatters::format_output(&result, output_format)?;
110 println!("{formatted}");
111 }
112
113 Ok(ExitCode::SUCCESS)
114}
115
116fn print_pretty_summary(result: &SetupResult) {
118 println!("✓ Node.js v{} detected", result.node_version);
119
120 if result.mcp_config_found {
121 println!("✓ MCP configuration found: {}", result.mcp_config_path);
122 } else {
123 println!("⚠ MCP configuration not found");
124 println!(" Expected location: {}", result.mcp_config_path);
125 println!(" Create it with your server configurations:");
126 println!();
127 println!(" {{");
128 println!(" \"mcpServers\": {{");
129 println!(" \"github\": {{");
130 println!(" \"command\": \"docker\",");
131 println!(" \"args\": [\"run\", \"-i\", \"--rm\", \"...\"]");
132 println!(" }}");
133 println!(" }}");
134 println!(" }}");
135 println!();
136 println!(" See examples/mcp.json.example for more details.");
137 }
138
139 #[cfg(unix)]
140 {
141 if result.servers_dir_found {
142 if result.files_made_executable > 0 {
143 println!(
144 "✓ Made {} TypeScript files executable",
145 result.files_made_executable
146 );
147 }
148 } else {
149 println!("⚠ No servers directory found");
150 println!(" Run 'mcp-execution-cli generate <server>' to create tools");
151 }
152 }
153
154 println!("\n✓ Runtime setup complete");
155 println!(" Claude Code can now execute MCP tools via:");
156 println!(" node ~/.claude/servers/<server>/<tool>.ts '{{\"param\":\"value\"}}'");
157 println!("\nNext steps:");
158 println!(" 1. Generate tools: mcp-execution-cli generate <server>");
159 println!(" 2. Configure servers in ~/.claude/mcp.json");
160 println!(" 3. Execute tools autonomously via Node.js");
161}
162
163async fn check_node_version() -> Result<String> {
175 let output = Command::new("node")
177 .arg("--version")
178 .stdout(Stdio::piped())
179 .stderr(Stdio::piped())
180 .output()
181 .await
182 .context(
183 "Node.js not found in PATH.\n\
184 \n\
185 Node.js 18+ is required for MCP tool execution.\n\
186 Install from: https://nodejs.org\n\
187 \n\
188 Or use a version manager:\n\
189 - nvm: https://github.com/nvm-sh/nvm\n\
190 - fnm: https://github.com/Schniz/fnm",
191 )?;
192
193 if !output.status.success() {
194 anyhow::bail!("Node.js is installed but not working correctly");
195 }
196
197 let version_str = String::from_utf8_lossy(&output.stdout);
199 let version_str = version_str.trim().trim_start_matches('v');
200
201 let major_version = version_str
203 .split('.')
204 .next()
205 .and_then(|s| s.parse::<u32>().ok())
206 .context("Failed to parse Node.js version")?;
207
208 if major_version < 18 {
209 anyhow::bail!(
210 "Node.js version {version_str} is too old.\n\
211 \n\
212 Required: Node.js 18.0.0 or higher\n\
213 Current: Node.js {version_str}\n\
214 \n\
215 Please upgrade Node.js:\n\
216 - Download: https://nodejs.org\n\
217 - Or use nvm: nvm install 18"
218 );
219 }
220
221 Ok(version_str.to_string())
222}
223
224#[cfg(unix)]
240async fn check_files_executable() -> Result<(bool, usize)> {
241 use std::os::unix::fs::PermissionsExt;
242 use tokio::fs;
243
244 let servers_dir = get_servers_dir()?;
245
246 if !servers_dir.exists() {
248 return Ok((false, 0));
249 }
250
251 let mut count = 0;
253 let mut entries = fs::read_dir(&servers_dir).await?;
254
255 while let Some(entry) = entries.next_entry().await? {
256 let path = entry.path();
257
258 if path.is_dir() {
259 if let Ok(mut server_entries) = fs::read_dir(&path).await {
261 while let Some(server_entry) = server_entries.next_entry().await? {
262 let file_path = server_entry.path();
263
264 if file_path.extension().and_then(|s| s.to_str()) == Some("ts") {
265 let metadata = fs::metadata(&file_path).await?;
266 let mut perms = metadata.permissions();
267 perms.set_mode(0o755); fs::set_permissions(&file_path, perms).await?;
269 count += 1;
270 }
271 }
272 }
273 }
274 }
275
276 Ok((true, count))
277}
278
279#[cfg(not(unix))]
283async fn check_files_executable() -> Result<(bool, usize)> {
284 Ok((false, 0))
285}
286
287fn get_mcp_config_path() -> Result<PathBuf> {
289 let home = dirs::home_dir().context("Failed to get home directory")?;
290 Ok(home.join(".claude").join("mcp.json"))
291}
292
293fn get_servers_dir() -> Result<PathBuf> {
295 let home = dirs::home_dir().context("Failed to get home directory")?;
296 Ok(home.join(".claude").join("servers"))
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[tokio::test]
304 async fn test_check_node_version() {
305 let result = check_node_version().await;
308
309 if let Err(e) = result {
312 let error_msg = e.to_string();
313 assert!(
314 error_msg.contains("Node.js") || error_msg.contains("version"),
315 "Error message should be helpful: {error_msg}"
316 );
317 }
318 }
319
320 #[test]
321 fn test_get_mcp_config_path() {
322 let path = get_mcp_config_path();
323 assert!(path.is_ok());
324
325 let path = path.unwrap();
326 assert!(path.to_string_lossy().contains(".claude"));
327 assert!(path.to_string_lossy().contains("mcp.json"));
328 }
329
330 #[test]
331 fn test_get_servers_dir() {
332 let path = get_servers_dir();
333 assert!(path.is_ok());
334
335 let path = path.unwrap();
336 assert!(path.to_string_lossy().contains(".claude"));
337 assert!(path.to_string_lossy().contains("servers"));
338 }
339
340 #[tokio::test]
341 async fn test_check_files_executable_no_panic() {
342 let result = check_files_executable().await;
344 assert!(result.is_ok());
345 }
346
347 #[test]
348 fn test_setup_result_serialization() {
349 let result = SetupResult {
350 node_version: "20.10.0".to_string(),
351 mcp_config_path: "/home/user/.claude/mcp.json".to_string(),
352 mcp_config_found: true,
353 servers_dir_found: true,
354 files_made_executable: 3,
355 };
356
357 let json = serde_json::to_string(&result).unwrap();
358 assert!(json.contains("\"node_version\":\"20.10.0\""));
359 assert!(json.contains("\"mcp_config_found\":true"));
360 assert!(json.contains("\"files_made_executable\":3"));
361 }
362
363 #[test]
364 fn test_setup_result_format_output_json() {
365 let result = SetupResult {
366 node_version: "20.10.0".to_string(),
367 mcp_config_path: "/home/user/.claude/mcp.json".to_string(),
368 mcp_config_found: false,
369 servers_dir_found: true,
370 files_made_executable: 7,
371 };
372
373 let formatted =
374 crate::formatters::format_output(&result, mcp_execution_core::cli::OutputFormat::Json)
375 .unwrap();
376 assert!(formatted.contains("\"node_version\": \"20.10.0\""));
377 assert!(formatted.contains("\"mcp_config_path\": \"/home/user/.claude/mcp.json\""));
378 assert!(formatted.contains("\"mcp_config_found\": false"));
379 assert!(formatted.contains("\"servers_dir_found\": true"));
380 assert!(formatted.contains("\"files_made_executable\": 7"));
381 }
382
383 #[test]
384 fn test_setup_result_format_output_text() {
385 let result = SetupResult {
386 node_version: "20.10.0".to_string(),
387 mcp_config_path: "/home/user/.claude/mcp.json".to_string(),
388 mcp_config_found: true,
389 servers_dir_found: false,
390 files_made_executable: 0,
391 };
392
393 let formatted =
394 crate::formatters::format_output(&result, mcp_execution_core::cli::OutputFormat::Text)
395 .unwrap();
396 assert!(!formatted.contains('\n'));
399 assert!(formatted.contains("\"node_version\":\"20.10.0\""));
400 assert!(formatted.contains("\"mcp_config_found\":true"));
401 assert!(formatted.contains("\"servers_dir_found\":false"));
402 assert!(formatted.contains("\"files_made_executable\":0"));
403 }
404}