lean_ctx/tools/
ctx_tools.rs1use serde_json::{Map, Value};
9
10use crate::core::config::Config;
11use crate::core::gateway;
12
13const DISABLED_HINT: &str = "gateway is disabled. Enable it in ~/.lean-ctx/config.toml:\n\
14 [gateway]\n\
15 enabled = true\n\n\
16 [[gateway.servers]]\n\
17 name = \"fs\"\n\
18 transport = \"stdio\"\n\
19 command = \"mcp-server-filesystem\"\n\
20 args = [\"/path/to/dir\"]";
21
22pub fn run(args: &Map<String, Value>) -> Result<String, String> {
24 let cfg = Config::load();
25 if !cfg.gateway.enabled_effective() {
26 return Err(DISABLED_HINT.to_string());
27 }
28 if cfg.gateway.active_servers().next().is_none() {
29 return Err(
30 "gateway is enabled but no downstream servers are configured \
31 (add one or more [[gateway.servers]] entries)."
32 .to_string(),
33 );
34 }
35
36 let action = args.get("action").and_then(Value::as_str).unwrap_or("find");
37 let rt = tokio::runtime::Handle::current();
38
39 match action {
40 "find" => {
41 let query = args
42 .get("query")
43 .and_then(Value::as_str)
44 .unwrap_or("")
45 .to_string();
46 let outcome = rt.block_on(gateway::find(&cfg.gateway, &query));
47 Ok(gateway::render_cards(&outcome))
48 }
49 "list" => Ok(rt.block_on(gateway::servers_overview(&cfg.gateway))),
50 "refresh" => {
51 gateway::catalog::invalidate();
52 let outcome = rt.block_on(gateway::find(&cfg.gateway, ""));
53 Ok(format!(
54 "gateway catalog refreshed.\n\n{}",
55 gateway::render_cards(&outcome)
56 ))
57 }
58 "call" => {
59 let tool = args.get("tool").and_then(Value::as_str).ok_or_else(|| {
60 "call requires 'tool' — a `server::tool` handle from `ctx_tools find`".to_string()
61 })?;
62 let arguments = match args.get("arguments") {
63 Some(Value::Object(m)) => m.clone(),
64 None | Some(Value::Null) => Map::new(),
65 Some(_) => return Err("'arguments' must be a JSON object".to_string()),
66 };
67 rt.block_on(gateway::proxy(&cfg.gateway, tool, arguments))
68 }
69 other => Err(format!(
70 "invalid action '{other}' (use: find, call, list, refresh)"
71 )),
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use serde_json::json;
79
80 #[test]
84 fn disabled_gateway_returns_hint() {
85 crate::test_env::remove_var("LEAN_CTX_GATEWAY");
87 let rt = tokio::runtime::Builder::new_multi_thread()
88 .enable_all()
89 .build()
90 .unwrap();
91 let args = json!({ "action": "find", "query": "anything" });
92 let out =
93 rt.block_on(async { tokio::task::block_in_place(|| run(args.as_object().unwrap())) });
94 if let Err(e) = out {
98 assert!(e.contains("gateway is disabled") || e.contains("no downstream"));
99 }
100 }
101}