Skip to main content

roboticus_cli/cli/
mcp.rs

1//! `roboticus mcp` CLI subcommands.
2//!
3//! List, add, remove, and test MCP server connections.
4
5use super::*;
6
7// ── MCP list (authoritative server-management API) ───────────
8
9pub async fn cmd_mcp_list(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
10    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
11    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
12    let c = RoboticusClient::new(url)?;
13    let data = c.get("/api/mcp/servers").await.map_err(|e| {
14        RoboticusClient::check_connectivity_hint(&*e);
15        e
16    })?;
17    if json {
18        println!("{}", serde_json::to_string_pretty(&data)?);
19        return Ok(());
20    }
21    heading("MCP Servers");
22    let servers = data.as_array().cloned().unwrap_or_default();
23    if servers.is_empty() {
24        eprintln!("  {DIM}No MCP servers configured.{RESET}");
25    } else {
26        kv("Configured servers", &servers.len().to_string());
27        for server in &servers {
28            let name = server["name"].as_str().unwrap_or("?");
29            let enabled = server["enabled"].as_bool().unwrap_or(false);
30            let connected = server["connected"].as_bool().unwrap_or(false);
31            let tool_count = server["tool_count"].as_u64().unwrap_or(0);
32            let status = if !enabled {
33                format!("{YELLOW}disabled{RESET}")
34            } else if connected {
35                format!("{GREEN}connected{RESET}")
36            } else {
37                format!("{RED}disconnected{RESET}")
38            };
39            kv(name, &format!("{status}  tools={tool_count}"));
40        }
41    }
42    eprintln!();
43    Ok(())
44}
45
46// ── MCP add (prints TOML snippet) ────────────────────────────
47
48pub fn cmd_mcp_add(
49    name: &str,
50    stdio: Option<&str>,
51    sse: Option<&str>,
52    args: &[String],
53) -> Result<(), Box<dyn std::error::Error>> {
54    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
55    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
56    match (stdio, sse) {
57        (Some(command), None) => {
58            eprintln!();
59            eprintln!("  {ACCENT}Adding STDIO MCP server '{name}'{RESET}");
60            eprintln!("  {DIM}command:{RESET} {MONO}{command}{RESET}");
61            if !args.is_empty() {
62                let arg_str = args
63                    .iter()
64                    .map(|a| format!("\"{}\"", a))
65                    .collect::<Vec<_>>()
66                    .join(", ");
67                eprintln!("  {DIM}args:{RESET}    {MONO}[{arg_str}]{RESET}");
68            }
69            eprintln!();
70            eprintln!("  {DIM}Add the following to your {MONO}roboticus.toml{RESET}{DIM}:{RESET}");
71            eprintln!();
72            println!("[[mcp.servers]]");
73            println!("name = \"{name}\"");
74            println!("enabled = true");
75            println!("[mcp.servers.spec]");
76            println!("type = \"stdio\"");
77            println!("command = \"{command}\"");
78            if !args.is_empty() {
79                let arg_str = args
80                    .iter()
81                    .map(|a| format!("\"{}\"", a))
82                    .collect::<Vec<_>>()
83                    .join(", ");
84                println!("args = [{arg_str}]");
85            }
86            eprintln!();
87        }
88        (None, Some(url)) => {
89            eprintln!();
90            eprintln!("  {ACCENT}Adding SSE MCP server '{name}'{RESET}");
91            eprintln!("  {DIM}url:{RESET} {MONO}{url}{RESET}");
92            eprintln!();
93            eprintln!("  {DIM}Add the following to your {MONO}roboticus.toml{RESET}{DIM}:{RESET}");
94            eprintln!();
95            println!("[[mcp.servers]]");
96            println!("name = \"{name}\"");
97            println!("enabled = true");
98            println!("[mcp.servers.spec]");
99            println!("type = \"sse\"");
100            println!("url = \"{url}\"");
101            eprintln!();
102        }
103        _ => {
104            return Err("Specify either --stdio <command> or --sse <url>".into());
105        }
106    }
107    Ok(())
108}
109
110// ── MCP remove (prints guidance) ─────────────────────────────
111
112pub fn cmd_mcp_remove(name: &str) -> Result<(), Box<dyn std::error::Error>> {
113    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
114    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
115    eprintln!();
116    eprintln!("  {ACCENT}Remove MCP server '{name}'{RESET}");
117    eprintln!();
118    eprintln!(
119        "  Delete the {MONO}[[mcp.servers]]{RESET} block with {MONO}name = \"{name}\"{RESET}"
120    );
121    eprintln!("  from your {MONO}roboticus.toml{RESET}, then restart the daemon:");
122    eprintln!();
123    println!("  roboticus daemon restart");
124    eprintln!();
125    Ok(())
126}
127
128// ── MCP test (via running daemon) ────────────────────────────
129
130pub async fn cmd_mcp_test(url: &str, name: &str) -> Result<(), Box<dyn std::error::Error>> {
131    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
132    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
133    let c = RoboticusClient::new(url)?;
134    eprintln!();
135    eprintln!("  {DIM}Testing MCP server '{MONO}{name}{RESET}{DIM}'...{RESET}");
136    let endpoint = format!("/api/mcp/servers/{name}/test");
137    match c.post(&endpoint, serde_json::json!({})).await {
138        Ok(data) => {
139            let ok = data["success"].as_bool().unwrap_or(false);
140            if ok {
141                eprintln!("  {GREEN}{OK} Connection test: PASSED{RESET}");
142                let tool_count = data["tool_count"].as_u64().unwrap_or(0);
143                eprintln!("  {DIM}Tools discovered:{RESET} {tool_count}");
144            } else {
145                let err = data["detail"].as_str().unwrap_or("unknown error");
146                eprintln!("  {RED}{ERR} Connection test: FAILED{RESET}");
147                eprintln!("  {DIM}Error:{RESET} {err}");
148            }
149        }
150        Err(e) => {
151            RoboticusClient::check_connectivity_hint(&*e);
152            eprintln!("  {RED}{ERR} Connection test: FAILED{RESET}");
153            eprintln!("  {DIM}Error:{RESET} {e}");
154        }
155    }
156    eprintln!();
157    Ok(())
158}