Skip to main content

lean_ctx/server/
registry.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use rmcp::model::Tool;
5
6use super::tool_trait::McpTool;
7
8/// Central registry mapping tool names to their trait-based handlers.
9/// Every tool is trait-based and resolved here; the earlier
10/// match-cascade dispatch has been fully retired.
11///
12/// Handlers are stored behind `Arc` (not `Box`) so the dispatch layer can hand
13/// an owned, `'static` handle to `tokio::task::spawn_blocking`. That lets a
14/// blocking handler run on the dedicated blocking pool under a watchdog
15/// deadline instead of pinning a scarce core worker via `block_in_place`
16/// (#271 — a hung handler must never swallow the JSON-RPC response).
17pub struct ToolRegistry {
18    tools: HashMap<&'static str, Arc<dyn McpTool>>,
19}
20
21impl ToolRegistry {
22    pub fn new() -> Self {
23        Self {
24            tools: HashMap::new(),
25        }
26    }
27
28    pub fn register(&mut self, tool: Box<dyn McpTool>) {
29        let name = tool.name();
30        self.tools.insert(name, Arc::from(tool));
31    }
32
33    pub fn get(&self, name: &str) -> Option<&dyn McpTool> {
34        self.tools.get(name).map(|t| &**t)
35    }
36
37    /// Clone an owned, `'static` handle to a registered tool.
38    ///
39    /// Unlike [`get`](Self::get), the returned `Arc` can be moved into
40    /// `spawn_blocking`, so the dispatch layer can execute the (synchronous)
41    /// handler off the async core workers under a watchdog (#271).
42    pub fn get_arc(&self, name: &str) -> Option<Arc<dyn McpTool>> {
43        self.tools.get(name).cloned()
44    }
45
46    pub fn contains(&self, name: &str) -> bool {
47        self.tools.contains_key(name)
48    }
49
50    /// Returns MCP Tool definitions for all registered tools.
51    /// Used by `list_tools` to expose schemas to clients.
52    /// Applies MCP `ToolAnnotations` (readOnlyHint, destructiveHint) so clients
53    /// can make informed decisions about tool usage in restricted contexts.
54    pub fn tool_defs(&self) -> Vec<Tool> {
55        let mut defs: Vec<Tool> = self.tools.values().map(|t| t.tool_def()).collect();
56        defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
57        crate::tool_defs::apply_tool_annotations(defs)
58    }
59
60    /// Returns tool definitions filtered by the dynamic tool state.
61    /// Only includes tools whose category is currently active.
62    pub fn active_tool_defs(&self) -> Vec<Tool> {
63        let Ok(state) = super::dynamic_tools::global().lock() else {
64            tracing::warn!("dynamic_tools mutex poisoned in active_tool_defs; returning all");
65            return self.tool_defs();
66        };
67        let mut defs: Vec<Tool> = self
68            .tools
69            .values()
70            .filter(|t| state.is_tool_active(t.name()))
71            .map(|t| t.tool_def())
72            .collect();
73        defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
74        crate::tool_defs::apply_tool_annotations(defs)
75    }
76
77    /// Returns tool definitions filtered by a tool profile.
78    /// Only includes tools whose name is enabled by the given profile.
79    pub fn profile_tool_defs(
80        &self,
81        profile: &crate::core::tool_profiles::ToolProfile,
82    ) -> Vec<Tool> {
83        let mut defs: Vec<Tool> = self
84            .tools
85            .values()
86            .filter(|t| profile.is_tool_enabled(t.name()))
87            .map(|t| t.tool_def())
88            .collect();
89        defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
90        crate::tool_defs::apply_tool_annotations(defs)
91    }
92
93    pub fn len(&self) -> usize {
94        self.tools.len()
95    }
96
97    pub fn is_empty(&self) -> bool {
98        self.tools.is_empty()
99    }
100
101    pub fn names(&self) -> Vec<&'static str> {
102        let mut names: Vec<_> = self.tools.keys().copied().collect();
103        names.sort_unstable();
104        names
105    }
106}
107
108impl Default for ToolRegistry {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114/// Number of registered MCP tools — the single source of truth for the
115/// "N MCP tools" count shown in `--help`, the README, and the feature catalog.
116/// Deriving it here means the count can never drift from the actual registry.
117pub fn tool_count() -> usize {
118    build_registry().len()
119}
120
121/// Register all trait-based tools. Called once during server startup.
122/// New tools are added here as their `McpTool` implementation lands.
123pub fn build_registry() -> ToolRegistry {
124    let mut registry = ToolRegistry::new();
125
126    use crate::tools::registered;
127    registry.register(Box::new(registered::ctx_tree::CtxTreeTool));
128    registry.register(Box::new(registered::ctx_benchmark::CtxBenchmarkTool));
129    registry.register(Box::new(registered::ctx_quality_lab::CtxQualityLabTool));
130    registry.register(Box::new(registered::ctx_analyze::CtxAnalyzeTool));
131    registry.register(Box::new(registered::ctx_discover::CtxDiscoverTool));
132    registry.register(Box::new(registered::ctx_response::CtxResponseTool));
133    registry.register(Box::new(registered::ctx_heatmap::CtxHeatmapTool));
134    registry.register(Box::new(registered::ctx_verify::CtxVerifyTool));
135    registry.register(Box::new(registered::ctx_outline::CtxOutlineTool));
136    registry.register(Box::new(registered::ctx_cost::CtxCostTool));
137    registry.register(Box::new(registered::ctx_gain::CtxGainTool));
138    registry.register(Box::new(registered::ctx_expand::CtxExpandTool));
139    registry.register(Box::new(registered::ctx_routes::CtxRoutesTool));
140    registry.register(Box::new(registered::ctx_call::CtxCallTool));
141    registry.register(Box::new(registered::ctx_callgraph::CtxCallgraphTool));
142    registry.register(Box::new(registered::ctx_refactor::CtxRefactorTool));
143    registry.register(Box::new(registered::ctx_repomap::CtxRepomapTool));
144    registry.register(Box::new(registered::ctx_symbol::CtxSymbolTool));
145    registry.register(Box::new(
146        registered::ctx_discover_tools::CtxDiscoverToolsTool,
147    ));
148    registry.register(Box::new(registered::ctx_tools::CtxToolsTool));
149    registry.register(Box::new(registered::ctx_review::CtxReviewTool));
150    registry.register(Box::new(registered::ctx_provider::CtxProviderTool));
151    registry.register(Box::new(registered::ctx_impact::CtxImpactTool));
152    registry.register(Box::new(registered::ctx_architecture::CtxArchitectureTool));
153    registry.register(Box::new(registered::ctx_smells::CtxSmellsTool));
154    registry.register(Box::new(registered::ctx_quality::CtxQualityTool));
155    registry.register(Box::new(registered::ctx_pack::CtxPackTool));
156    registry.register(Box::new(registered::ctx_plugins::CtxPluginsTool));
157    registry.register(Box::new(registered::ctx_rules::CtxRulesTool));
158    registry.register(Box::new(registered::ctx_index::CtxIndexTool));
159    registry.register(Box::new(registered::ctx_artifacts::CtxArtifactsTool));
160    registry.register(Box::new(
161        registered::ctx_compress_memory::CtxCompressMemoryTool,
162    ));
163    registry.register(Box::new(registered::ctx_read::CtxReadTool));
164    registry.register(Box::new(registered::ctx_multi_read::CtxMultiReadTool));
165    registry.register(Box::new(registered::ctx_multi_repo::CtxMultiRepoTool));
166    registry.register(Box::new(registered::ctx_smart_read::CtxSmartReadTool));
167    registry.register(Box::new(registered::ctx_delta::CtxDeltaTool));
168    registry.register(Box::new(registered::ctx_edit::CtxEditTool));
169    registry.register(Box::new(registered::ctx_patch::CtxPatchTool));
170    registry.register(Box::new(registered::ctx_fill::CtxFillTool));
171    registry.register(Box::new(registered::ctx_glob::CtxGlobTool));
172    registry.register(Box::new(registered::ctx_shell::CtxShellTool));
173    registry.register(Box::new(registered::shell_alias::ShellAliasTool));
174    registry.register(Box::new(registered::ctx_search::CtxSearchTool));
175    registry.register(Box::new(registered::ctx_url_read::CtxUrlReadTool));
176    registry.register(Box::new(registered::ctx_git_read::CtxGitReadTool));
177    registry.register(Box::new(registered::ctx_checkpoint::CtxCheckpointTool));
178    registry.register(Box::new(registered::ctx_compose::CtxComposeTool));
179    registry.register(Box::new(registered::ctx_explore::CtxExploreTool));
180    registry.register(Box::new(registered::ctx_execute::CtxExecuteTool));
181
182    // Utility tools (migrated from dispatch/utility_tools.rs)
183    registry.register(Box::new(registered::ctx_compress::CtxCompressTool));
184    registry.register(Box::new(registered::ctx_compare::CtxCompareTool));
185    registry.register(Box::new(crate::tools::ctx_cognitive::CtxCognitiveTool));
186    registry.register(Box::new(registered::ctx_metrics::CtxMetricsTool));
187    registry.register(Box::new(registered::ctx_radar::CtxRadarTool));
188    registry.register(Box::new(registered::ctx_dedup::CtxDedupTool));
189    registry.register(Box::new(registered::ctx_intent::CtxIntentTool));
190    registry.register(Box::new(registered::ctx_context::CtxContextTool));
191    registry.register(Box::new(registered::ctx_graph::CtxGraphTool));
192    registry.register(Box::new(registered::ctx_proof::CtxProofTool));
193    registry.register(Box::new(registered::ctx_cache::CtxCacheTool));
194    registry.register(Box::new(registered::ctx_ledger::CtxLedgerTool));
195    registry.register(Box::new(registered::ctx_retrieve::CtxRetrieveTool));
196    registry.register(Box::new(registered::ctx_overview::CtxOverviewTool));
197    registry.register(Box::new(registered::ctx_preload::CtxPreloadTool));
198    registry.register(Box::new(registered::ctx_prefetch::CtxPrefetchTool));
199    registry.register(Box::new(
200        registered::ctx_semantic_search::CtxSemanticSearchTool,
201    ));
202    registry.register(Box::new(registered::ctx_feedback::CtxFeedbackTool));
203    registry.register(Box::new(registered::ctx_control::CtxControlTool));
204    registry.register(Box::new(registered::ctx_plan::CtxPlanTool));
205    registry.register(Box::new(registered::ctx_compile::CtxCompileTool));
206
207    // Session tools (migrated from legacy dispatch)
208    registry.register(Box::new(registered::ctx_session::CtxSessionTool));
209    registry.register(Box::new(registered::ctx_knowledge::CtxKnowledgeTool));
210    registry.register(Box::new(registered::ctx_agent::CtxAgentTool));
211    registry.register(Box::new(registered::ctx_share::CtxShareTool));
212    registry.register(Box::new(registered::ctx_skillify::CtxSkillifyTool));
213    registry.register(Box::new(registered::ctx_summary::CtxSummaryTool));
214    registry.register(Box::new(
215        registered::ctx_transcript_compact::CtxTranscriptCompactTool,
216    ));
217    registry.register(Box::new(registered::ctx_package::CtxPackageTool));
218    registry.register(Box::new(registered::ctx_task::CtxTaskTool));
219    registry.register(Box::new(registered::ctx_handoff::CtxHandoffTool));
220    registry.register(Box::new(registered::ctx_workflow::CtxWorkflowTool));
221    registry.register(Box::new(registered::ctx_load_tools::CtxLoadToolsTool));
222
223    register_plugin_tools(&mut registry);
224
225    registry
226}
227
228/// Append manifest-declared plugin tools (EPIC 12.11) without forking the
229/// registry. Only enabled plugins contribute; a tool whose name collides with a
230/// native tool is skipped (native tools win) so a plugin can never shadow core
231/// behavior. No-op when no plugins are installed.
232fn register_plugin_tools(registry: &mut ToolRegistry) {
233    for spec in crate::core::plugins::PluginManager::tool_specs() {
234        if registry.contains(&spec.name) {
235            tracing::warn!(
236                "plugin '{}' tool '{}' collides with a native tool; skipping",
237                spec.plugin_name,
238                spec.name
239            );
240            continue;
241        }
242        registry.register(Box::new(
243            crate::tools::registered::plugin_tool::PluginTool::from_spec(spec),
244        ));
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn get_arc_returns_owned_handle_for_known_tool() {
254        let registry = build_registry();
255        let arc = registry.get_arc("ctx_tree");
256        assert!(arc.is_some(), "ctx_tree must be registered");
257        assert_eq!(arc.unwrap().name(), "ctx_tree");
258    }
259
260    #[test]
261    fn get_arc_is_none_for_unknown_tool() {
262        let registry = build_registry();
263        assert!(registry.get_arc("ctx_does_not_exist_xyz").is_none());
264    }
265
266    #[test]
267    fn get_and_get_arc_agree_for_core_tool() {
268        let registry = build_registry();
269        assert_eq!(
270            registry.get("ctx_read").is_some(),
271            registry.get_arc("ctx_read").is_some(),
272            "get and get_arc must agree on tool presence"
273        );
274    }
275}