use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use rmcp::model::{CallToolRequestParams, ReadResourceRequestParams};
use rmcp::transport::StreamableHttpClientTransport;
use rmcp::ServiceExt;
use serde_json::{Map, Value};
#[derive(Parser)]
#[command(name = "gt-mcp-cli", version, about = "CLI client for the gt-mcp server")]
struct Cli {
#[arg(
long,
env = "GT_MCP_URL",
default_value = "http://127.0.0.1:8765/mcp",
global = true
)]
url: String,
#[command(subcommand)]
cmd: Command,
}
#[derive(Subcommand)]
enum Command {
Tools {
#[arg(long)]
full: bool,
},
Resources,
Call {
name: String,
#[arg(long = "arg", value_name = "K=V")]
args: Vec<String>,
#[arg(long)]
json: Option<String>,
},
Read { uri: String },
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let transport = StreamableHttpClientTransport::from_uri(cli.url.clone());
let client = ()
.serve(transport)
.await
.with_context(|| format!("connect + MCP initialize at {}", cli.url))?;
let mut exit_error = false;
let outcome: Result<()> = async {
match cli.cmd {
Command::Tools { full } => {
let tools = client.list_all_tools().await.context("list tools")?;
if full {
println!("{}", serde_json::to_string_pretty(&tools)?);
} else {
for t in &tools {
let desc = t.description.as_deref().unwrap_or("");
println!("{}\t{}", t.name, desc);
}
}
}
Command::Resources => {
let resources = client.list_all_resources().await.context("list resources")?;
for r in &resources {
println!("{}\t{}", r.uri, r.name);
}
}
Command::Call { name, args, json } => {
let arguments = build_arguments(&args, json.as_deref())?;
let mut params = CallToolRequestParams::new(name);
if let Some(obj) = arguments {
params = params.with_arguments(obj);
}
let result = client.call_tool(params).await.context("call tool")?;
println!("{}", serde_json::to_string_pretty(&result)?);
exit_error = result.is_error == Some(true);
}
Command::Read { uri } => {
let result = client
.read_resource(ReadResourceRequestParams::new(uri))
.await
.context("read resource")?;
println!("{}", serde_json::to_string_pretty(&result)?);
}
}
Ok(())
}
.await;
let _ = client.cancel().await;
outcome?;
if exit_error {
std::process::exit(1);
}
Ok(())
}
fn build_arguments(pairs: &[String], json: Option<&str>) -> Result<Option<Map<String, Value>>> {
if let Some(raw) = json {
let value: Value = serde_json::from_str(raw).context("parse --json")?;
let obj = value
.as_object()
.ok_or_else(|| anyhow!("--json must be a JSON object"))?
.clone();
return Ok(Some(obj));
}
if pairs.is_empty() {
return Ok(None);
}
let mut map = Map::new();
for pair in pairs {
let (key, raw) = pair
.split_once('=')
.ok_or_else(|| anyhow!("--arg must be in k=v form: {pair}"))?;
let value =
serde_json::from_str::<Value>(raw).unwrap_or_else(|_| Value::String(raw.to_string()));
map.insert(key.to_string(), value);
}
Ok(Some(map))
}