Skip to main content

cersei_tools/
tool_search.rs

1//! ToolSearch tool: search available tools by name or description.
2
3use super::*;
4use serde::Deserialize;
5
6pub struct ToolSearchTool {
7    tool_names: Vec<(String, String)>, // (name, description)
8}
9
10impl ToolSearchTool {
11    pub fn new(tools: &[Box<dyn Tool>]) -> Self {
12        Self {
13            tool_names: tools
14                .iter()
15                .map(|t| (t.name().to_string(), t.description().to_string()))
16                .collect(),
17        }
18    }
19}
20
21#[async_trait]
22impl Tool for ToolSearchTool {
23    fn name(&self) -> &str {
24        "ToolSearch"
25    }
26    fn description(&self) -> &str {
27        "Search for available tools by keyword. Returns matching tool names and descriptions."
28    }
29    fn permission_level(&self) -> PermissionLevel {
30        PermissionLevel::None
31    }
32
33    fn input_schema(&self) -> Value {
34        serde_json::json!({
35            "type": "object",
36            "properties": {
37                "query": { "type": "string", "description": "Search query (matches against tool names and descriptions)" }
38            },
39            "required": ["query"]
40        })
41    }
42
43    async fn execute(&self, input: Value, _ctx: &ToolContext) -> ToolResult {
44        #[derive(Deserialize)]
45        struct Input {
46            query: String,
47        }
48
49        let input: Input = match serde_json::from_value(input) {
50            Ok(i) => i,
51            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
52        };
53
54        let query = input.query.to_lowercase();
55        let matches: Vec<String> = self
56            .tool_names
57            .iter()
58            .filter(|(name, desc)| {
59                name.to_lowercase().contains(&query) || desc.to_lowercase().contains(&query)
60            })
61            .map(|(name, desc)| format!("- **{}**: {}", name, desc))
62            .collect();
63
64        if matches.is_empty() {
65            ToolResult::success(format!("No tools found matching '{}'", input.query))
66        } else {
67            ToolResult::success(format!(
68                "Found {} tool(s) matching '{}':\n{}",
69                matches.len(),
70                input.query,
71                matches.join("\n")
72            ))
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::file_read::FileReadTool;
81    use crate::file_write::FileWriteTool;
82    use crate::permissions::AllowAll;
83    use std::sync::Arc;
84
85    fn test_ctx() -> ToolContext {
86        ToolContext {
87            working_dir: std::env::temp_dir(),
88            session_id: "test".into(),
89            permissions: Arc::new(AllowAll),
90            cost_tracker: Arc::new(CostTracker::new()),
91            mcp_manager: None,
92            extensions: Extensions::default(),
93        }
94    }
95
96    #[tokio::test]
97    async fn test_search_file() {
98        let tools: Vec<Box<dyn Tool>> = vec![Box::new(FileReadTool), Box::new(FileWriteTool)];
99        let search = ToolSearchTool::new(&tools);
100        let result = search
101            .execute(serde_json::json!({"query": "file"}), &test_ctx())
102            .await;
103        assert!(!result.is_error);
104        assert!(result.content.contains("Read"));
105        assert!(result.content.contains("Write"));
106    }
107
108    #[tokio::test]
109    async fn test_search_no_match() {
110        let tools: Vec<Box<dyn Tool>> = vec![Box::new(FileReadTool)];
111        let search = ToolSearchTool::new(&tools);
112        let result = search
113            .execute(serde_json::json!({"query": "xyz123"}), &test_ctx())
114            .await;
115        assert!(result.content.contains("No tools found"));
116    }
117}