lean_ctx_server/server/
schema_hook.rs1use std::borrow::Cow;
5
6use lean_ctx::core::context_kernel::list_tools_opt;
7
8#[must_use]
10pub fn optimize_tools(tools: Vec<rmcp::model::Tool>, client_name: &str) -> Vec<rmcp::model::Tool> {
11 if !list_tools_opt::should_optimize_for_client(client_name) {
12 return tools;
13 }
14
15 let tuples = tools
16 .iter()
17 .map(|tool| {
18 let parameter_count = tool
19 .input_schema
20 .get("properties")
21 .and_then(serde_json::Value::as_object)
22 .map_or(0, serde_json::Map::len);
23 (
24 tool.name.to_string(),
25 tool.description.as_deref().unwrap_or("").to_owned(),
26 parameter_count,
27 )
28 })
29 .collect();
30 let mut optimized: std::collections::HashMap<String, String> =
34 list_tools_opt::optimize_descriptions(tuples, client_name)
35 .into_iter()
36 .map(|(name, description, _)| (name, description))
37 .collect();
38
39 tools
40 .into_iter()
41 .map(|mut tool| {
42 if let Some(description) = optimized.remove(tool.name.as_ref()) {
43 tool.description = Some(Cow::Owned(description));
44 }
45 tool
46 })
47 .collect()
48}
49
50#[must_use]
52pub fn summary() -> list_tools_opt::SchemaOptSummary {
53 list_tools_opt::schema_opt_summary()
54}
55
56#[cfg(test)]
57mod tests {
58 use std::sync::MutexGuard;
59
60 use super::list_tools_opt;
61 use lean_ctx::core::context_kernel::kernel_config::{self, KERNEL_TEST_LOCK, KernelFeatures};
62
63 fn setup() -> MutexGuard<'static, ()> {
64 let guard = KERNEL_TEST_LOCK
65 .lock()
66 .unwrap_or_else(std::sync::PoisonError::into_inner);
67 kernel_config::reset_features();
68 list_tools_opt::reset();
69 guard
70 }
71
72 #[test]
73 fn disabled_returns_same_count() {
74 let _guard = setup();
75 let features = KernelFeatures {
76 enabled: false,
77 ..KernelFeatures::default()
78 };
79 kernel_config::update_features(features);
80 let tools = vec![("tool".to_owned(), "description".to_owned(), 1)];
81
82 assert_eq!(
83 list_tools_opt::optimize_descriptions(tools, "cursor").len(),
84 1
85 );
86 }
87
88 #[test]
89 fn summary_works() {
90 let _guard = setup();
91 let summary = list_tools_opt::schema_opt_summary();
92
93 assert_eq!(summary.optimizations_applied, 0);
94 assert_eq!(summary.total_tokens_saved, 0);
95 assert_eq!(summary.avg_reduction_percent, 0.0);
96 }
97
98 #[test]
101 fn descriptions_are_paired_by_name_and_no_tool_is_lost() {
102 let _guard = setup();
103 let tools: Vec<rmcp::model::Tool> = (0..40)
104 .map(|i| {
105 let description = format!("tool-{i}: {}", "x".repeat(8_000 - i * 100));
108 rmcp::model::Tool::new(
109 format!("tool_{i}"),
110 description,
111 std::sync::Arc::new(serde_json::Map::new()),
112 )
113 })
114 .collect();
115
116 let optimized = super::optimize_tools(tools, "cursor");
117
118 assert_eq!(optimized.len(), 40, "no tool may be dropped from the list");
119 for (i, tool) in optimized.iter().enumerate() {
120 assert_eq!(tool.name, format!("tool_{i}"));
121 let description = tool.description.as_deref().unwrap_or("");
122 assert!(
123 description.starts_with(&format!("tool-{i}: ")),
124 "{} got a foreign description: {}",
125 tool.name,
126 &description[..description.len().min(20)]
127 );
128 }
129 }
130
131 #[test]
132 fn optimization_flag_check() {
133 let _guard = setup();
134
135 assert!(list_tools_opt::should_optimize_for_client("cursor"));
136 assert!(!list_tools_opt::should_optimize_for_client(
137 "unknown-client"
138 ));
139 }
140}