rmcp_openapi/tool/
tool_collection.rs

1use super::Tool;
2use crate::config::Authorization;
3use crate::error::{ToolCallError, ToolCallValidationError};
4use rmcp::model::{CallToolResult, Tool as McpTool};
5use serde_json::Value;
6use tracing::debug_span;
7
8/// Collection of tools with built-in validation and lookup capabilities
9///
10/// This struct encapsulates all tool management logic in the library layer,
11/// providing a clean API for the binary to delegate tool operations to.
12#[derive(Clone, Default)]
13pub struct ToolCollection {
14    tools: Vec<Tool>,
15}
16
17impl ToolCollection {
18    /// Create a new empty tool collection
19    pub fn new() -> Self {
20        Self { tools: Vec::new() }
21    }
22
23    /// Create a tool collection from a vector of tools
24    pub fn from_tools(tools: Vec<Tool>) -> Self {
25        Self { tools }
26    }
27
28    /// Add a tool to the collection
29    pub fn add_tool(&mut self, tool: Tool) {
30        self.tools.push(tool);
31    }
32
33    /// Get the number of tools in the collection
34    pub fn len(&self) -> usize {
35        self.tools.len()
36    }
37
38    /// Check if the collection is empty
39    pub fn is_empty(&self) -> bool {
40        self.tools.is_empty()
41    }
42
43    /// Get all tool names
44    pub fn get_tool_names(&self) -> Vec<String> {
45        self.tools
46            .iter()
47            .map(|tool| tool.metadata.name.clone())
48            .collect()
49    }
50
51    /// Check if a specific tool exists
52    pub fn has_tool(&self, name: &str) -> bool {
53        self.tools.iter().any(|tool| tool.metadata.name == name)
54    }
55
56    /// Get a tool by name
57    pub fn get_tool(&self, name: &str) -> Option<&Tool> {
58        self.tools.iter().find(|tool| tool.metadata.name == name)
59    }
60
61    /// Convert all tools to MCP Tool format for list_tools response
62    pub fn to_mcp_tools(&self) -> Vec<McpTool> {
63        self.tools.iter().map(McpTool::from).collect()
64    }
65
66    /// Call a tool by name with validation
67    ///
68    /// This method encapsulates all tool validation logic:
69    /// - Tool not found errors with suggestions
70    /// - Parameter validation
71    /// - Tool execution
72    pub async fn call_tool(
73        &self,
74        tool_name: &str,
75        arguments: &Value,
76        authorization: Authorization,
77    ) -> Result<CallToolResult, ToolCallError> {
78        let span = debug_span!(
79            "tool_execution",
80            tool_name = %tool_name,
81            total_tools = self.tools.len()
82        );
83        let _enter = span.enter();
84
85        // First validate that the tool exists
86        if let Some(tool) = self.get_tool(tool_name) {
87            // Tool exists, delegate to the tool's call method
88            tool.call(arguments, authorization).await
89        } else {
90            // Tool not found - generate suggestions and return validation error
91            let tool_names: Vec<&str> = self
92                .tools
93                .iter()
94                .map(|tool| tool.metadata.name.as_str())
95                .collect();
96
97            Err(ToolCallError::Validation(
98                ToolCallValidationError::tool_not_found(tool_name.to_string(), &tool_names),
99            ))
100        }
101    }
102
103    /// Get basic statistics about the tool collection
104    pub fn get_stats(&self) -> String {
105        format!("Total tools: {}", self.tools.len())
106    }
107
108    /// Get an iterator over the tools
109    pub fn iter(&self) -> impl Iterator<Item = &Tool> {
110        self.tools.iter()
111    }
112}
113
114impl From<Vec<Tool>> for ToolCollection {
115    fn from(tools: Vec<Tool>) -> Self {
116        Self::from_tools(tools)
117    }
118}
119
120impl IntoIterator for ToolCollection {
121    type Item = Tool;
122    type IntoIter = std::vec::IntoIter<Tool>;
123
124    fn into_iter(self) -> Self::IntoIter {
125        self.tools.into_iter()
126    }
127}
128
129impl<'a> IntoIterator for &'a ToolCollection {
130    type Item = &'a Tool;
131    type IntoIter = std::slice::Iter<'a, Tool>;
132
133    fn into_iter(self) -> Self::IntoIter {
134        self.tools.iter()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::tool::ToolMetadata;
142    use serde_json::json;
143
144    fn create_test_tool(name: &str, description: &str) -> Tool {
145        let metadata = ToolMetadata {
146            name: name.to_string(),
147            title: Some(name.to_string()),
148            description: description.to_string(),
149            parameters: json!({
150                "type": "object",
151                "properties": {
152                    "id": {"type": "integer"}
153                },
154                "required": ["id"]
155            }),
156            output_schema: None,
157            method: "GET".to_string(),
158            path: format!("/{}", name),
159            security: None,
160        };
161        Tool::new(metadata, None, None).unwrap()
162    }
163
164    #[test]
165    fn test_tool_collection_creation() {
166        let collection = ToolCollection::new();
167        assert_eq!(collection.len(), 0);
168        assert!(collection.is_empty());
169    }
170
171    #[test]
172    fn test_tool_collection_from_tools() {
173        let tool1 = create_test_tool("test1", "Test tool 1");
174        let tool2 = create_test_tool("test2", "Test tool 2");
175        let tools = vec![tool1, tool2];
176
177        let collection = ToolCollection::from_tools(tools);
178        assert_eq!(collection.len(), 2);
179        assert!(!collection.is_empty());
180        assert!(collection.has_tool("test1"));
181        assert!(collection.has_tool("test2"));
182        assert!(!collection.has_tool("test3"));
183    }
184
185    #[test]
186    fn test_add_tool() {
187        let mut collection = ToolCollection::new();
188        let tool = create_test_tool("test", "Test tool");
189
190        collection.add_tool(tool);
191        assert_eq!(collection.len(), 1);
192        assert!(collection.has_tool("test"));
193    }
194
195    #[test]
196    fn test_get_tool_names() {
197        let tool1 = create_test_tool("getPetById", "Get pet by ID");
198        let tool2 = create_test_tool("getPetsByStatus", "Get pets by status");
199        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
200
201        let names = collection.get_tool_names();
202        assert_eq!(names, vec!["getPetById", "getPetsByStatus"]);
203    }
204
205    #[test]
206    fn test_get_tool() {
207        let tool = create_test_tool("test", "Test tool");
208        let collection = ToolCollection::from_tools(vec![tool]);
209
210        assert!(collection.get_tool("test").is_some());
211        assert!(collection.get_tool("nonexistent").is_none());
212    }
213
214    #[test]
215    fn test_to_mcp_tools() {
216        let tool1 = create_test_tool("test1", "Test tool 1");
217        let tool2 = create_test_tool("test2", "Test tool 2");
218        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
219
220        let mcp_tools = collection.to_mcp_tools();
221        assert_eq!(mcp_tools.len(), 2);
222        assert_eq!(mcp_tools[0].name, "test1");
223        assert_eq!(mcp_tools[1].name, "test2");
224    }
225
226    #[actix_web::test]
227    async fn test_call_tool_not_found_with_suggestions() {
228        let tool1 = create_test_tool("getPetById", "Get pet by ID");
229        let tool2 = create_test_tool("getPetsByStatus", "Get pets by status");
230        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
231
232        let result = collection
233            .call_tool("getPetByID", &json!({}), Authorization::default())
234            .await;
235        assert!(result.is_err());
236
237        if let Err(ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
238            tool_name,
239            suggestions,
240        })) = result
241        {
242            assert_eq!(tool_name, "getPetByID");
243            // The algorithm finds multiple similar matches
244            assert!(suggestions.contains(&"getPetById".to_string()));
245            assert!(!suggestions.is_empty());
246        } else {
247            panic!("Expected ToolNotFound error with suggestions");
248        }
249    }
250
251    #[actix_web::test]
252    async fn test_call_tool_not_found_no_suggestions() {
253        let tool = create_test_tool("getPetById", "Get pet by ID");
254        let collection = ToolCollection::from_tools(vec![tool]);
255
256        let result = collection
257            .call_tool(
258                "completelyDifferentName",
259                &json!({}),
260                Authorization::default(),
261            )
262            .await;
263        assert!(result.is_err());
264
265        if let Err(ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
266            tool_name,
267            suggestions,
268        })) = result
269        {
270            assert_eq!(tool_name, "completelyDifferentName");
271            assert!(suggestions.is_empty());
272        } else {
273            panic!("Expected ToolNotFound error with no suggestions");
274        }
275    }
276
277    #[test]
278    fn test_iterators() {
279        let tool1 = create_test_tool("test1", "Test tool 1");
280        let tool2 = create_test_tool("test2", "Test tool 2");
281        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
282
283        // Test iter()
284        let names: Vec<String> = collection
285            .iter()
286            .map(|tool| tool.metadata.name.clone())
287            .collect();
288        assert_eq!(names, vec!["test1", "test2"]);
289
290        // Test IntoIterator for &collection
291        let names: Vec<String> = (&collection)
292            .into_iter()
293            .map(|tool| tool.metadata.name.clone())
294            .collect();
295        assert_eq!(names, vec!["test1", "test2"]);
296
297        // Test IntoIterator for collection (consumes it)
298        let names: Vec<String> = collection
299            .into_iter()
300            .map(|tool| tool.metadata.name.clone())
301            .collect();
302        assert_eq!(names, vec!["test1", "test2"]);
303    }
304
305    #[test]
306    fn test_from_vec() {
307        let tool1 = create_test_tool("test1", "Test tool 1");
308        let tool2 = create_test_tool("test2", "Test tool 2");
309        let tools = vec![tool1, tool2];
310
311        let collection: ToolCollection = tools.into();
312        assert_eq!(collection.len(), 2);
313    }
314}