Skip to main content

fabryk_mcp_core/
registry.rs

1//! Tool registry trait for MCP servers.
2//!
3//! This module defines the `ToolRegistry` trait that abstracts over
4//! tool registration and dispatch. Domains implement this trait to
5//! define their available MCP tools.
6//!
7//! The `CompositeRegistry` combines multiple registries into one,
8//! enabling composition of tools from separate sources (content,
9//! search, graph, etc.).
10
11use rmcp::model::{CallToolResult, ErrorData, Tool};
12use serde_json::Value;
13use std::future::Future;
14use std::pin::Pin;
15
16/// Type alias for async tool handler results.
17pub type ToolResult = Pin<Box<dyn Future<Output = Result<CallToolResult, ErrorData>> + Send>>;
18
19/// Trait for registering and dispatching MCP tools.
20///
21/// Each domain or feature implements this to define its available tools.
22/// The `FabrykMcpServer` delegates `list_tools` and `call_tool` to the
23/// registry it holds.
24///
25/// # Example
26///
27/// ```rust,ignore
28/// struct MyTools { /* ... */ }
29///
30/// impl ToolRegistry for MyTools {
31///     fn tools(&self) -> Vec<Tool> {
32///         vec![/* tool definitions */]
33///     }
34///
35///     fn call(&self, name: &str, args: Value) -> Option<ToolResult> {
36///         match name {
37///             "my_tool" => Some(Box::pin(self.handle_my_tool(args))),
38///             _ => None,
39///         }
40///     }
41/// }
42/// ```
43pub trait ToolRegistry: Send + Sync {
44    /// Returns information about all available tools.
45    fn tools(&self) -> Vec<Tool>;
46
47    /// Dispatches a tool call by name.
48    ///
49    /// Returns `None` if the tool is not recognized by this registry.
50    fn call(&self, name: &str, args: Value) -> Option<ToolResult>;
51
52    /// Returns the number of registered tools.
53    fn tool_count(&self) -> usize {
54        self.tools().len()
55    }
56
57    /// Check if a tool exists by name.
58    fn has_tool(&self, name: &str) -> bool {
59        self.tools().iter().any(|t| t.name == name)
60    }
61}
62
63/// A registry that combines multiple sub-registries.
64///
65/// Useful for composing tools from multiple sources (content, search,
66/// graph, etc.) into a single registry for the MCP server.
67///
68/// # Example
69///
70/// ```rust,ignore
71/// let registry = CompositeRegistry::new()
72///     .add(content_tools)
73///     .add(search_tools)
74///     .add(graph_tools);
75///
76/// assert_eq!(registry.tool_count(), 14);
77/// ```
78pub struct CompositeRegistry {
79    registries: Vec<Box<dyn ToolRegistry>>,
80}
81
82impl CompositeRegistry {
83    /// Create a new empty composite registry.
84    pub fn new() -> Self {
85        Self {
86            registries: Vec::new(),
87        }
88    }
89
90    /// Add a sub-registry.
91    #[allow(clippy::should_implement_trait)]
92    pub fn add<R: ToolRegistry + 'static>(mut self, registry: R) -> Self {
93        self.registries.push(Box::new(registry));
94        self
95    }
96}
97
98impl Default for CompositeRegistry {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl ToolRegistry for CompositeRegistry {
105    fn tools(&self) -> Vec<Tool> {
106        self.registries.iter().flat_map(|r| r.tools()).collect()
107    }
108
109    fn call(&self, name: &str, args: Value) -> Option<ToolResult> {
110        for registry in &self.registries {
111            if let Some(result) = registry.call(name, args.clone()) {
112                return Some(result);
113            }
114        }
115        None
116    }
117}
118
119// ============================================================================
120// Tests
121// ============================================================================
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use rmcp::model::Content;
127    use serde_json::json;
128
129    fn make_tool(name: &str, description: &str) -> Tool {
130        Tool::new(
131            name.to_string(),
132            description.to_string(),
133            crate::empty_input_schema(),
134        )
135    }
136
137    struct TestRegistry {
138        tool_list: Vec<Tool>,
139    }
140
141    impl ToolRegistry for TestRegistry {
142        fn tools(&self) -> Vec<Tool> {
143            self.tool_list.clone()
144        }
145
146        fn call(&self, name: &str, _args: Value) -> Option<ToolResult> {
147            if self.has_tool(name) {
148                let name = name.to_string();
149                Some(Box::pin(async move {
150                    Ok(CallToolResult::success(vec![Content::text(format!(
151                        "called: {name}"
152                    ))]))
153                }))
154            } else {
155                None
156            }
157        }
158    }
159
160    #[test]
161    fn test_tool_count() {
162        let registry = TestRegistry {
163            tool_list: vec![make_tool("tool1", "First"), make_tool("tool2", "Second")],
164        };
165        assert_eq!(registry.tool_count(), 2);
166    }
167
168    #[test]
169    fn test_has_tool() {
170        let registry = TestRegistry {
171            tool_list: vec![make_tool("exists", "A tool")],
172        };
173        assert!(registry.has_tool("exists"));
174        assert!(!registry.has_tool("missing"));
175    }
176
177    #[tokio::test]
178    async fn test_call_known_tool() {
179        let registry = TestRegistry {
180            tool_list: vec![make_tool("greet", "Say hello")],
181        };
182
183        let future = registry.call("greet", json!({})).unwrap();
184        let result = future.await.unwrap();
185        assert_eq!(result.is_error, Some(false));
186    }
187
188    #[test]
189    fn test_call_unknown_tool() {
190        let registry = TestRegistry {
191            tool_list: vec![make_tool("greet", "Say hello")],
192        };
193        assert!(registry.call("missing", json!({})).is_none());
194    }
195
196    #[test]
197    fn test_composite_registry_empty() {
198        let composite = CompositeRegistry::new();
199        assert_eq!(composite.tool_count(), 0);
200        assert!(!composite.has_tool("anything"));
201    }
202
203    #[test]
204    fn test_composite_registry_combines_tools() {
205        let reg1 = TestRegistry {
206            tool_list: vec![make_tool("tool1", "From reg1")],
207        };
208        let reg2 = TestRegistry {
209            tool_list: vec![make_tool("tool2", "From reg2")],
210        };
211
212        let composite = CompositeRegistry::new().add(reg1).add(reg2);
213
214        assert_eq!(composite.tool_count(), 2);
215        assert!(composite.has_tool("tool1"));
216        assert!(composite.has_tool("tool2"));
217        assert!(!composite.has_tool("tool3"));
218    }
219
220    #[tokio::test]
221    async fn test_composite_registry_dispatches() {
222        let reg1 = TestRegistry {
223            tool_list: vec![make_tool("search", "Search")],
224        };
225        let reg2 = TestRegistry {
226            tool_list: vec![make_tool("graph", "Graph")],
227        };
228
229        let composite = CompositeRegistry::new().add(reg1).add(reg2);
230
231        // Should dispatch to reg1
232        assert!(composite.call("search", json!({})).is_some());
233        // Should dispatch to reg2
234        assert!(composite.call("graph", json!({})).is_some());
235        // Should return None
236        assert!(composite.call("missing", json!({})).is_none());
237    }
238
239    #[test]
240    fn test_composite_registry_default() {
241        let composite = CompositeRegistry::default();
242        assert_eq!(composite.tool_count(), 0);
243    }
244
245    #[test]
246    fn test_composite_registry_multiple_add() {
247        let composite = CompositeRegistry::new()
248            .add(TestRegistry {
249                tool_list: vec![make_tool("a", "A")],
250            })
251            .add(TestRegistry {
252                tool_list: vec![make_tool("b", "B")],
253            })
254            .add(TestRegistry {
255                tool_list: vec![make_tool("c", "C")],
256            });
257
258        assert_eq!(composite.tool_count(), 3);
259    }
260
261    #[test]
262    fn test_trait_object_safety() {
263        fn _assert_object_safe(_: &dyn ToolRegistry) {}
264    }
265}