rmcp_openapi/tool/
tool_collection.rs

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