lean_ctx_server/tools/
ctx_tools.rs1use serde_json::{Map, Value};
12
13use lean_ctx::core::config::Config;
14use lean_ctx::core::mcp_catalog;
15
16const DISABLED_HINT: &str = "gateway is disabled. Enable it in ~/.lean-ctx/config.toml:\n\
17 [gateway]\n\
18 enabled = true\n\n\
19 [[gateway.servers]]\n\
20 name = \"fs\"\n\
21 transport = \"stdio\"\n\
22 command = \"mcp-server-filesystem\"\n\
23 args = [\"/path/to/dir\"]";
24
25use mcp_catalog::runtime::block_on;
26
27pub fn run(args: &Map<String, Value>, project_root: &str) -> Result<String, String> {
33 let cfg = Config::load();
34 if !cfg.gateway.enabled_effective() {
35 return Err(DISABLED_HINT.to_string());
36 }
37 if cfg.gateway.active_servers().next().is_none() {
38 return Err(
39 "gateway is enabled but no downstream servers are configured \
40 (add one or more [[gateway.servers]] entries)."
41 .to_string(),
42 );
43 }
44
45 mcp_catalog::adapters::compression::ensure_registered(&cfg.gateway);
48
49 let action = args.get("action").and_then(Value::as_str).unwrap_or("find");
50
51 match action {
52 "find" => {
53 let query = args
54 .get("query")
55 .and_then(Value::as_str)
56 .unwrap_or("")
57 .to_string();
58 let outcome = block_on(mcp_catalog::find(&cfg.gateway, &query))?;
59 Ok(mcp_catalog::render_cards(&outcome))
60 }
61 "list" => block_on(mcp_catalog::servers_overview(&cfg.gateway)),
62 "refresh" => {
63 mcp_catalog::catalog::invalidate();
64 let outcome = block_on(mcp_catalog::find(&cfg.gateway, ""))?;
65 Ok(format!(
66 "gateway catalog refreshed.\n\n{}",
67 mcp_catalog::render_cards(&outcome)
68 ))
69 }
70 "call" => {
71 let tool = args.get("tool").and_then(Value::as_str).ok_or_else(|| {
72 "call requires 'tool' — a `server::tool` handle from `ctx_tools find`".to_string()
73 })?;
74 let arguments = match args.get("arguments") {
75 Some(Value::Object(m)) => m.clone(),
76 None | Some(Value::Null) => Map::new(),
77 Some(_) => return Err("'arguments' must be a JSON object".to_string()),
78 };
79 lean_ctx_hive::metrics_v2::signals::mcp_relay_call(tool);
81 block_on(mcp_catalog::proxy(
82 &cfg.gateway,
83 tool,
84 arguments,
85 project_root,
86 ))?
87 }
88 other => Err(format!(
89 "invalid action '{other}' (use: find, call, list, refresh)"
90 )),
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use serde_json::json;
98
99 #[test]
103 fn disabled_gateway_returns_hint() {
104 let _env_lock = lean_ctx::core::data_dir::test_env_lock();
105 lean_ctx::test_env::remove_var("LEAN_CTX_GATEWAY");
107 let rt = tokio::runtime::Builder::new_multi_thread()
108 .enable_all()
109 .build()
110 .unwrap();
111 let args = json!({ "action": "find", "query": "anything" });
112 let out = rt
113 .block_on(async { tokio::task::block_in_place(|| run(args.as_object().unwrap(), "")) });
114 if let Err(e) = out {
118 assert!(e.contains("gateway is disabled") || e.contains("no downstream"));
119 }
120 }
121
122 #[test]
127 fn run_without_ambient_runtime_does_not_panic() {
128 let _env_lock = lean_ctx::core::data_dir::test_env_lock();
129 lean_ctx::test_env::remove_var("LEAN_CTX_GATEWAY");
130 let args = json!({ "action": "list" });
131 let _ = run(args.as_object().unwrap(), ""); }
133}