claude_code_acp/mcp/tools/
web_search.rs1use async_trait::async_trait;
7use serde::Deserialize;
8use serde_json::{Value, json};
9
10use super::base::Tool;
11use crate::mcp::registry::{ToolContext, ToolResult};
12
13#[derive(Debug, Deserialize)]
15struct WebSearchInput {
16 query: String,
18 #[serde(default)]
20 allowed_domains: Option<Vec<String>>,
21 #[serde(default)]
23 blocked_domains: Option<Vec<String>>,
24}
25
26#[derive(Debug, Default)]
28pub struct WebSearchTool;
29
30impl WebSearchTool {
31 pub fn new() -> Self {
33 Self
34 }
35}
36
37#[async_trait]
38impl Tool for WebSearchTool {
39 fn name(&self) -> &str {
40 "WebSearch"
41 }
42
43 fn description(&self) -> &str {
44 "Searches the web and uses the results to inform responses. \
45 Provides up-to-date information for current events and recent data. \
46 Use this tool for accessing information beyond the model's knowledge cutoff. \
47 Returns search result information including links as markdown hyperlinks."
48 }
49
50 fn input_schema(&self) -> Value {
51 json!({
52 "type": "object",
53 "required": ["query"],
54 "properties": {
55 "query": {
56 "type": "string",
57 "minLength": 2,
58 "description": "The search query to use"
59 },
60 "allowed_domains": {
61 "type": "array",
62 "items": {"type": "string"},
63 "description": "Only include search results from these domains"
64 },
65 "blocked_domains": {
66 "type": "array",
67 "items": {"type": "string"},
68 "description": "Never include search results from these domains"
69 }
70 }
71 })
72 }
73
74 async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
75 let params: WebSearchInput = match serde_json::from_value(input) {
77 Ok(p) => p,
78 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
79 };
80
81 if params.query.len() < 2 {
83 return ToolResult::error("Search query must be at least 2 characters");
84 }
85
86 tracing::info!(
87 "WebSearch request for query: {} (session: {})",
88 params.query,
89 context.session_id
90 );
91
92 let mut output = format!(
101 "WebSearch is available but requires search API integration.\n\n\
102 Search query: {}\n",
103 params.query
104 );
105
106 if let Some(ref allowed) = params.allowed_domains {
107 output.push_str(&format!("Allowed domains: {}\n", allowed.join(", ")));
108 }
109 if let Some(ref blocked) = params.blocked_domains {
110 output.push_str(&format!("Blocked domains: {}\n", blocked.join(", ")));
111 }
112
113 output.push_str(
114 "\nTo fully implement this tool, integrate with a search API \
115 (e.g., Google Custom Search, Bing Search API, or SerpAPI).",
116 );
117
118 ToolResult::success(output).with_metadata(json!({
119 "query": params.query,
120 "allowed_domains": params.allowed_domains,
121 "blocked_domains": params.blocked_domains,
122 "status": "stub_implementation"
123 }))
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use tempfile::TempDir;
131
132 #[test]
133 fn test_web_search_properties() {
134 let tool = WebSearchTool::new();
135 assert_eq!(tool.name(), "WebSearch");
136 assert!(tool.description().contains("search"));
137 assert!(tool.description().contains("web"));
138 }
139
140 #[test]
141 fn test_web_search_input_schema() {
142 let tool = WebSearchTool::new();
143 let schema = tool.input_schema();
144
145 assert_eq!(schema["type"], "object");
146 assert!(schema["properties"]["query"].is_object());
147 assert!(schema["properties"]["allowed_domains"].is_object());
148 assert!(schema["properties"]["blocked_domains"].is_object());
149 assert!(
150 schema["required"]
151 .as_array()
152 .unwrap()
153 .contains(&json!("query"))
154 );
155 }
156
157 #[tokio::test]
158 async fn test_web_search_execute() {
159 let temp_dir = TempDir::new().unwrap();
160 let tool = WebSearchTool::new();
161 let context = ToolContext::new("test-session", temp_dir.path());
162
163 let result = tool
164 .execute(
165 json!({
166 "query": "Rust programming language"
167 }),
168 &context,
169 )
170 .await;
171
172 assert!(!result.is_error);
174 assert!(result.content.contains("WebSearch"));
175 assert!(result.content.contains("Rust programming language"));
176 }
177
178 #[tokio::test]
179 async fn test_web_search_with_domains() {
180 let temp_dir = TempDir::new().unwrap();
181 let tool = WebSearchTool::new();
182 let context = ToolContext::new("test-session", temp_dir.path());
183
184 let result = tool
185 .execute(
186 json!({
187 "query": "Rust docs",
188 "allowed_domains": ["doc.rust-lang.org", "docs.rs"],
189 "blocked_domains": ["stackoverflow.com"]
190 }),
191 &context,
192 )
193 .await;
194
195 assert!(!result.is_error);
196 assert!(result.content.contains("doc.rust-lang.org"));
197 assert!(result.content.contains("stackoverflow.com"));
198 }
199
200 #[tokio::test]
201 async fn test_web_search_short_query() {
202 let temp_dir = TempDir::new().unwrap();
203 let tool = WebSearchTool::new();
204 let context = ToolContext::new("test-session", temp_dir.path());
205
206 let result = tool
207 .execute(
208 json!({
209 "query": "a"
210 }),
211 &context,
212 )
213 .await;
214
215 assert!(result.is_error);
216 assert!(result.content.contains("2 characters"));
217 }
218}