1use std::fmt::Write;
9use std::sync::{Arc, Mutex};
10
11use async_trait::async_trait;
12
13use crate::tools::mcp_deferred::{ActivatedToolSet, DeferredMcpToolSet};
14use crate::tools::traits::{Tool, ToolResult};
15
16const DEFAULT_MAX_RESULTS: usize = 5;
18
19pub struct ToolSearchTool {
21 deferred: DeferredMcpToolSet,
22 activated: Arc<Mutex<ActivatedToolSet>>,
23}
24
25impl ToolSearchTool {
26 pub fn new(deferred: DeferredMcpToolSet, activated: Arc<Mutex<ActivatedToolSet>>) -> Self {
27 Self {
28 deferred,
29 activated,
30 }
31 }
32}
33
34#[async_trait]
35impl Tool for ToolSearchTool {
36 fn name(&self) -> &str {
37 "tool_search"
38 }
39
40 fn description(&self) -> &str {
41 "Fetch full schema definitions for deferred MCP tools so they can be called. \
42 Use \"select:name1,name2\" for exact match or keywords to search."
43 }
44
45 fn parameters_schema(&self) -> serde_json::Value {
46 serde_json::json!({
47 "type": "object",
48 "properties": {
49 "query": {
50 "description": "Query to find deferred tools. Use \"select:<tool_name>\" for direct selection, or keywords to search.",
51 "type": "string"
52 },
53 "max_results": {
54 "description": "Maximum number of results to return (default: 5)",
55 "type": "number",
56 "default": DEFAULT_MAX_RESULTS
57 }
58 },
59 "required": ["query"]
60 })
61 }
62
63 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
64 let query = args
65 .get("query")
66 .and_then(|v| v.as_str())
67 .unwrap_or_default()
68 .trim();
69
70 let max_results = args
71 .get("max_results")
72 .and_then(|v| v.as_u64())
73 .map(|v| usize::try_from(v).unwrap_or(DEFAULT_MAX_RESULTS))
74 .unwrap_or(DEFAULT_MAX_RESULTS);
75
76 if query.is_empty() {
77 return Ok(ToolResult {
78 success: false,
79 output: String::new(),
80 error: Some("query parameter is required".into()),
81 });
82 }
83
84 if let Some(names_str) = query.strip_prefix("select:") {
86 let names: Vec<&str> = names_str.split(',').map(str::trim).collect();
88 return self.select_tools(&names);
89 }
90
91 let results = self.deferred.search(query, max_results);
93 if results.is_empty() {
94 return Ok(ToolResult {
95 success: true,
96 output: "No matching deferred tools found.".into(),
97 error: None,
98 });
99 }
100
101 let mut output = String::from("<functions>\n");
103 let mut activated_count = 0;
104 let mut guard = self.activated.lock().unwrap();
105
106 for stub in &results {
107 if let Some(spec) = self.deferred.tool_spec(&stub.prefixed_name) {
108 if !guard.is_activated(&stub.prefixed_name) {
109 if let Some(tool) = self.deferred.activate(&stub.prefixed_name) {
110 guard.activate(stub.prefixed_name.clone(), Arc::from(tool));
111 activated_count += 1;
112 }
113 }
114 let _ = writeln!(
115 output,
116 "<function>{{\"name\": \"{}\", \"description\": \"{}\", \"parameters\": {}}}</function>",
117 spec.name,
118 spec.description.replace('"', "\\\""),
119 spec.parameters
120 );
121 }
122 }
123
124 output.push_str("</functions>\n");
125 drop(guard);
126
127 tracing::debug!(
128 "tool_search: query={query:?}, matched={}, activated={activated_count}",
129 results.len()
130 );
131
132 Ok(ToolResult {
133 success: true,
134 output,
135 error: None,
136 })
137 }
138}
139
140impl ToolSearchTool {
141 fn select_tools(&self, names: &[&str]) -> anyhow::Result<ToolResult> {
142 let mut output = String::from("<functions>\n");
143 let mut not_found = Vec::new();
144 let mut activated_count = 0;
145 let mut guard = self.activated.lock().unwrap();
146
147 for name in names {
148 if name.is_empty() {
149 continue;
150 }
151 match self.deferred.get_by_name(name) {
153 Some(stub) => {
154 let full_name = &stub.prefixed_name;
155 if let Some(spec) = self.deferred.tool_spec(full_name) {
156 if !guard.is_activated(full_name) {
157 if let Some(tool) = self.deferred.activate(full_name) {
158 guard.activate(full_name.clone(), Arc::from(tool));
159 activated_count += 1;
160 }
161 }
162 let _ = writeln!(
163 output,
164 "<function>{{\"name\": \"{}\", \"description\": \"{}\", \"parameters\": {}}}</function>",
165 spec.name,
166 spec.description.replace('"', "\\\""),
167 spec.parameters
168 );
169 }
170 }
171 None => {
172 not_found.push(*name);
173 }
174 }
175 }
176
177 output.push_str("</functions>\n");
178 drop(guard);
179
180 if !not_found.is_empty() {
181 let _ = write!(output, "\nNot found: {}", not_found.join(", "));
182 }
183
184 tracing::debug!(
185 "tool_search select: requested={}, activated={activated_count}, not_found={}",
186 names.len(),
187 not_found.len()
188 );
189
190 Ok(ToolResult {
191 success: true,
192 output,
193 error: None,
194 })
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::tools::mcp_client::McpRegistry;
202 use crate::tools::mcp_deferred::DeferredMcpToolStub;
203 use crate::tools::mcp_protocol::McpToolDef;
204
205 async fn make_deferred_set(stubs: Vec<DeferredMcpToolStub>) -> DeferredMcpToolSet {
206 let registry = Arc::new(McpRegistry::connect_all(&[]).await.unwrap());
207 DeferredMcpToolSet { stubs, registry }
208 }
209
210 fn make_stub(name: &str, desc: &str) -> DeferredMcpToolStub {
211 let def = McpToolDef {
212 name: name.to_string(),
213 description: Some(desc.to_string()),
214 input_schema: serde_json::json!({"type": "object", "properties": {}}),
215 };
216 DeferredMcpToolStub::new(name.to_string(), def)
217 }
218
219 #[tokio::test]
220 async fn tool_metadata() {
221 let tool = ToolSearchTool::new(
222 make_deferred_set(vec![]).await,
223 Arc::new(Mutex::new(ActivatedToolSet::new())),
224 );
225 assert_eq!(tool.name(), "tool_search");
226 assert!(!tool.description().is_empty());
227 assert!(tool.parameters_schema()["properties"]["query"].is_object());
228 }
229
230 #[tokio::test]
231 async fn empty_query_returns_error() {
232 let tool = ToolSearchTool::new(
233 make_deferred_set(vec![]).await,
234 Arc::new(Mutex::new(ActivatedToolSet::new())),
235 );
236 let result = tool
237 .execute(serde_json::json!({"query": ""}))
238 .await
239 .unwrap();
240 assert!(!result.success);
241 }
242
243 #[tokio::test]
244 async fn select_nonexistent_tool_reports_not_found() {
245 let tool = ToolSearchTool::new(
246 make_deferred_set(vec![]).await,
247 Arc::new(Mutex::new(ActivatedToolSet::new())),
248 );
249 let result = tool
250 .execute(serde_json::json!({"query": "select:nonexistent"}))
251 .await
252 .unwrap();
253 assert!(result.success);
254 assert!(result.output.contains("Not found"));
255 }
256
257 #[tokio::test]
258 async fn keyword_search_no_matches() {
259 let tool = ToolSearchTool::new(
260 make_deferred_set(vec![make_stub("fs__read", "Read file")]).await,
261 Arc::new(Mutex::new(ActivatedToolSet::new())),
262 );
263 let result = tool
264 .execute(serde_json::json!({"query": "zzzzz_nonexistent"}))
265 .await
266 .unwrap();
267 assert!(result.success);
268 assert!(result.output.contains("No matching"));
269 }
270
271 #[tokio::test]
272 async fn keyword_search_finds_match() {
273 let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
274 let tool = ToolSearchTool::new(
275 make_deferred_set(vec![make_stub("fs__read", "Read a file from disk")]).await,
276 Arc::clone(&activated),
277 );
278 let result = tool
279 .execute(serde_json::json!({"query": "read file"}))
280 .await
281 .unwrap();
282 assert!(result.success);
283 assert!(result.output.contains("<function>"));
284 assert!(result.output.contains("fs__read"));
285 assert!(activated.lock().unwrap().is_activated("fs__read"));
287 }
288
289 #[tokio::test]
292 async fn multiple_servers_stubs_all_searchable() {
293 let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
294 let stubs = vec![
295 make_stub("server_a__list_files", "List files on server A"),
296 make_stub("server_a__read_file", "Read file on server A"),
297 make_stub("server_b__query_db", "Query database on server B"),
298 make_stub("server_b__insert_row", "Insert row on server B"),
299 ];
300 let tool = ToolSearchTool::new(make_deferred_set(stubs).await, Arc::clone(&activated));
301
302 let result = tool
304 .execute(serde_json::json!({"query": "file"}))
305 .await
306 .unwrap();
307 assert!(result.success);
308 assert!(result.output.contains("server_a__list_files"));
309 assert!(result.output.contains("server_a__read_file"));
310
311 let result = tool
313 .execute(serde_json::json!({"query": "database query"}))
314 .await
315 .unwrap();
316 assert!(result.success);
317 assert!(result.output.contains("server_b__query_db"));
318 }
319
320 #[tokio::test]
323 async fn select_activates_and_persists_across_calls() {
324 let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
325 let stubs = vec![
326 make_stub("srv__tool_a", "Tool A"),
327 make_stub("srv__tool_b", "Tool B"),
328 ];
329 let tool = ToolSearchTool::new(make_deferred_set(stubs).await, Arc::clone(&activated));
330
331 let result = tool
333 .execute(serde_json::json!({"query": "select:srv__tool_a"}))
334 .await
335 .unwrap();
336 assert!(result.success);
337 assert!(activated.lock().unwrap().is_activated("srv__tool_a"));
338 assert!(!activated.lock().unwrap().is_activated("srv__tool_b"));
339
340 let result = tool
342 .execute(serde_json::json!({"query": "select:srv__tool_b"}))
343 .await
344 .unwrap();
345 assert!(result.success);
346
347 let guard = activated.lock().unwrap();
349 assert!(guard.is_activated("srv__tool_a"));
350 assert!(guard.is_activated("srv__tool_b"));
351 assert_eq!(guard.tool_specs().len(), 2);
352 }
353
354 #[tokio::test]
356 async fn reactivation_is_idempotent() {
357 let activated = Arc::new(Mutex::new(ActivatedToolSet::new()));
358 let tool = ToolSearchTool::new(
359 make_deferred_set(vec![make_stub("srv__tool", "A tool")]).await,
360 Arc::clone(&activated),
361 );
362
363 tool.execute(serde_json::json!({"query": "select:srv__tool"}))
364 .await
365 .unwrap();
366 tool.execute(serde_json::json!({"query": "select:srv__tool"}))
367 .await
368 .unwrap();
369
370 assert_eq!(activated.lock().unwrap().tool_specs().len(), 1);
371 }
372}