claude_code_acp/mcp/tools/
web_search.rs

1//! WebSearch tool for searching the web
2//!
3//! Searches the web and returns results to inform responses.
4//! Note: Full implementation requires external search API integration.
5
6use 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/// Input parameters for WebSearch
14#[derive(Debug, Deserialize)]
15struct WebSearchInput {
16    /// The search query
17    query: String,
18    /// Domain filter - only include results from these domains
19    #[serde(default)]
20    allowed_domains: Option<Vec<String>>,
21    /// Domain filter - exclude results from these domains
22    #[serde(default)]
23    blocked_domains: Option<Vec<String>>,
24}
25
26/// WebSearch tool for searching the web
27#[derive(Debug, Default)]
28pub struct WebSearchTool;
29
30impl WebSearchTool {
31    /// Create a new WebSearch tool
32    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        // Parse input
76        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        // Validate query
82        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        // Note: Full implementation would:
93        // 1. Call an external search API (Google, Bing, etc.)
94        // 2. Filter results by allowed/blocked domains
95        // 3. Format results as markdown with hyperlinks
96        // 4. Return structured search results
97
98        // For now, return a placeholder indicating the tool is available
99        // but requires external search API integration
100        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        // Should succeed (stub implementation)
173        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}