fabryk_mcp_core/
registry.rs1use rmcp::model::{CallToolResult, ErrorData, Tool};
12use serde_json::Value;
13use std::future::Future;
14use std::pin::Pin;
15
16pub type ToolResult = Pin<Box<dyn Future<Output = Result<CallToolResult, ErrorData>> + Send>>;
18
19pub trait ToolRegistry: Send + Sync {
44 fn tools(&self) -> Vec<Tool>;
46
47 fn call(&self, name: &str, args: Value) -> Option<ToolResult>;
51
52 fn tool_count(&self) -> usize {
54 self.tools().len()
55 }
56
57 fn has_tool(&self, name: &str) -> bool {
59 self.tools().iter().any(|t| t.name == name)
60 }
61}
62
63pub struct CompositeRegistry {
79 registries: Vec<Box<dyn ToolRegistry>>,
80}
81
82impl CompositeRegistry {
83 pub fn new() -> Self {
85 Self {
86 registries: Vec::new(),
87 }
88 }
89
90 #[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#[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 assert!(composite.call("search", json!({})).is_some());
233 assert!(composite.call("graph", json!({})).is_some());
235 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}