cersei_tools/
tool_search.rs1use super::*;
4use serde::Deserialize;
5
6pub struct ToolSearchTool {
7 tool_names: Vec<(String, String)>, }
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}