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: Some(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            parameter_mappings: std::collections::HashMap::new(),
161        };
162        Tool::new(metadata, None, None).unwrap()
163    }
164
165    #[test]
166    fn test_tool_collection_creation() {
167        let collection = ToolCollection::new();
168        assert_eq!(collection.len(), 0);
169        assert!(collection.is_empty());
170    }
171
172    #[test]
173    fn test_tool_collection_from_tools() {
174        let tool1 = create_test_tool("test1", "Test tool 1");
175        let tool2 = create_test_tool("test2", "Test tool 2");
176        let tools = vec![tool1, tool2];
177
178        let collection = ToolCollection::from_tools(tools);
179        assert_eq!(collection.len(), 2);
180        assert!(!collection.is_empty());
181        assert!(collection.has_tool("test1"));
182        assert!(collection.has_tool("test2"));
183        assert!(!collection.has_tool("test3"));
184    }
185
186    #[test]
187    fn test_add_tool() {
188        let mut collection = ToolCollection::new();
189        let tool = create_test_tool("test", "Test tool");
190
191        collection.add_tool(tool);
192        assert_eq!(collection.len(), 1);
193        assert!(collection.has_tool("test"));
194    }
195
196    #[test]
197    fn test_get_tool_names() {
198        let tool1 = create_test_tool("getPetById", "Get pet by ID");
199        let tool2 = create_test_tool("getPetsByStatus", "Get pets by status");
200        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
201
202        let names = collection.get_tool_names();
203        assert_eq!(names, vec!["getPetById", "getPetsByStatus"]);
204    }
205
206    #[test]
207    fn test_get_tool() {
208        let tool = create_test_tool("test", "Test tool");
209        let collection = ToolCollection::from_tools(vec![tool]);
210
211        assert!(collection.get_tool("test").is_some());
212        assert!(collection.get_tool("nonexistent").is_none());
213    }
214
215    #[test]
216    fn test_to_mcp_tools() {
217        let tool1 = create_test_tool("test1", "Test tool 1");
218        let tool2 = create_test_tool("test2", "Test tool 2");
219        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
220
221        let mcp_tools = collection.to_mcp_tools();
222        assert_eq!(mcp_tools.len(), 2);
223        assert_eq!(mcp_tools[0].name, "test1");
224        assert_eq!(mcp_tools[1].name, "test2");
225    }
226
227    #[actix_web::test]
228    async fn test_call_tool_not_found_with_suggestions() {
229        let tool1 = create_test_tool("getPetById", "Get pet by ID");
230        let tool2 = create_test_tool("getPetsByStatus", "Get pets by status");
231        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
232
233        let result = collection
234            .call_tool("getPetByID", &json!({}), Authorization::default())
235            .await;
236        assert!(result.is_err());
237
238        if let Err(ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
239            tool_name,
240            suggestions,
241        })) = result
242        {
243            assert_eq!(tool_name, "getPetByID");
244            // The algorithm finds multiple similar matches
245            assert!(suggestions.contains(&"getPetById".to_string()));
246            assert!(!suggestions.is_empty());
247        } else {
248            panic!("Expected ToolNotFound error with suggestions");
249        }
250    }
251
252    #[actix_web::test]
253    async fn test_call_tool_not_found_no_suggestions() {
254        let tool = create_test_tool("getPetById", "Get pet by ID");
255        let collection = ToolCollection::from_tools(vec![tool]);
256
257        let result = collection
258            .call_tool(
259                "completelyDifferentName",
260                &json!({}),
261                Authorization::default(),
262            )
263            .await;
264        assert!(result.is_err());
265
266        if let Err(ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
267            tool_name,
268            suggestions,
269        })) = result
270        {
271            assert_eq!(tool_name, "completelyDifferentName");
272            assert!(suggestions.is_empty());
273        } else {
274            panic!("Expected ToolNotFound error with no suggestions");
275        }
276    }
277
278    #[test]
279    fn test_iterators() {
280        let tool1 = create_test_tool("test1", "Test tool 1");
281        let tool2 = create_test_tool("test2", "Test tool 2");
282        let collection = ToolCollection::from_tools(vec![tool1, tool2]);
283
284        // Test iter()
285        let names: Vec<String> = collection
286            .iter()
287            .map(|tool| tool.metadata.name.clone())
288            .collect();
289        assert_eq!(names, vec!["test1", "test2"]);
290
291        // Test IntoIterator for &collection
292        let names: Vec<String> = (&collection)
293            .into_iter()
294            .map(|tool| tool.metadata.name.clone())
295            .collect();
296        assert_eq!(names, vec!["test1", "test2"]);
297
298        // Test IntoIterator for collection (consumes it)
299        let names: Vec<String> = collection
300            .into_iter()
301            .map(|tool| tool.metadata.name.clone())
302            .collect();
303        assert_eq!(names, vec!["test1", "test2"]);
304    }
305
306    #[test]
307    fn test_from_vec() {
308        let tool1 = create_test_tool("test1", "Test tool 1");
309        let tool2 = create_test_tool("test2", "Test tool 2");
310        let tools = vec![tool1, tool2];
311
312        let collection: ToolCollection = tools.into();
313        assert_eq!(collection.len(), 2);
314    }
315}