use async_trait::async_trait;
use clap::{Parser, Subcommand};
use serde_json::Value;
use std::collections::HashMap;
use std::io::{self, Write};
use tracing::{debug, error, info, warn};
use crate::common::{
BaseClient, ClientConfig, ConnectionStatus, McpClientBase, McpToolRequest, McpToolResponse,
ServerCapabilities,
};
use crate::{McpToolsError, Result};
#[derive(Parser)]
#[command(name = "mcp-cli")]
#[command(about = "MCP Tools CLI Client")]
#[command(version = "1.0")]
pub struct CliArgs {
#[arg(short, long, default_value = "http://localhost:8080")]
pub server: String,
#[arg(short, long, default_value = "30")]
pub timeout: u64,
#[arg(short, long)]
pub verbose: bool,
#[arg(short, long, default_value = "table")]
pub format: String,
#[command(subcommand)]
pub command: Option<CliCommand>,
}
#[derive(Subcommand, Clone)]
pub enum CliCommand {
Connect,
ListTools,
Execute {
tool: String,
#[arg(short, long)]
args: Option<String>,
#[arg(short = 'p', long = "param")]
params: Vec<String>,
},
Interactive,
Status,
Disconnect,
}
pub struct CliClient {
base: BaseClient,
args: CliArgs,
}
impl CliClient {
pub fn new(config: ClientConfig, args: CliArgs) -> Self {
let base = BaseClient::new(config);
Self { base, args }
}
pub async fn run(&mut self) -> Result<()> {
if self.args.verbose {
tracing_subscriber::fmt().with_env_filter("debug").init();
} else {
tracing_subscriber::fmt().with_env_filter("info").init();
}
info!("Starting MCP CLI Client");
match self.args.command.clone() {
Some(command) => self.execute_command(command).await,
None => self.interactive_mode().await,
}
}
async fn execute_command(&mut self, command: CliCommand) -> Result<()> {
match command {
CliCommand::Connect => {
println!("Connecting to MCP server at {}...", self.args.server);
self.connect().await?;
let capabilities = self.get_server_capabilities().await?;
self.print_capabilities(&capabilities);
Ok(())
}
CliCommand::ListTools => {
self.connect().await?;
let capabilities = self.get_server_capabilities().await?;
self.print_tools(&capabilities);
Ok(())
}
CliCommand::Execute { tool, args, params } => {
self.connect().await?;
let arguments = self.parse_arguments(args.as_deref(), ¶ms)?;
let request = McpToolRequest {
id: uuid::Uuid::new_v4(),
tool: tool.clone(),
arguments: serde_json::to_value(arguments)?,
session_id: uuid::Uuid::new_v4().to_string(),
metadata: HashMap::new(),
};
let response = self.execute_tool(request).await?;
self.print_response(&response);
Ok(())
}
CliCommand::Interactive => self.interactive_mode().await,
CliCommand::Status => {
let status = self.get_status().await?;
self.print_status(&status);
Ok(())
}
CliCommand::Disconnect => {
self.disconnect().await?;
println!("Disconnected from MCP server");
Ok(())
}
}
}
async fn interactive_mode(&mut self) -> Result<()> {
println!("MCP Tools CLI - Interactive Mode");
println!("Type 'help' for available commands, 'quit' to exit");
print!("Connecting to {}... ", self.args.server);
io::stdout().flush().unwrap();
self.connect().await?;
println!("Connected!");
let capabilities = self.get_server_capabilities().await?;
println!(
"Server capabilities loaded. {} tools available.",
capabilities.tools.len()
);
loop {
print!("mcp> ");
io::stdout().flush().unwrap();
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_) => {
let input = input.trim();
if input.is_empty() {
continue;
}
match self.handle_interactive_command(input, &capabilities).await {
Ok(should_continue) => {
if !should_continue {
break;
}
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
self.disconnect().await?;
println!("Goodbye!");
Ok(())
}
async fn handle_interactive_command(
&mut self,
input: &str,
capabilities: &ServerCapabilities,
) -> Result<bool> {
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.is_empty() {
return Ok(true);
}
match parts[0] {
"help" => {
self.print_help();
Ok(true)
}
"quit" | "exit" => Ok(false),
"tools" | "list" => {
self.print_tools(capabilities);
Ok(true)
}
"status" => {
let status = self.get_status().await?;
self.print_status(&status);
Ok(true)
}
"capabilities" => {
self.print_capabilities(capabilities);
Ok(true)
}
tool_name => {
if capabilities.tools.iter().any(|t| t.name == tool_name) {
let mut arguments = HashMap::new();
for part in &parts[1..] {
if let Some((key, value)) = part.split_once('=') {
arguments.insert(key.to_string(), Value::String(value.to_string()));
}
}
let request = McpToolRequest {
id: uuid::Uuid::new_v4(),
tool: tool_name.to_string(),
arguments: serde_json::to_value(arguments)?,
session_id: uuid::Uuid::new_v4().to_string(),
metadata: HashMap::new(),
};
let response = self.execute_tool(request).await?;
self.print_response(&response);
} else {
println!(
"Unknown command or tool: {}. Type 'help' for available commands.",
tool_name
);
}
Ok(true)
}
}
}
fn parse_arguments(
&self,
json_args: Option<&str>,
params: &[String],
) -> Result<HashMap<String, Value>> {
let mut arguments = HashMap::new();
if let Some(json_str) = json_args {
let json_value: Value = serde_json::from_str(json_str)
.map_err(|e| McpToolsError::Server(format!("Invalid JSON arguments: {}", e)))?;
if let Value::Object(obj) = json_value {
for (key, value) in obj {
arguments.insert(key, value);
}
}
}
for param in params {
if let Some((key, value)) = param.split_once('=') {
arguments.insert(key.to_string(), Value::String(value.to_string()));
} else {
return Err(McpToolsError::Server(format!(
"Invalid parameter format: {}. Use key=value",
param
)));
}
}
Ok(arguments)
}
fn print_help(&self) {
println!("Available commands:");
println!(" help - Show this help message");
println!(" tools, list - List available tools");
println!(" status - Show connection status");
println!(" capabilities - Show server capabilities");
println!(" <tool_name> key=value - Execute a tool with parameters");
println!(" quit, exit - Exit interactive mode");
println!();
println!("Examples:");
println!(" git_status repo_path=/path/to/repo");
println!(" http_request url=https://api.example.com method=GET");
println!(" analyze_code file_path=main.rs language=rust");
}
fn print_capabilities(&self, capabilities: &ServerCapabilities) {
match self.args.format.as_str() {
"json" => {
println!(
"{}",
serde_json::to_string_pretty(capabilities).unwrap_or_default()
);
}
"yaml" => {
println!("YAML format not implemented");
}
_ => {
println!("Server Capabilities:");
println!(" Protocol Version: {}", capabilities.info.protocol_version);
println!(" Server Name: {}", capabilities.info.name);
println!(" Server Version: {}", capabilities.info.version);
println!(" Tools Available: {}", capabilities.tools.len());
if !capabilities.tools.is_empty() {
println!("\nTools:");
for tool in &capabilities.tools {
println!(" - {} ({})", tool.name, tool.category);
println!(" Description: {}", tool.description);
if tool.requires_permission {
println!(" Permissions: {:?}", tool.permissions);
}
}
}
}
}
}
fn print_tools(&self, capabilities: &ServerCapabilities) {
match self.args.format.as_str() {
"json" => {
println!(
"{}",
serde_json::to_string_pretty(&capabilities.tools).unwrap_or_default()
);
}
_ => {
println!("Available Tools ({}):", capabilities.tools.len());
println!("{:<20} {:<15} {}", "Name", "Category", "Description");
println!("{}", "-".repeat(80));
for tool in &capabilities.tools {
println!(
"{:<20} {:<15} {}",
tool.name,
tool.category,
if tool.description.len() > 40 {
format!("{}...", &tool.description[..37])
} else {
tool.description.clone()
}
);
}
}
}
}
fn print_response(&self, response: &McpToolResponse) {
match self.args.format.as_str() {
"json" => {
println!(
"{}",
serde_json::to_string_pretty(response).unwrap_or_default()
);
}
_ => {
if response.is_error {
println!(
"Error: {}",
response.error.as_deref().unwrap_or("Unknown error")
);
} else {
println!("Tool Response (ID: {}):", response.id);
for content in &response.content {
match content {
crate::common::McpContent::Text { text } => {
println!("{}", text);
}
crate::common::McpContent::Image { data, mime_type } => {
println!("Image: {} bytes ({})", data.len(), mime_type);
}
crate::common::McpContent::Resource {
uri,
mime_type,
text,
} => {
println!(
"Resource: {} ({})",
uri,
mime_type.as_deref().unwrap_or("unknown")
);
if let Some(text) = text {
println!("{}", text);
}
}
}
}
if !response.metadata.is_empty() {
println!("\nMetadata:");
for (key, value) in &response.metadata {
println!(" {}: {}", key, value);
}
}
}
}
}
}
fn print_status(&self, status: &ConnectionStatus) {
match self.args.format.as_str() {
"json" => {
println!(
"{}",
serde_json::to_string_pretty(status).unwrap_or_default()
);
}
_ => {
println!("Connection Status:");
match status {
ConnectionStatus::Disconnected => println!(" Status: Disconnected"),
ConnectionStatus::Connecting => println!(" Status: Connecting"),
ConnectionStatus::Connected => println!(" Status: Connected"),
ConnectionStatus::Error(error) => println!(" Status: Error - {}", error),
}
}
}
}
}
#[async_trait]
impl McpClientBase for CliClient {
async fn connect(&mut self) -> Result<()> {
debug!("Connecting to MCP server");
self.base.connect().await
}
async fn disconnect(&mut self) -> Result<()> {
debug!("Disconnecting from MCP server");
self.base.disconnect().await
}
async fn get_server_capabilities(&self) -> Result<ServerCapabilities> {
debug!("Getting server capabilities");
self.base.get_server_capabilities().await
}
async fn execute_tool(&self, request: McpToolRequest) -> Result<McpToolResponse> {
debug!("Executing tool: {}", request.tool);
self.base.execute_tool(request).await
}
async fn get_status(&self) -> Result<ConnectionStatus> {
debug!("Getting connection status");
self.base.get_status().await
}
}