mcp_execution_cli/commands/
setup.rs1use anyhow::{Context, Result};
9use mcp_execution_core::cli::ExitCode;
10use std::path::PathBuf;
11use std::process::Stdio;
12use tokio::process::Command;
13
14pub async fn run() -> Result<ExitCode> {
44 println!("Checking runtime environment...\n");
45
46 check_node_version().await?;
48
49 check_mcp_config()?;
51
52 #[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
67async fn check_node_version() -> Result<()> {
78 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 let version_str = String::from_utf8_lossy(&output.stdout);
102 let version_str = version_str.trim().trim_start_matches('v');
103
104 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
128fn 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#[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 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 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 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); 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
223fn 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
229fn 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 let result = check_node_version().await;
244
245 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 let result = check_mcp_config();
280 assert!(result.is_ok());
281 }
282}