1use 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#[derive(Parser)]
21#[command(name = "mcp-cli")]
22#[command(about = "MCP Tools CLI Client")]
23#[command(version = "1.0")]
24pub struct CliArgs {
25 #[arg(short, long, default_value = "http://localhost:8080")]
27 pub server: String,
28
29 #[arg(short, long, default_value = "30")]
31 pub timeout: u64,
32
33 #[arg(short, long)]
35 pub verbose: bool,
36
37 #[arg(short, long, default_value = "table")]
39 pub format: String,
40
41 #[command(subcommand)]
43 pub command: Option<CliCommand>,
44}
45
46#[derive(Subcommand, Clone)]
48pub enum CliCommand {
49 Connect,
51
52 ListTools,
54
55 Execute {
57 tool: String,
59
60 #[arg(short, long)]
62 args: Option<String>,
63
64 #[arg(short = 'p', long = "param")]
66 params: Vec<String>,
67 },
68
69 Interactive,
71
72 Status,
74
75 Disconnect,
77}
78
79pub 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 pub async fn run(&mut self) -> Result<()> {
93 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 match self.args.command.clone() {
104 Some(command) => self.execute_command(command).await,
105 None => self.interactive_mode().await,
106 }
107 }
108
109 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(), ¶ms)?;
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 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 print!("Connecting to {}... ", self.args.server);
160 io::stdout().flush().unwrap();
161 self.connect().await?;
162 println!("Connected!");
163
164 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 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 if capabilities.tools.iter().any(|t| t.name == tool_name) {
239 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 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 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 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 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 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 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 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 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 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}