lean_ctx/tool_defs/
mod.rs1use std::sync::Arc;
2
3use rmcp::model::Tool;
4use serde_json::{Map, Value};
5
6mod granular;
7pub use granular::{granular_tool_defs, unified_tool_defs};
8
9pub fn tool_def(name: &'static str, description: &'static str, schema_value: Value) -> Tool {
10 let mut schema: Map<String, Value> = match schema_value {
11 Value::Object(map) => map,
12 _ => Map::new(),
13 };
14 normalize_for_strict_validators(&mut schema);
15 Tool::new(name, description, Arc::new(schema))
16}
17
18pub fn normalize_for_strict_validators(schema: &mut Map<String, Value>) {
36 let is_object = schema.get("type").and_then(Value::as_str) == Some("object");
37 let is_array = schema.get("type").and_then(Value::as_str) == Some("array");
38
39 if is_object && schema.contains_key("properties") && !schema.contains_key("required") {
40 schema.insert("required".into(), Value::Array(Vec::new()));
41 }
42 if is_array && !schema.contains_key("items") {
43 schema.insert("items".into(), Value::Object(Map::new()));
44 }
45
46 if let Some(Value::Object(props)) = schema.get_mut("properties") {
47 for prop in props.values_mut() {
48 if let Value::Object(p) = prop {
49 normalize_for_strict_validators(p);
50 }
51 }
52 }
53 if let Some(Value::Object(items)) = schema.get_mut("items") {
54 normalize_for_strict_validators(items);
55 }
56 if let Some(Value::Object(ap)) = schema.get_mut("additionalProperties") {
57 normalize_for_strict_validators(ap);
58 }
59 for combinator in ["anyOf", "oneOf", "allOf"] {
60 if let Some(Value::Array(branches)) = schema.get_mut(combinator) {
61 for branch in branches.iter_mut() {
62 if let Value::Object(b) = branch {
63 normalize_for_strict_validators(b);
64 }
65 }
66 }
67 }
68}
69
70pub const CORE_TOOL_NAMES: &[&str] = &[
71 "ctx_read",
72 "ctx_search",
73 "ctx_semantic_search",
74 "ctx_shell",
75 "shell",
76 "ctx_tree",
77 "ctx_edit",
78 "ctx_session",
79 "ctx_knowledge",
80 "ctx_overview",
81 "ctx_graph",
82 "ctx_call",
83 "ctx_provider",
84 "ctx_expand",
85];
86
87pub fn core_tool_names() -> &'static [&'static str] {
88 CORE_TOOL_NAMES
89}
90
91pub fn lazy_tool_defs() -> Vec<Tool> {
92 let all = granular_tool_defs();
93 all.into_iter()
94 .filter(|t| CORE_TOOL_NAMES.contains(&t.name.as_ref()))
95 .collect()
96}
97
98pub fn discover_tools(query: &str) -> String {
99 let all = crate::server::registry::build_registry().tool_defs();
102 let query_lower = query.to_lowercase();
103 let matches: Vec<(String, String)> = all
104 .iter()
105 .filter_map(|t| {
106 let name = t.name.as_ref();
107 let desc = t.description.as_deref().unwrap_or("");
108 if name.to_lowercase().contains(&query_lower)
109 || desc.to_lowercase().contains(&query_lower)
110 {
111 Some((name.to_string(), desc.to_string()))
112 } else {
113 None
114 }
115 })
116 .collect();
117
118 if matches.is_empty() {
119 return format!(
120 "No tools found matching '{query}'. Try broader terms like: graph, cost, session, search, compress, agent, workflow, gain."
121 );
122 }
123
124 let mut out = format!("{} tools matching '{query}':\n", matches.len());
125 for (name, desc) in &matches {
126 let first = desc.lines().next().unwrap_or(desc);
128 let short = if first.len() > 80 {
129 &first[..first.floor_char_boundary(80)]
130 } else {
131 first
132 };
133 out.push_str(&format!(" {name} — {short}\n"));
134 }
135 out.push_str(
136 "\nIf your MCP client registers tools only once at startup (static tools/list), \
137use ctx_call (available in lazy mode) to invoke discovered tools:\n\
138 ctx_call {\"name\":\"ctx_graph\",\"arguments\":{\"action\":\"status\"}}\n",
139 );
140 out
141}
142
143pub fn is_full_mode() -> bool {
144 std::env::var("LEAN_CTX_FULL_TOOLS").is_ok_and(|v| v != "0" && !v.eq_ignore_ascii_case("false"))
145 || std::env::var("LEAN_CTX_LAZY_TOOLS")
146 .is_ok_and(|v| v == "0" || v.eq_ignore_ascii_case("false"))
147}