mcp_tools/clients/
cli.rs

1//! CLI MCP Client
2//!
3//! Command-line interface for interacting with MCP servers
4//! Provides interactive and batch modes for testing and automation
5
6use async_trait::async_trait;
7use clap::{Parser, Subcommand};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::io::{self, Write};
11use tracing::{debug, error, info, warn};
12
13use crate::common::{
14    BaseClient, ClientConfig, ConnectionStatus, McpClientBase, McpToolRequest, McpToolResponse,
15    ServerCapabilities,
16};
17use crate::{McpToolsError, Result};
18
19/// CLI arguments for the MCP client
20#[derive(Parser)]
21#[command(name = "mcp-cli")]
22#[command(about = "MCP Tools CLI Client")]
23#[command(version = "1.0")]
24pub struct CliArgs {
25    /// Server URL to connect to
26    #[arg(short, long, default_value = "http://localhost:8080")]
27    pub server: String,
28
29    /// Connection timeout in seconds
30    #[arg(short, long, default_value = "30")]
31    pub timeout: u64,
32
33    /// Enable verbose logging
34    #[arg(short, long)]
35    pub verbose: bool,
36
37    /// Output format (json, yaml, table)
38    #[arg(short, long, default_value = "table")]
39    pub format: String,
40
41    /// Command to execute
42    #[command(subcommand)]
43    pub command: Option<CliCommand>,
44}
45
46/// CLI commands
47#[derive(Subcommand, Clone)]
48pub enum CliCommand {
49    /// Connect to MCP server and show capabilities
50    Connect,
51
52    /// List available tools
53    ListTools,
54
55    /// Execute a tool
56    Execute {
57        /// Tool name to execute
58        tool: String,
59
60        /// Tool arguments as JSON string
61        #[arg(short, long)]
62        args: Option<String>,
63
64        /// Tool arguments as key=value pairs
65        #[arg(short = 'p', long = "param")]
66        params: Vec<String>,
67    },
68
69    /// Interactive mode
70    Interactive,
71
72    /// Show server status
73    Status,
74
75    /// Disconnect from server
76    Disconnect,
77}
78
79/// CLI MCP Client
80pub struct CliClient {
81    base: BaseClient,
82    args: CliArgs,
83}
84
85impl CliClient {
86    pub fn new(config: ClientConfig, args: CliArgs) -> Self {
87        let base = BaseClient::new(config);
88        Self { base, args }
89    }
90
91    /// Run the CLI client
92    pub async fn run(&mut self) -> Result<()> {
93        // Initialize logging
94        if self.args.verbose {
95            tracing_subscriber::fmt().with_env_filter("debug").init();
96        } else {
97            tracing_subscriber::fmt().with_env_filter("info").init();
98        }
99
100        info!("Starting MCP CLI Client");
101
102        // Execute command or enter interactive mode
103        match self.args.command.clone() {
104            Some(command) => self.execute_command(command).await,
105            None => self.interactive_mode().await,
106        }
107    }
108
109    /// Execute a specific command
110    async fn execute_command(&mut self, command: CliCommand) -> Result<()> {
111        match command {
112            CliCommand::Connect => {
113                println!("Connecting to MCP server at {}...", self.args.server);
114                self.connect().await?;
115                let capabilities = self.get_server_capabilities().await?;
116                self.print_capabilities(&capabilities);
117                Ok(())
118            }
119            CliCommand::ListTools => {
120                self.connect().await?;
121                let capabilities = self.get_server_capabilities().await?;
122                self.print_tools(&capabilities);
123                Ok(())
124            }
125            CliCommand::Execute { tool, args, params } => {
126                self.connect().await?;
127                let arguments = self.parse_arguments(args.as_deref(), &params)?;
128                let request = McpToolRequest {
129                    id: uuid::Uuid::new_v4(),
130                    tool: tool.clone(),
131                    arguments: serde_json::to_value(arguments)?,
132                    session_id: uuid::Uuid::new_v4().to_string(),
133                    metadata: HashMap::new(),
134                };
135                let response = self.execute_tool(request).await?;
136                self.print_response(&response);
137                Ok(())
138            }
139            CliCommand::Interactive => self.interactive_mode().await,
140            CliCommand::Status => {
141                let status = self.get_status().await?;
142                self.print_status(&status);
143                Ok(())
144            }
145            CliCommand::Disconnect => {
146                self.disconnect().await?;
147                println!("Disconnected from MCP server");
148                Ok(())
149            }
150        }
151    }
152
153    /// Enter interactive mode
154    async fn interactive_mode(&mut self) -> Result<()> {
155        println!("MCP Tools CLI - Interactive Mode");
156        println!("Type 'help' for available commands, 'quit' to exit");
157
158        // Connect to server
159        print!("Connecting to {}... ", self.args.server);
160        io::stdout().flush().unwrap();
161        self.connect().await?;
162        println!("Connected!");
163
164        // Get capabilities
165        let capabilities = self.get_server_capabilities().await?;
166        println!(
167            "Server capabilities loaded. {} tools available.",
168            capabilities.tools.len()
169        );
170
171        loop {
172            print!("mcp> ");
173            io::stdout().flush().unwrap();
174
175            let mut input = String::new();
176            match io::stdin().read_line(&mut input) {
177                Ok(_) => {
178                    let input = input.trim();
179                    if input.is_empty() {
180                        continue;
181                    }
182
183                    match self.handle_interactive_command(input, &capabilities).await {
184                        Ok(should_continue) => {
185                            if !should_continue {
186                                break;
187                            }
188                        }
189                        Err(e) => {
190                            eprintln!("Error: {}", e);
191                        }
192                    }
193                }
194                Err(e) => {
195                    eprintln!("Error reading input: {}", e);
196                    break;
197                }
198            }
199        }
200
201        self.disconnect().await?;
202        println!("Goodbye!");
203        Ok(())
204    }
205
206    /// Handle interactive command
207    async fn handle_interactive_command(
208        &mut self,
209        input: &str,
210        capabilities: &ServerCapabilities,
211    ) -> Result<bool> {
212        let parts: Vec<&str> = input.split_whitespace().collect();
213        if parts.is_empty() {
214            return Ok(true);
215        }
216
217        match parts[0] {
218            "help" => {
219                self.print_help();
220                Ok(true)
221            }
222            "quit" | "exit" => Ok(false),
223            "tools" | "list" => {
224                self.print_tools(capabilities);
225                Ok(true)
226            }
227            "status" => {
228                let status = self.get_status().await?;
229                self.print_status(&status);
230                Ok(true)
231            }
232            "capabilities" => {
233                self.print_capabilities(capabilities);
234                Ok(true)
235            }
236            tool_name => {
237                // Try to execute as a tool
238                if capabilities.tools.iter().any(|t| t.name == tool_name) {
239                    // Parse arguments from remaining parts
240                    let mut arguments = HashMap::new();
241                    for part in &parts[1..] {
242                        if let Some((key, value)) = part.split_once('=') {
243                            arguments.insert(key.to_string(), Value::String(value.to_string()));
244                        }
245                    }
246
247                    let request = McpToolRequest {
248                        id: uuid::Uuid::new_v4(),
249                        tool: tool_name.to_string(),
250                        arguments: serde_json::to_value(arguments)?,
251                        session_id: uuid::Uuid::new_v4().to_string(),
252                        metadata: HashMap::new(),
253                    };
254
255                    let response = self.execute_tool(request).await?;
256                    self.print_response(&response);
257                } else {
258                    println!(
259                        "Unknown command or tool: {}. Type 'help' for available commands.",
260                        tool_name
261                    );
262                }
263                Ok(true)
264            }
265        }
266    }
267
268    /// Parse arguments from JSON string or key=value pairs
269    fn parse_arguments(
270        &self,
271        json_args: Option<&str>,
272        params: &[String],
273    ) -> Result<HashMap<String, Value>> {
274        let mut arguments = HashMap::new();
275
276        // Parse JSON arguments if provided
277        if let Some(json_str) = json_args {
278            let json_value: Value = serde_json::from_str(json_str)
279                .map_err(|e| McpToolsError::Server(format!("Invalid JSON arguments: {}", e)))?;
280
281            if let Value::Object(obj) = json_value {
282                for (key, value) in obj {
283                    arguments.insert(key, value);
284                }
285            }
286        }
287
288        // Parse key=value parameters
289        for param in params {
290            if let Some((key, value)) = param.split_once('=') {
291                arguments.insert(key.to_string(), Value::String(value.to_string()));
292            } else {
293                return Err(McpToolsError::Server(format!(
294                    "Invalid parameter format: {}. Use key=value",
295                    param
296                )));
297            }
298        }
299
300        Ok(arguments)
301    }
302
303    /// Print help information
304    fn print_help(&self) {
305        println!("Available commands:");
306        println!("  help                    - Show this help message");
307        println!("  tools, list             - List available tools");
308        println!("  status                  - Show connection status");
309        println!("  capabilities            - Show server capabilities");
310        println!("  <tool_name> key=value   - Execute a tool with parameters");
311        println!("  quit, exit              - Exit interactive mode");
312        println!();
313        println!("Examples:");
314        println!("  git_status repo_path=/path/to/repo");
315        println!("  http_request url=https://api.example.com method=GET");
316        println!("  analyze_code file_path=main.rs language=rust");
317    }
318
319    /// Print server capabilities
320    fn print_capabilities(&self, capabilities: &ServerCapabilities) {
321        match self.args.format.as_str() {
322            "json" => {
323                println!(
324                    "{}",
325                    serde_json::to_string_pretty(capabilities).unwrap_or_default()
326                );
327            }
328            "yaml" => {
329                // Would need serde_yaml dependency
330                println!("YAML format not implemented");
331            }
332            _ => {
333                println!("Server Capabilities:");
334                println!("  Protocol Version: {}", capabilities.info.protocol_version);
335                println!("  Server Name: {}", capabilities.info.name);
336                println!("  Server Version: {}", capabilities.info.version);
337                println!("  Tools Available: {}", capabilities.tools.len());
338
339                if !capabilities.tools.is_empty() {
340                    println!("\nTools:");
341                    for tool in &capabilities.tools {
342                        println!("  - {} ({})", tool.name, tool.category);
343                        println!("    Description: {}", tool.description);
344                        if tool.requires_permission {
345                            println!("    Permissions: {:?}", tool.permissions);
346                        }
347                    }
348                }
349            }
350        }
351    }
352
353    /// Print available tools
354    fn print_tools(&self, capabilities: &ServerCapabilities) {
355        match self.args.format.as_str() {
356            "json" => {
357                println!(
358                    "{}",
359                    serde_json::to_string_pretty(&capabilities.tools).unwrap_or_default()
360                );
361            }
362            _ => {
363                println!("Available Tools ({}):", capabilities.tools.len());
364                println!("{:<20} {:<15} {}", "Name", "Category", "Description");
365                println!("{}", "-".repeat(80));
366
367                for tool in &capabilities.tools {
368                    println!(
369                        "{:<20} {:<15} {}",
370                        tool.name,
371                        tool.category,
372                        if tool.description.len() > 40 {
373                            format!("{}...", &tool.description[..37])
374                        } else {
375                            tool.description.clone()
376                        }
377                    );
378                }
379            }
380        }
381    }
382
383    /// Print tool response
384    fn print_response(&self, response: &McpToolResponse) {
385        match self.args.format.as_str() {
386            "json" => {
387                println!(
388                    "{}",
389                    serde_json::to_string_pretty(response).unwrap_or_default()
390                );
391            }
392            _ => {
393                if response.is_error {
394                    println!(
395                        "Error: {}",
396                        response.error.as_deref().unwrap_or("Unknown error")
397                    );
398                } else {
399                    println!("Tool Response (ID: {}):", response.id);
400                    for content in &response.content {
401                        match content {
402                            crate::common::McpContent::Text { text } => {
403                                println!("{}", text);
404                            }
405                            crate::common::McpContent::Image { data, mime_type } => {
406                                println!("Image: {} bytes ({})", data.len(), mime_type);
407                            }
408                            crate::common::McpContent::Resource {
409                                uri,
410                                mime_type,
411                                text,
412                            } => {
413                                println!(
414                                    "Resource: {} ({})",
415                                    uri,
416                                    mime_type.as_deref().unwrap_or("unknown")
417                                );
418                                if let Some(text) = text {
419                                    println!("{}", text);
420                                }
421                            }
422                        }
423                    }
424
425                    if !response.metadata.is_empty() {
426                        println!("\nMetadata:");
427                        for (key, value) in &response.metadata {
428                            println!("  {}: {}", key, value);
429                        }
430                    }
431                }
432            }
433        }
434    }
435
436    /// Print connection status
437    fn print_status(&self, status: &ConnectionStatus) {
438        match self.args.format.as_str() {
439            "json" => {
440                println!(
441                    "{}",
442                    serde_json::to_string_pretty(status).unwrap_or_default()
443                );
444            }
445            _ => {
446                println!("Connection Status:");
447                match status {
448                    ConnectionStatus::Disconnected => println!("  Status: Disconnected"),
449                    ConnectionStatus::Connecting => println!("  Status: Connecting"),
450                    ConnectionStatus::Connected => println!("  Status: Connected"),
451                    ConnectionStatus::Error(error) => println!("  Status: Error - {}", error),
452                }
453            }
454        }
455    }
456}
457
458#[async_trait]
459impl McpClientBase for CliClient {
460    async fn connect(&mut self) -> Result<()> {
461        debug!("Connecting to MCP server");
462        self.base.connect().await
463    }
464
465    async fn disconnect(&mut self) -> Result<()> {
466        debug!("Disconnecting from MCP server");
467        self.base.disconnect().await
468    }
469
470    async fn get_server_capabilities(&self) -> Result<ServerCapabilities> {
471        debug!("Getting server capabilities");
472        self.base.get_server_capabilities().await
473    }
474
475    async fn execute_tool(&self, request: McpToolRequest) -> Result<McpToolResponse> {
476        debug!("Executing tool: {}", request.tool);
477        self.base.execute_tool(request).await
478    }
479
480    async fn get_status(&self) -> Result<ConnectionStatus> {
481        debug!("Getting connection status");
482        self.base.get_status().await
483    }
484}