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    pub fn tool_defs(&self) -> Vec<Tool> {
53        let mut defs: Vec<Tool> = self.tools.values().map(|t| t.tool_def()).collect();
54        defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
55        defs
56    }
57
58    /// Returns tool definitions filtered by the dynamic tool state.
59    /// Only includes tools whose category is currently active.
60    pub fn active_tool_defs(&self) -> Vec<Tool> {
61        let Ok(state) = super::dynamic_tools::global().lock() else {
62            tracing::warn!("dynamic_tools mutex poisoned in active_tool_defs; returning all");
63            return self.tool_defs();
64        };
65        let mut defs: Vec<Tool> = self
66            .tools
67            .values()
68            .filter(|t| state.is_tool_active(t.name()))
69            .map(|t| t.tool_def())
70            .collect();
71        defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
72        defs
73    }
74
75    /// Returns tool definitions filtered by a tool profile.
76    /// Only includes tools whose name is enabled by the given profile.
77    pub fn profile_tool_defs(
78        &self,
79        profile: &crate::core::tool_profiles::ToolProfile,
80    ) -> Vec<Tool> {
81        let mut defs: Vec<Tool> = self
82            .tools
83            .values()
84            .filter(|t| profile.is_tool_enabled(t.name()))
85            .map(|t| t.tool_def())
86            .collect();
87        defs.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
88        defs
89    }
90
91    pub fn len(&self) -> usize {
92        self.tools.len()
93    }
94
95    pub fn is_empty(&self) -> bool {
96        self.tools.is_empty()
97    }
98
99    pub fn names(&self) -> Vec<&'static str> {
100        let mut names: Vec<_> = self.tools.keys().copied().collect();
101        names.sort_unstable();
102        names
103    }
104}
105
106impl Default for ToolRegistry {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112/// Number of registered MCP tools — the single source of truth for the
113/// "N MCP tools" count shown in `--help`, the README, and the feature catalog.
114/// Deriving it here means the count can never drift from the actual registry.
115pub fn tool_count() -> usize {
116    build_registry().len()
117}
118
119/// Register all trait-based tools. Called once during server startup.
120/// New tools are added here as their `McpTool` implementation lands.
121pub fn build_registry() -> ToolRegistry {
122    let mut registry = ToolRegistry::new();
123
124    use crate::tools::registered;
125    registry.register(Box::new(registered::ctx_tree::CtxTreeTool));
126    registry.register(Box::new(registered::ctx_benchmark::CtxBenchmarkTool));
127    registry.register(Box::new(registered::ctx_analyze::CtxAnalyzeTool));
128    registry.register(Box::new(registered::ctx_discover::CtxDiscoverTool));
129    registry.register(Box::new(registered::ctx_response::CtxResponseTool));
130    registry.register(Box::new(registered::ctx_heatmap::CtxHeatmapTool));
131    registry.register(Box::new(registered::ctx_verify::CtxVerifyTool));
132    registry.register(Box::new(registered::ctx_outline::CtxOutlineTool));
133    registry.register(Box::new(registered::ctx_cost::CtxCostTool));
134    registry.register(Box::new(registered::ctx_gain::CtxGainTool));
135    registry.register(Box::new(registered::ctx_expand::CtxExpandTool));
136    registry.register(Box::new(registered::ctx_routes::CtxRoutesTool));
137    registry.register(Box::new(registered::ctx_call::CtxCallTool));
138    registry.register(Box::new(registered::ctx_callgraph::CtxCallgraphTool));
139    registry.register(Box::new(registered::ctx_refactor::CtxRefactorTool));
140    registry.register(Box::new(registered::ctx_repomap::CtxRepomapTool));
141    registry.register(Box::new(registered::ctx_symbol::CtxSymbolTool));
142    registry.register(Box::new(
143        registered::ctx_discover_tools::CtxDiscoverToolsTool,
144    ));
145    registry.register(Box::new(registered::ctx_tools::CtxToolsTool));
146    registry.register(Box::new(registered::ctx_review::CtxReviewTool));
147    registry.register(Box::new(registered::ctx_provider::CtxProviderTool));
148    registry.register(Box::new(registered::ctx_impact::CtxImpactTool));
149    registry.register(Box::new(registered::ctx_architecture::CtxArchitectureTool));
150    registry.register(Box::new(registered::ctx_smells::CtxSmellsTool));
151    registry.register(Box::new(registered::ctx_pack::CtxPackTool));
152    registry.register(Box::new(registered::ctx_plugins::CtxPluginsTool));
153    registry.register(Box::new(registered::ctx_rules::CtxRulesTool));
154    registry.register(Box::new(registered::ctx_index::CtxIndexTool));
155    registry.register(Box::new(registered::ctx_artifacts::CtxArtifactsTool));
156    registry.register(Box::new(
157        registered::ctx_compress_memory::CtxCompressMemoryTool,
158    ));
159    registry.register(Box::new(registered::ctx_read::CtxReadTool));
160    registry.register(Box::new(registered::ctx_multi_read::CtxMultiReadTool));
161    registry.register(Box::new(registered::ctx_multi_repo::CtxMultiRepoTool));
162    registry.register(Box::new(registered::ctx_smart_read::CtxSmartReadTool));
163    registry.register(Box::new(registered::ctx_delta::CtxDeltaTool));
164    registry.register(Box::new(registered::ctx_edit::CtxEditTool));
165    registry.register(Box::new(registered::ctx_fill::CtxFillTool));
166    registry.register(Box::new(registered::ctx_glob::CtxGlobTool));
167    registry.register(Box::new(registered::ctx_shell::CtxShellTool));
168    registry.register(Box::new(registered::shell_alias::ShellAliasTool));
169    registry.register(Box::new(registered::ctx_search::CtxSearchTool));
170    registry.register(Box::new(registered::ctx_url_read::CtxUrlReadTool));
171    registry.register(Box::new(registered::ctx_git_read::CtxGitReadTool));
172    registry.register(Box::new(registered::ctx_checkpoint::CtxCheckpointTool));
173    registry.register(Box::new(registered::ctx_compose::CtxComposeTool));
174    registry.register(Box::new(registered::ctx_execute::CtxExecuteTool));
175
176    // Utility tools (migrated from dispatch/utility_tools.rs)
177    registry.register(Box::new(registered::ctx_compress::CtxCompressTool));
178    registry.register(Box::new(registered::ctx_metrics::CtxMetricsTool));
179    registry.register(Box::new(registered::ctx_radar::CtxRadarTool));
180    registry.register(Box::new(registered::ctx_dedup::CtxDedupTool));
181    registry.register(Box::new(registered::ctx_intent::CtxIntentTool));
182    registry.register(Box::new(registered::ctx_context::CtxContextTool));
183    registry.register(Box::new(registered::ctx_graph::CtxGraphTool));
184    registry.register(Box::new(registered::ctx_proof::CtxProofTool));
185    registry.register(Box::new(registered::ctx_cache::CtxCacheTool));
186    registry.register(Box::new(registered::ctx_ledger::CtxLedgerTool));
187    registry.register(Box::new(registered::ctx_retrieve::CtxRetrieveTool));
188    registry.register(Box::new(registered::ctx_overview::CtxOverviewTool));
189    registry.register(Box::new(registered::ctx_preload::CtxPreloadTool));
190    registry.register(Box::new(registered::ctx_prefetch::CtxPrefetchTool));
191    registry.register(Box::new(
192        registered::ctx_semantic_search::CtxSemanticSearchTool,
193    ));
194    registry.register(Box::new(registered::ctx_feedback::CtxFeedbackTool));
195    registry.register(Box::new(registered::ctx_control::CtxControlTool));
196    registry.register(Box::new(registered::ctx_plan::CtxPlanTool));
197    registry.register(Box::new(registered::ctx_compile::CtxCompileTool));
198
199    // Session tools (migrated from legacy dispatch)
200    registry.register(Box::new(registered::ctx_session::CtxSessionTool));
201    registry.register(Box::new(registered::ctx_knowledge::CtxKnowledgeTool));
202    registry.register(Box::new(registered::ctx_agent::CtxAgentTool));
203    registry.register(Box::new(registered::ctx_share::CtxShareTool));
204    registry.register(Box::new(registered::ctx_skillify::CtxSkillifyTool));
205    registry.register(Box::new(registered::ctx_summary::CtxSummaryTool));
206    registry.register(Box::new(registered::ctx_package::CtxPackageTool));
207    registry.register(Box::new(registered::ctx_task::CtxTaskTool));
208    registry.register(Box::new(registered::ctx_handoff::CtxHandoffTool));
209    registry.register(Box::new(registered::ctx_workflow::CtxWorkflowTool));
210    registry.register(Box::new(registered::ctx_load_tools::CtxLoadToolsTool));
211
212    register_plugin_tools(&mut registry);
213
214    registry
215}
216
217/// Append manifest-declared plugin tools (EPIC 12.11) without forking the
218/// registry. Only enabled plugins contribute; a tool whose name collides with a
219/// native tool is skipped (native tools win) so a plugin can never shadow core
220/// behavior. No-op when no plugins are installed.
221fn register_plugin_tools(registry: &mut ToolRegistry) {
222    for spec in crate::core::plugins::PluginManager::tool_specs() {
223        if registry.contains(&spec.name) {
224            tracing::warn!(
225                "plugin '{}' tool '{}' collides with a native tool; skipping",
226                spec.plugin_name,
227                spec.name
228            );
229            continue;
230        }
231        registry.register(Box::new(
232            crate::tools::registered::plugin_tool::PluginTool::from_spec(spec),
233        ));
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn get_arc_returns_owned_handle_for_known_tool() {
243        let registry = build_registry();
244        let arc = registry.get_arc("ctx_tree");
245        assert!(arc.is_some(), "ctx_tree must be registered");
246        assert_eq!(arc.unwrap().name(), "ctx_tree");
247    }
248
249    #[test]
250    fn get_arc_is_none_for_unknown_tool() {
251        let registry = build_registry();
252        assert!(registry.get_arc("ctx_does_not_exist_xyz").is_none());
253    }
254
255    #[test]
256    fn get_and_get_arc_agree_for_core_tool() {
257        let registry = build_registry();
258        assert_eq!(
259            registry.get("ctx_read").is_some(),
260            registry.get_arc("ctx_read").is_some(),
261            "get and get_arc must agree on tool presence"
262        );
263    }
264}