use std::io::{self, Write};
use std::net::SocketAddr;
use serde::Serialize;
use crate::cli::OutputFormat;
use crate::cli::admin_auth::{admin_get, parse_error_message};
#[derive(Debug, Serialize)]
pub struct ToolEntry {
pub name: String,
pub provider: String,
pub description: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ToolListData {
pub tools: Vec<ToolEntry>,
}
#[derive(Debug, Serialize)]
pub struct ServerEntry {
pub name: String,
pub tool_count: u64,
pub filtered: bool,
}
#[derive(Debug, Serialize)]
pub struct ToolStatusData {
pub servers: Vec<ServerEntry>,
}
pub fn query_list(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
) -> Result<ToolListData, Box<dyn std::error::Error>> {
let resp = admin_get(config, addr, "/admin/tools")?;
if !resp.status().is_success() {
let msg = parse_error_message(resp)?;
return Err(format!("failed to list tools: {msg}").into());
}
let body: serde_json::Value = resp.json()?;
let tools = body["tools"]
.as_array()
.map(|arr| {
arr.iter()
.map(|t| ToolEntry {
name: t["name"].as_str().unwrap_or("?").to_owned(),
provider: t["provider"].as_str().unwrap_or("?").to_owned(),
description: t["description"]
.as_str()
.filter(|s| !s.is_empty())
.map(str::to_owned),
})
.collect()
})
.unwrap_or_default();
Ok(ToolListData { tools })
}
pub fn render_list_text(data: &ToolListData, w: &mut impl Write) -> io::Result<()> {
if data.tools.is_empty() {
writeln!(w, " (no tools available)")?;
return Ok(());
}
for tool in &data.tools {
if let Some(ref desc) = tool.description {
writeln!(w, " {} [{}] \u{2014} {desc}", tool.name, tool.provider)?;
} else {
writeln!(w, " {} [{}]", tool.name, tool.provider)?;
}
}
Ok(())
}
pub fn query_status(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
) -> Result<ToolStatusData, Box<dyn std::error::Error>> {
let resp = admin_get(config, addr, "/admin/tools/upstreams")?;
if !resp.status().is_success() {
let msg = parse_error_message(resp)?;
return Err(format!("failed to get tool status: {msg}").into());
}
let body: serde_json::Value = resp.json()?;
let servers = body["servers"]
.as_array()
.map(|arr| {
arr.iter()
.map(|s| ServerEntry {
name: s["name"].as_str().unwrap_or("?").to_owned(),
tool_count: s["tool_count"].as_u64().unwrap_or(0),
filtered: s["filter"].is_object(),
})
.collect()
})
.unwrap_or_default();
Ok(ToolStatusData { servers })
}
pub fn render_status_text(data: &ToolStatusData, w: &mut impl Write) -> io::Result<()> {
if data.servers.is_empty() {
writeln!(w, " (no MCP servers configured)")?;
return Ok(());
}
for server in &data.servers {
let filter_info = if server.filtered { " (filtered)" } else { "" };
writeln!(
w,
" {} {} tools{filter_info}",
server.name, server.tool_count
)?;
}
Ok(())
}
pub fn run_list(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
output: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let data = query_list(config, addr)?;
match output {
OutputFormat::Text => render_list_text(&data, &mut io::stdout())?,
OutputFormat::Json => serde_json::to_writer(io::stdout(), &data)?,
}
Ok(())
}
pub fn run_status(
config: &bitrouter_config::BitrouterConfig,
addr: SocketAddr,
output: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let data = query_status(config, addr)?;
match output {
OutputFormat::Text => render_status_text(&data, &mut io::stdout())?,
OutputFormat::Json => serde_json::to_writer(io::stdout(), &data)?,
}
Ok(())
}
#[cfg(feature = "mcp")]
pub async fn run_discover(
config: &bitrouter_config::BitrouterConfig,
provider_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
use bitrouter_providers::mcp::client::config::{McpServerConfig, McpServerTransport};
use bitrouter_providers::mcp::client::upstream::UpstreamConnection;
let provider = config
.providers
.get(provider_name)
.ok_or_else(|| format!("provider '{provider_name}' not found in config"))?;
let url = provider
.api_base
.as_deref()
.ok_or_else(|| format!("provider '{provider_name}' has no api_base configured"))?;
let mut headers = provider.default_headers.clone().unwrap_or_default();
if let Some(ref key) = provider.api_key {
headers
.entry("Authorization".to_owned())
.or_insert_with(|| format!("Bearer {key}"));
}
let mcp_config = McpServerConfig {
name: provider_name.to_owned(),
transport: McpServerTransport::Http {
url: url.to_owned(),
headers,
},
};
eprintln!("Connecting to {provider_name} ({url})...");
let conn = UpstreamConnection::connect(mcp_config, None)
.await
.map_err(|e| format!("failed to connect to '{provider_name}': {e}"))?;
let tools = conn.raw_tools().await;
if tools.is_empty() {
eprintln!("No tools discovered from '{provider_name}'.");
return Ok(());
}
eprintln!("Discovered {} tools from '{provider_name}'.", tools.len());
eprintln!();
println!("# Discovered from provider \"{provider_name}\"");
println!("# Paste under the `tools:` section of your bitrouter.yaml");
println!();
println!("tools:");
for tool in &tools {
println!(" {}:", tool.name);
println!(" endpoints:");
println!(" - provider: {provider_name}");
println!(" tool_id: {}", tool.name);
if let Some(ref desc) = tool.description {
let escaped = desc.replace('"', "\\\"");
println!(" description: \"{escaped}\"");
}
if tool.input_schema.is_object() && tool.input_schema.get("properties").is_some() {
println!(
" input_schema: {}",
serde_json::to_string(&tool.input_schema).unwrap_or_default()
);
}
}
Ok(())
}
#[cfg(not(feature = "mcp"))]
pub async fn run_discover(
_config: &bitrouter_config::BitrouterConfig,
_provider_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
Err("MCP feature is not enabled — cannot discover tools".into())
}