Skip to main content

claude_rust_tools/infrastructure/
tool_search_tool.rs

1use claude_rust_errors::AppResult;
2use claude_rust_types::{PermissionLevel, SearchReadInfo, Tool};
3use serde_json::{Value, json};
4
5pub struct ToolSearchTool {
6    tool_catalog: Vec<(String, String)>,
7}
8
9impl ToolSearchTool {
10    pub fn new(tool_catalog: Vec<(String, String)>) -> Self {
11        Self { tool_catalog }
12    }
13}
14
15#[async_trait::async_trait]
16impl Tool for ToolSearchTool {
17    fn name(&self) -> &str {
18        "tool_search"
19    }
20
21    fn description(&self) -> &str {
22        "Search available tools by name or description. Returns matching tool names with their descriptions."
23    }
24
25    fn input_schema(&self) -> Value {
26        json!({
27            "type": "object",
28            "properties": {
29                "query": {
30                    "type": "string",
31                    "description": "Search query to match against tool names and descriptions"
32                },
33                "max_results": {
34                    "type": "integer",
35                    "description": "Maximum number of results to return (default 5)"
36                }
37            },
38            "required": ["query"]
39        })
40    }
41
42    fn permission_level(&self) -> PermissionLevel {
43        PermissionLevel::ReadOnly
44    }
45
46    fn is_read_only(&self, _input: &Value) -> bool { true }
47    fn is_concurrent_safe(&self, _input: &Value) -> bool { true }
48    fn always_load(&self) -> bool { true }
49
50    fn is_search_or_read_command(&self, _input: &Value) -> SearchReadInfo {
51        SearchReadInfo { is_search: true, is_read: false, is_list: false }
52    }
53
54    async fn execute(&self, input: Value) -> AppResult<String> {
55        let query = input
56            .get("query")
57            .and_then(|v| v.as_str())
58            .ok_or_else(|| claude_rust_errors::AppError::Tool("missing 'query' field".into()))?;
59
60        let max_results = input
61            .get("max_results")
62            .and_then(|v| v.as_u64())
63            .unwrap_or(5) as usize;
64
65        let query_lower = query.to_lowercase();
66
67        tracing::info!(query, max_results, "searching tools");
68
69        let matches: Vec<&(String, String)> = self
70            .tool_catalog
71            .iter()
72            .filter(|(name, desc)| {
73                name.to_lowercase().contains(&query_lower)
74                    || desc.to_lowercase().contains(&query_lower)
75            })
76            .take(max_results)
77            .collect();
78
79        if matches.is_empty() {
80            return Ok(format!("No tools matched query '{query}'."));
81        }
82
83        let mut result = format!("{} tool(s) matched:\n", matches.len());
84        for (name, desc) in matches {
85            result.push_str(&format!("\n- **{name}**: {desc}"));
86        }
87
88        Ok(result)
89    }
90}