lean_ctx_server/server/
registry.rs1use std::collections::HashMap;
3use std::sync::Arc;
4
5use rmcp::model::Tool;
6
7use super::tool_trait::McpTool;
8
9pub struct ToolRegistry {
19 tools: HashMap<&'static str, Arc<dyn McpTool>>,
20}
21
22impl ToolRegistry {
23 pub fn new() -> Self {
24 Self {
25 tools: HashMap::new(),
26 }
27 }
28
29 pub fn register(&mut self, tool: Box<dyn McpTool>) {
30 let name = tool.name();
31 self.tools.insert(name, Arc::from(tool));
32 }
33
34 pub fn get(&self, name: &str) -> Option<&dyn McpTool> {
35 self.tools.get(name).map(|t| &**t)
36 }
37
38 pub fn get_arc(&self, name: &str) -> Option<Arc<dyn McpTool>> {
44 self.tools.get(name).cloned()
45 }
46
47 pub fn contains(&self, name: &str) -> bool {
48 self.tools.contains_key(name)
49 }
50
51 pub fn static_name(&self, name: &str) -> Option<&'static str> {
55 self.tools.get_key_value(name).map(|(key, _)| *key)
56 }
57
58 pub fn tool_defs(&self) -> Vec<Tool> {
66 self.tool_defs_filtered(false)
67 }
68
69 pub fn registered_tool_defs(&self) -> Vec<Tool> {
73 self.tool_defs_filtered(true)
74 }
75
76 fn tool_defs_filtered(&self, include_non_public: bool) -> Vec<Tool> {
79 let mut defs: Vec<Tool> = self
80 .tools
81 .values()
82 .filter(|t| {
83 include_non_public || super::dynamic_tools::is_publicly_advertised_tool(t.name())
84 })
85 .map(|t| t.tool_def())
86 .collect();
87 defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
88 lean_ctx::tool_defs::apply_tool_annotations(defs)
89 }
90
91 pub fn active_tool_defs(&self) -> Vec<Tool> {
94 let Ok(state) = super::dynamic_tools::global().lock() else {
95 tracing::warn!("dynamic_tools mutex poisoned in active_tool_defs; returning all");
96 return self.tool_defs();
97 };
98 let mut defs: Vec<Tool> = self
99 .tools
100 .values()
101 .filter(|t| {
102 state.is_tool_active(t.name())
103 && super::dynamic_tools::is_publicly_advertised_tool(t.name())
104 })
105 .map(|t| t.tool_def())
106 .collect();
107 defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
108 lean_ctx::tool_defs::apply_tool_annotations(defs)
109 }
110
111 pub fn profile_tool_defs(
114 &self,
115 profile: &lean_ctx::core::tool_profiles::ToolProfile,
116 ) -> Vec<Tool> {
117 let mut defs: Vec<Tool> = self
118 .tools
119 .values()
120 .filter(|t| {
121 profile.is_tool_enabled(t.name())
122 && super::dynamic_tools::is_publicly_advertised_tool(t.name())
123 })
124 .map(|t| t.tool_def())
125 .collect();
126 defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
127 lean_ctx::tool_defs::apply_tool_annotations(defs)
128 }
129
130 pub fn len(&self) -> usize {
131 self.tools.len()
132 }
133
134 pub fn is_empty(&self) -> bool {
135 self.tools.is_empty()
136 }
137
138 pub fn names(&self) -> Vec<&'static str> {
139 let mut names: Vec<_> = self.tools.keys().copied().collect();
140 names.sort_unstable();
141 names
142 }
143}
144
145impl Default for ToolRegistry {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151pub fn tool_count() -> usize {
155 build_registry().tool_defs().len()
156}
157
158pub fn register_tool_catalog() {
163 lean_ctx::core::tool_catalog::register_tool_catalog(
164 lean_ctx::core::tool_catalog::ToolCatalog {
165 tool_defs: || build_registry().tool_defs(),
166 registered_tool_defs: || build_registry().registered_tool_defs(),
167 },
168 );
169}
170
171pub fn build_registry() -> ToolRegistry {
174 let mut registry = ToolRegistry::new();
175
176 use crate::tools::registered;
177 registry.register(Box::new(registered::ctx_tree::CtxTreeTool));
178 registry.register(Box::new(registered::ctx_discover::CtxDiscoverTool));
179 registry.register(Box::new(registered::ctx_heatmap::CtxHeatmapTool));
180 registry.register(Box::new(registered::ctx_verify::CtxVerifyTool));
181 registry.register(Box::new(registered::ctx_outline::CtxOutlineTool));
182 registry.register(Box::new(registered::ctx_cost::CtxCostTool));
183 registry.register(Box::new(registered::ctx_gain::CtxGainTool));
184 registry.register(Box::new(registered::ctx_expand::CtxExpandTool));
185 registry.register(Box::new(registered::ctx_call::CtxCallTool));
186 registry.register(Box::new(registered::ctx_callgraph::CtxCallgraphTool));
187 registry.register(Box::new(
188 registered::ctx_discover_tools::CtxDiscoverToolsTool,
189 ));
190 registry.register(Box::new(registered::ctx_tools::CtxToolsTool));
191 registry.register(Box::new(registered::ctx_provider::CtxProviderTool));
192 registry.register(Box::new(registered::ctx_doc::CtxDocTool));
193 registry.register(Box::new(registered::ctx_pack::CtxPackTool));
194 registry.register(Box::new(registered::ctx_index::CtxIndexTool));
195 registry.register(Box::new(registered::ctx_artifacts::CtxArtifactsTool));
196 registry.register(Box::new(
197 registered::ctx_compress_memory::CtxCompressMemoryTool,
198 ));
199 registry.register(Box::new(registered::ctx_read::CtxReadTool));
200 registry.register(Box::new(registered::ctx_multi_repo::CtxMultiRepoTool));
201 registry.register(Box::new(registered::ctx_delta::CtxDeltaTool));
202 registry.register(Box::new(registered::ctx_edit::CtxEditTool));
203 registry.register(Box::new(registered::ctx_patch::CtxPatchTool));
204 registry.register(Box::new(registered::ctx_glob::CtxGlobTool));
205 registry.register(Box::new(registered::ctx_shell::CtxShellTool));
206 registry.register(Box::new(registered::ctx_shape::CtxShapeTool));
207 registry.register(Box::new(registered::shell_alias::ShellAliasTool));
208 registry.register(Box::new(registered::ctx_search::CtxSearchTool));
209 registry.register(Box::new(registered::ctx_url_read::CtxUrlReadTool));
210 registry.register(Box::new(registered::ctx_git_read::CtxGitReadTool));
211 registry.register(Box::new(registered::ctx_checkpoint::CtxCheckpointTool));
212 registry.register(Box::new(registered::ctx_compose::CtxComposeTool));
213 registry.register(Box::new(registered::ctx_memory::CtxMemoryTool));
214 registry.register(Box::new(registered::ctx_execute::CtxExecuteTool));
215
216 registry.register(Box::new(registered::ctx_compress::CtxCompressTool));
218 registry.register(Box::new(registered::ctx_metrics::CtxMetricsTool));
219 registry.register(Box::new(registered::ctx_radar::CtxRadarTool));
220 registry.register(Box::new(registered::ctx_dedup::CtxDedupTool));
221 registry.register(Box::new(registered::ctx_context::CtxContextTool));
222 registry.register(Box::new(registered::ctx_graph::CtxGraphTool));
223 registry.register(Box::new(registered::ctx_proof::CtxProofTool));
224 registry.register(Box::new(registered::ctx_preload::CtxPreloadTool));
225 registry.register(Box::new(registered::ctx_prefetch::CtxPrefetchTool));
226 registry.register(Box::new(registered::ctx_feedback::CtxFeedbackTool));
227 registry.register(Box::new(registered::ctx_control::CtxControlTool));
228
229 registry.register(Box::new(registered::ctx_session::CtxSessionTool));
231 registry.register(Box::new(registered::ctx_knowledge::CtxKnowledgeTool));
232 registry.register(Box::new(registered::ctx_agent::CtxAgentTool));
233 registry.register(Box::new(registered::ctx_share::CtxShareTool));
234 registry.register(Box::new(
235 registered::ctx_transcript_compact::CtxTranscriptCompactTool,
236 ));
237 registry.register(Box::new(registered::ctx_package::CtxPackageTool));
238 registry.register(Box::new(registered::ctx_handoff::CtxHandoffTool));
239 registry.register(Box::new(registered::ctx_work_graph::CtxWorkGraphTool));
240 registry.register(Box::new(registered::ctx_tasks::CtxTasksTool));
241 registry.register(Box::new(registered::ctx_load_tools::CtxLoadToolsTool));
242
243 registry
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn get_arc_returns_owned_handle_for_known_tool() {
255 let registry = build_registry();
256 let arc = registry.get_arc("ctx_tree");
257 assert!(arc.is_some(), "ctx_tree must be registered");
258 assert_eq!(arc.unwrap().name(), "ctx_tree");
259 }
260
261 #[test]
262 fn work_graph_is_registered_and_publicly_advertised() {
263 let registry = build_registry();
264 assert!(registry.contains("ctx_work_graph"));
265 let tools = registry.tool_defs();
266 let tool = tools
267 .iter()
268 .find(|tool| tool.name.as_ref() == "ctx_work_graph")
269 .expect("work graph must be advertised");
270 assert_eq!(
271 tool.input_schema.get("additionalProperties"),
272 Some(&serde_json::Value::Bool(false))
273 );
274 let actions = tool.input_schema["properties"]["action"]["enum"]
275 .as_array()
276 .expect("action enum");
277 assert!(actions.iter().any(|action| action == "start"));
278 assert!(!actions.iter().any(|action| action == "execute"));
279 assert!(
280 tool.input_schema["properties"]
281 .get("execution_fence")
282 .is_some()
283 );
284 assert!(tool.input_schema["properties"].get("connector").is_none());
285 }
286
287 #[test]
290 fn every_registered_tool_is_a_known_wire_tool() {
291 use lean_ctx_protocol::hive::metrics_v2::KNOWN_TOOLS;
292 let missing: Vec<&str> = build_registry()
293 .names()
294 .into_iter()
295 .filter(|name| !KNOWN_TOOLS.contains(name))
296 .collect();
297 assert!(
298 missing.is_empty(),
299 "add these registered tools to metrics_v2::KNOWN_TOOLS: {missing:?}"
300 );
301 }
302
303 #[test]
304 fn get_arc_is_none_for_unknown_tool() {
305 let registry = build_registry();
306 assert!(registry.get_arc("ctx_does_not_exist_xyz").is_none());
307 }
308
309 #[test]
310 fn get_and_get_arc_agree_for_core_tool() {
311 let registry = build_registry();
312 assert_eq!(
313 registry.get("ctx_read").is_some(),
314 registry.get_arc("ctx_read").is_some(),
315 "get and get_arc must agree on tool presence"
316 );
317 }
318
319 #[test]
320 fn local_collaboration_tools_are_hidden_but_remain_directly_callable() {
321 let registry = build_registry();
322 let public_defs = registry.tool_defs();
323 let public_names: Vec<_> = public_defs.iter().map(|tool| tool.name.as_ref()).collect();
324 let power_defs =
325 registry.profile_tool_defs(&lean_ctx::core::tool_profiles::ToolProfile::Power);
326 let power_names: Vec<_> = power_defs.iter().map(|tool| tool.name.as_ref()).collect();
327 let registered_defs = registry.registered_tool_defs();
328 let registered_names: Vec<_> = registered_defs
329 .iter()
330 .map(|tool| tool.name.as_ref())
331 .collect();
332
333 for name in crate::server::dynamic_tools::LOCAL_COLLABORATION_COMPATIBILITY_TOOLS {
334 assert!(
335 !public_names.contains(name),
336 "{name} must stay out of the public tool surface"
337 );
338 assert!(
339 !power_names.contains(name),
340 "{name} must stay out of an explicit Power advertisement"
341 );
342 assert!(
343 registered_names.contains(name),
344 "{name} must remain in the direct compatibility registry"
345 );
346 assert!(
347 registry.get(name).is_some(),
348 "{name} must remain directly callable"
349 );
350 }
351 }
352}