lc/cli/
mcp.rs

1//! MCP (Model Context Protocol) commands
2
3use crate::cli::{McpCommands, McpServerType as CliMcpServerType};
4use anyhow::Result;
5use colored::*;
6use std::collections::HashMap;
7
8/// Handle MCP-related commands
9pub async fn handle(command: McpCommands) -> Result<()> {
10    use crate::services::mcp::{McpConfig, McpServerType as ConfigMcpServerType};
11    
12    match command {
13        McpCommands::Add {
14            name,
15            command_or_url,
16            server_type,
17            env,
18        } => {
19            let mut config = McpConfig::load().await?;
20            
21            // Convert CLI enum to config enum
22            let config_server_type = match server_type {
23                CliMcpServerType::Stdio => ConfigMcpServerType::Stdio,
24                CliMcpServerType::Sse => ConfigMcpServerType::Sse,
25                CliMcpServerType::Streamable => ConfigMcpServerType::Streamable,
26            };
27            
28            // Convert env vec to HashMap
29            let env_map: HashMap<String, String> = env.into_iter().collect();
30            
31            // For npx commands without -y, add it to ensure package download
32            let final_command_or_url =
33                if command_or_url.starts_with("npx ") && !command_or_url.contains(" -y ") {
34                    command_or_url.replacen("npx ", "npx -y ", 1)
35                } else {
36                    command_or_url.clone()
37                };
38            
39            config.add_server_with_env(
40                name.clone(),
41                final_command_or_url.clone(),
42                config_server_type,
43                env_map.clone(),
44            )?;
45            config.save().await?;
46            
47            println!("{} MCP server '{}' added successfully", "✓".green(), name);
48            println!("  Type: {:?}", server_type);
49            println!("  Command/URL: {}", final_command_or_url);
50            if !env_map.is_empty() {
51                println!("  Environment variables:");
52                for (key, _) in env_map {
53                    println!("    - {}", key);
54                }
55            }
56        }
57        McpCommands::Delete { name } => {
58            let mut config = McpConfig::load().await?;
59            
60            if config.get_server(&name).is_none() {
61                anyhow::bail!("MCP server '{}' not found", name);
62            }
63            
64            config.delete_server(&name)?;
65            config.save().await?;
66            
67            println!("{} MCP server '{}' deleted successfully", "✓".green(), name);
68        }
69        McpCommands::Functions { name } => {
70            let config = McpConfig::load().await?;
71            
72            if config.get_server(&name).is_some() {
73                println!(
74                    "{} Listing functions for MCP server '{}'...",
75                    "🔍".blue(),
76                    name
77                );
78                
79                let daemon_client = crate::services::mcp_daemon::DaemonClient::new()?;
80                
81                // First ensure the server is connected
82                match daemon_client.ensure_server_connected(&name).await {
83                    Ok(_) => {
84                        // Now list the tools/functions
85                        match daemon_client.list_tools(&name).await {
86                            Ok(tools_map) => {
87                                if let Some(tools) = tools_map.get(&name) {
88                                    if tools.is_empty() {
89                                        println!("  No functions exposed by this server.");
90                                    } else {
91                                        println!("\n{} Functions exposed by '{}':", "Functions:".bold().blue(), name);
92                                        for tool in tools {
93                                            let description = tool.description
94                                                .as_ref()
95                                                .map(|d| d.to_string())
96                                                .unwrap_or_else(|| "No description available".to_string());
97                                            println!("  {} {} - {}", "•".blue(), tool.name.bold(), description);
98                                            if !tool.input_schema.is_empty() {
99                                                // Pretty print the schema as JSON Value
100                                                let schema_value = serde_json::Value::Object((*tool.input_schema).clone());
101                                                if let Ok(pretty) = serde_json::to_string_pretty(&schema_value) {
102                                                    for line in pretty.lines() {
103                                                        println!("      {}", line.dimmed());
104                                                    }
105                                                }
106                                            }
107                                        }
108                                    }
109                                } else {
110                                    println!("  No functions exposed by this server.");
111                                }
112                            }
113                            Err(e) => {
114                                anyhow::bail!("Failed to list functions for '{}': {}", name, e);
115                            }
116                        }
117                    }
118                    Err(e) => {
119                        anyhow::bail!("Failed to connect to MCP server '{}': {}", name, e);
120                    }
121                }
122            } else {
123                anyhow::bail!("MCP server '{}' not found in configuration", name);
124            }
125        }
126        McpCommands::Invoke {
127            name,
128            function,
129            args,
130        } => {
131            let config = McpConfig::load().await?;
132            
133            if config.get_server(&name).is_some() {
134                println!(
135                    "{} Invoking function '{}' on MCP server '{}'...",
136                    "⚡".yellow(),
137                    function.bold(),
138                    name.bold()
139                );
140                
141                let daemon_client = crate::services::mcp_daemon::DaemonClient::new()?;
142                
143                // First ensure the server is connected
144                match daemon_client.ensure_server_connected(&name).await {
145                    Ok(_) => {
146                        // Parse arguments as JSON
147                        let args_json = if args.is_empty() {
148                            serde_json::json!({})
149                        } else if args.len() == 1 {
150                            // Try to parse as JSON directly
151                            match serde_json::from_str::<serde_json::Value>(&args[0]) {
152                                Ok(json) => json,
153                                Err(_) => {
154                                    // Check if it's a key=value format
155                                    if args[0].contains('=') {
156                                        let mut obj = serde_json::Map::new();
157                                        let parts: Vec<&str> = args[0].splitn(2, '=').collect();
158                                        if parts.len() == 2 {
159                                            obj.insert(
160                                                parts[0].to_string(),
161                                                serde_json::Value::String(parts[1].to_string())
162                                            );
163                                        }
164                                        serde_json::Value::Object(obj)
165                                    } else {
166                                        // If not valid JSON and not key=value, treat as string value
167                                        serde_json::json!({ "value": args[0] })
168                                    }
169                                }
170                            }
171                        } else {
172                            // Multiple args - check if they are key=value pairs
173                            let mut obj = serde_json::Map::new();
174                            let mut all_key_value = true;
175                            
176                            for arg in args.iter() {
177                                if arg.contains('=') {
178                                    let parts: Vec<&str> = arg.splitn(2, '=').collect();
179                                    if parts.len() == 2 {
180                                        // Try to parse the value as JSON first (for nested objects/arrays)
181                                        let value = match serde_json::from_str::<serde_json::Value>(parts[1]) {
182                                            Ok(json_val) => json_val,
183                                            Err(_) => serde_json::Value::String(parts[1].to_string()),
184                                        };
185                                        obj.insert(parts[0].to_string(), value);
186                                    } else {
187                                        all_key_value = false;
188                                        break;
189                                    }
190                                } else {
191                                    all_key_value = false;
192                                    break;
193                                }
194                            }
195                            
196                            if !all_key_value {
197                                // Fallback to indexed keys if not all args are key=value
198                                obj.clear();
199                                for (i, arg) in args.iter().enumerate() {
200                                    obj.insert(format!("arg{}", i), serde_json::Value::String(arg.clone()));
201                                }
202                            }
203                            
204                            serde_json::Value::Object(obj)
205                        };
206                        
207                        // Invoke the function
208                        match daemon_client.call_tool(&name, &function, args_json).await {
209                            Ok(result) => {
210                                println!("{} Function invoked successfully\n", "✓".green());
211                                
212                                // Pretty print the result
213                                if let Ok(pretty) = serde_json::to_string_pretty(&result) {
214                                    println!("Result:");
215                                    for line in pretty.lines() {
216                                        println!("  {}", line);
217                                    }
218                                } else {
219                                    println!("Result: {:?}", result);
220                                }
221                            }
222                            Err(e) => {
223                                anyhow::bail!("Failed to invoke function: {}", e);
224                            }
225                        }
226                    }
227                    Err(e) => {
228                        anyhow::bail!("Failed to connect to MCP server '{}': {}", name, e);
229                    }
230                }
231            } else {
232                anyhow::bail!("MCP server '{}' not found in configuration", name);
233            }
234        }
235        McpCommands::Start { name, command, args } => {
236            use crate::services::mcp::{McpConfig, McpServerType};
237            use std::collections::HashMap;
238            
239            let mut config = McpConfig::load().await?;
240            
241            // Check if command is provided or use existing configuration
242            let (full_command, server_type) = if let Some(cmd) = command {
243                // New server or override existing
244                println!(
245                    "{} Configuring and starting MCP server '{}'...",
246                    "🚀".cyan(),
247                    name.bold()
248                );
249                
250                let full_cmd = if args.is_empty() {
251                    cmd.clone()
252                } else {
253                    format!("{} {}", cmd, args.join(" "))
254                };
255                
256                // Determine server type based on command pattern
257                let srv_type = if cmd.starts_with("http://") || cmd.starts_with("https://") {
258                    McpServerType::Sse
259                } else {
260                    McpServerType::Stdio
261                };
262                
263                // Save the configuration
264                config.add_server_with_env(
265                    name.clone(),
266                    full_cmd.clone(),
267                    srv_type.clone(),
268                    HashMap::new(),
269                )?;
270                config.save().await?;
271                
272                (full_cmd, srv_type)
273            } else {
274                // Use existing configuration
275                if let Some(server_config) = config.get_server(&name) {
276                    println!(
277                        "{} Starting MCP server '{}'...",
278                        "🚀".cyan(),
279                        name.bold()
280                    );
281                    (server_config.command_or_url.clone(), server_config.server_type.clone())
282                } else {
283                    anyhow::bail!(
284                        "MCP server '{}' not found in configuration. Use 'lc mcp add' to configure it first, or provide a command.",
285                        name
286                    );
287                }
288            };
289            
290            // Now connect via daemon
291            let daemon_client = crate::services::mcp_daemon::DaemonClient::new()?;
292            
293            match daemon_client.ensure_server_connected(&name).await {
294                Ok(_) => {
295                    println!(
296                        "{} MCP server '{}' started successfully",
297                        "✓".green(),
298                        name
299                    );
300                    println!("  Command: {}", full_command.dimmed());
301                    println!("  Type: {:?}", server_type);
302                }
303                Err(e) => {
304                    anyhow::bail!("Failed to start MCP server '{}': {}", name, e);
305                }
306            }
307        }
308        McpCommands::Stop { name } => {
309            println!(
310                "{} Stopping MCP server '{}'...",
311                "🛑".red(),
312                name.bold()
313            );
314            
315            let daemon_client = crate::services::mcp_daemon::DaemonClient::new()?;
316            match daemon_client.close_server(&name).await {
317                Ok(_) => {
318                    println!(
319                        "{} MCP server '{}' stopped successfully",
320                        "✓".green(),
321                        name
322                    );
323                    
324                    // Also remove from config
325                    use crate::services::mcp::McpConfig;
326                    let mut config = McpConfig::load().await?;
327                    if config.delete_server(&name).is_ok() {
328                        let _ = config.save().await;
329                    }
330                }
331                Err(e) => {
332                    println!(
333                        "{} Failed to stop MCP server '{}': {}",
334                        "⚠️".yellow(),
335                        name,
336                        e
337                    );
338                }
339            }
340        }
341        McpCommands::List => {
342            println!("{} MCP servers:", "📋".blue());
343            
344            // Load MCP config to show configured servers
345            use crate::services::mcp::McpConfig;
346            let config = McpConfig::load().await?;
347            let servers = config.list_servers();
348            
349            if servers.is_empty() {
350                println!("  No MCP servers configured.");
351                println!(
352                    "\n{}",
353                    "Add one with: lc mcp start <name> <command>".italic().dimmed()
354                );
355            } else {
356                // Check daemon for active connections
357                let daemon_client = crate::services::mcp_daemon::DaemonClient::new();
358                let mut active_servers = vec![];
359                
360                // Try to get connection status for each server
361                if let Ok(client) = daemon_client {
362                    for (name, _) in &servers {
363                        // Try to list tools as a way to check if connected
364                        if client.list_tools(name).await.is_ok() {
365                            active_servers.push(name.clone());
366                        }
367                    }
368                }
369                
370                for (name, server_config) in servers {
371                    let status = if active_servers.contains(&name) {
372                        format!("{} (connected)", "✓".green())
373                    } else {
374                        "".to_string()
375                    };
376                    
377                    println!(
378                        "  {} {} - {:?} ({}) {}",
379                        "•".blue(),
380                        name.bold(),
381                        server_config.server_type,
382                        server_config.command_or_url.dimmed(),
383                        status
384                    );
385                }
386            }
387        }
388        McpCommands::Status { name } => {
389            if let Some(server_name) = name {
390                println!(
391                    "{} Checking status of MCP server '{}'...",
392                    "🔍".blue(),
393                    server_name.bold()
394                );
395                
396                // Check if server is configured
397                use crate::services::mcp::McpConfig;
398                let config = McpConfig::load().await?;
399                
400                if let Some(server_config) = config.get_server(&server_name) {
401                    println!("  Configuration:");
402                    println!("    Type: {:?}", server_config.server_type);
403                    println!("    Command/URL: {}", server_config.command_or_url);
404                    
405                    // Check daemon connection status
406                    let daemon_client = crate::services::mcp_daemon::DaemonClient::new()?;
407                    
408                    // Try to list tools as a connection test
409                    match daemon_client.list_tools(&server_name).await {
410                        Ok(tools_map) => {
411                            println!("    Status: {} Connected", "✓".green());
412                            if let Some(tools) = tools_map.get(&server_name) {
413                                println!("    Available tools: {}", tools.len());
414                                if !tools.is_empty() {
415                                    println!("    Tools:");
416                                    for tool in tools.iter().take(5) {
417                                        println!("      - {}", tool.name);
418                                    }
419                                    if tools.len() > 5 {
420                                        println!("      ... and {} more", tools.len() - 5);
421                                    }
422                                }
423                            }
424                        }
425                        Err(_) => {
426                            println!("    Status: {} Not connected", "✗".red());
427                            println!("    Use 'lc mcp start {}' to connect", server_name);
428                        }
429                    }
430                } else {
431                    println!("{} MCP server '{}' not found", "✗".red(), server_name);
432                    println!("\nAvailable servers:");
433                    let servers = config.list_servers();
434                    if servers.is_empty() {
435                        println!("  No servers configured");
436                    } else {
437                        for (name, _) in servers {
438                            println!("  - {}", name);
439                        }
440                    }
441                }
442            } else {
443                // Show status of all servers
444                println!("{} MCP server status:", "📊".blue());
445                
446                use crate::services::mcp::McpConfig;
447                let config = McpConfig::load().await?;
448                let servers = config.list_servers();
449                
450                if servers.is_empty() {
451                    println!("  No MCP servers configured.");
452                } else {
453                    let daemon_client = crate::services::mcp_daemon::DaemonClient::new();
454                    
455                    for (name, server_config) in servers {
456                        let status = if let Ok(client) = &daemon_client {
457                            if client.list_tools(&name).await.is_ok() {
458                                format!("{} Connected", "✓".green())
459                            } else {
460                                format!("{} Not connected", "✗".red())
461                            }
462                        } else {
463                            format!("{} Daemon unavailable", "⚠️".yellow())
464                        };
465                        
466                        println!(
467                            "  {} {} ({:?}) - {}",
468                            "•".blue(),
469                            name.bold(),
470                            server_config.server_type,
471                            status
472                        );
473                    }
474                }
475            }
476        }
477    }
478    Ok(())
479}