roboticus-cli 0.11.3

CLI commands and migration engine for the Roboticus agent runtime
Documentation
//! `roboticus mcp` CLI subcommands.
//!
//! List, add, remove, and test MCP server connections.

use super::*;

// ── MCP list (authoritative server-management API) ───────────

pub async fn cmd_mcp_list(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
    let c = RoboticusClient::new(url)?;
    let data = c.get("/api/mcp/servers").await.map_err(|e| {
        RoboticusClient::check_connectivity_hint(&*e);
        e
    })?;
    if json {
        println!("{}", serde_json::to_string_pretty(&data)?);
        return Ok(());
    }
    heading("MCP Servers");
    let servers = data.as_array().cloned().unwrap_or_default();
    if servers.is_empty() {
        eprintln!("  {DIM}No MCP servers configured.{RESET}");
    } else {
        kv("Configured servers", &servers.len().to_string());
        for server in &servers {
            let name = server["name"].as_str().unwrap_or("?");
            let enabled = server["enabled"].as_bool().unwrap_or(false);
            let connected = server["connected"].as_bool().unwrap_or(false);
            let tool_count = server["tool_count"].as_u64().unwrap_or(0);
            let status = if !enabled {
                format!("{YELLOW}disabled{RESET}")
            } else if connected {
                format!("{GREEN}connected{RESET}")
            } else {
                format!("{RED}disconnected{RESET}")
            };
            kv(name, &format!("{status}  tools={tool_count}"));
        }
    }
    eprintln!();
    Ok(())
}

// ── MCP add (prints TOML snippet) ────────────────────────────

pub fn cmd_mcp_add(
    name: &str,
    stdio: Option<&str>,
    sse: Option<&str>,
    args: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
    match (stdio, sse) {
        (Some(command), None) => {
            eprintln!();
            eprintln!("  {ACCENT}Adding STDIO MCP server '{name}'{RESET}");
            eprintln!("  {DIM}command:{RESET} {MONO}{command}{RESET}");
            if !args.is_empty() {
                let arg_str = args
                    .iter()
                    .map(|a| format!("\"{}\"", a))
                    .collect::<Vec<_>>()
                    .join(", ");
                eprintln!("  {DIM}args:{RESET}    {MONO}[{arg_str}]{RESET}");
            }
            eprintln!();
            eprintln!("  {DIM}Add the following to your {MONO}roboticus.toml{RESET}{DIM}:{RESET}");
            eprintln!();
            println!("[[mcp.servers]]");
            println!("name = \"{name}\"");
            println!("enabled = true");
            println!("[mcp.servers.spec]");
            println!("type = \"stdio\"");
            println!("command = \"{command}\"");
            if !args.is_empty() {
                let arg_str = args
                    .iter()
                    .map(|a| format!("\"{}\"", a))
                    .collect::<Vec<_>>()
                    .join(", ");
                println!("args = [{arg_str}]");
            }
            eprintln!();
        }
        (None, Some(url)) => {
            eprintln!();
            eprintln!("  {ACCENT}Adding SSE MCP server '{name}'{RESET}");
            eprintln!("  {DIM}url:{RESET} {MONO}{url}{RESET}");
            eprintln!();
            eprintln!("  {DIM}Add the following to your {MONO}roboticus.toml{RESET}{DIM}:{RESET}");
            eprintln!();
            println!("[[mcp.servers]]");
            println!("name = \"{name}\"");
            println!("enabled = true");
            println!("[mcp.servers.spec]");
            println!("type = \"sse\"");
            println!("url = \"{url}\"");
            eprintln!();
        }
        _ => {
            return Err("Specify either --stdio <command> or --sse <url>".into());
        }
    }
    Ok(())
}

// ── MCP remove (prints guidance) ─────────────────────────────

pub fn cmd_mcp_remove(name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
    eprintln!();
    eprintln!("  {ACCENT}Remove MCP server '{name}'{RESET}");
    eprintln!();
    eprintln!(
        "  Delete the {MONO}[[mcp.servers]]{RESET} block with {MONO}name = \"{name}\"{RESET}"
    );
    eprintln!("  from your {MONO}roboticus.toml{RESET}, then restart the daemon:");
    eprintln!();
    println!("  roboticus daemon restart");
    eprintln!();
    Ok(())
}

// ── MCP test (via running daemon) ────────────────────────────

pub async fn cmd_mcp_test(url: &str, name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
    let c = RoboticusClient::new(url)?;
    eprintln!();
    eprintln!("  {DIM}Testing MCP server '{MONO}{name}{RESET}{DIM}'...{RESET}");
    let endpoint = format!("/api/mcp/servers/{name}/test");
    match c.post(&endpoint, serde_json::json!({})).await {
        Ok(data) => {
            let ok = data["success"].as_bool().unwrap_or(false);
            if ok {
                eprintln!("  {GREEN}{OK} Connection test: PASSED{RESET}");
                let tool_count = data["tool_count"].as_u64().unwrap_or(0);
                eprintln!("  {DIM}Tools discovered:{RESET} {tool_count}");
            } else {
                let err = data["detail"].as_str().unwrap_or("unknown error");
                eprintln!("  {RED}{ERR} Connection test: FAILED{RESET}");
                eprintln!("  {DIM}Error:{RESET} {err}");
            }
        }
        Err(e) => {
            RoboticusClient::check_connectivity_hint(&*e);
            eprintln!("  {RED}{ERR} Connection test: FAILED{RESET}");
            eprintln!("  {DIM}Error:{RESET} {e}");
        }
    }
    eprintln!();
    Ok(())
}