claude_code_acp/mcp/tools/
web_fetch.rs

1//! WebFetch tool for fetching and processing web content
2//!
3//! Fetches content from URLs and processes it using an AI model.
4//! Note: Full implementation requires HTTP client and AI 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 WebFetch
14#[derive(Debug, Deserialize)]
15struct WebFetchInput {
16    /// The URL to fetch content from
17    url: String,
18    /// The prompt to run on the fetched content
19    prompt: String,
20}
21
22/// WebFetch tool for fetching and analyzing web content
23#[derive(Debug, Default)]
24pub struct WebFetchTool;
25
26impl WebFetchTool {
27    /// Create a new WebFetch tool
28    pub fn new() -> Self {
29        Self
30    }
31
32    /// Validate URL format
33    fn validate_url(url: &str) -> Result<(), String> {
34        // Basic URL validation
35        if !url.starts_with("http://") && !url.starts_with("https://") {
36            return Err("URL must start with http:// or https://".to_string());
37        }
38        if url.len() < 10 {
39            return Err("URL is too short".to_string());
40        }
41        Ok(())
42    }
43}
44
45#[async_trait]
46impl Tool for WebFetchTool {
47    fn name(&self) -> &str {
48        "WebFetch"
49    }
50
51    fn description(&self) -> &str {
52        "Fetches content from a specified URL and processes it using an AI model. \
53         Takes a URL and a prompt as input, fetches the URL content, converts HTML to markdown, \
54         and processes the content with the prompt. Use this tool when you need to retrieve \
55         and analyze web content."
56    }
57
58    fn input_schema(&self) -> Value {
59        json!({
60            "type": "object",
61            "required": ["url", "prompt"],
62            "properties": {
63                "url": {
64                    "type": "string",
65                    "format": "uri",
66                    "description": "The URL to fetch content from"
67                },
68                "prompt": {
69                    "type": "string",
70                    "description": "The prompt to run on the fetched content"
71                }
72            }
73        })
74    }
75
76    async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
77        // Parse input
78        let params: WebFetchInput = match serde_json::from_value(input) {
79            Ok(p) => p,
80            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
81        };
82
83        // Validate URL
84        if let Err(e) = Self::validate_url(&params.url) {
85            return ToolResult::error(e);
86        }
87
88        // Validate prompt
89        if params.prompt.trim().is_empty() {
90            return ToolResult::error("Prompt cannot be empty");
91        }
92
93        tracing::info!(
94            "WebFetch request for URL: {} with prompt: {} (session: {})",
95            params.url,
96            params.prompt,
97            context.session_id
98        );
99
100        // Note: Full implementation would:
101        // 1. Use reqwest to fetch the URL content
102        // 2. Convert HTML to markdown
103        // 3. Use AI API to process content with the prompt
104        // 4. Return the processed result
105
106        // For now, return a placeholder indicating the tool is available
107        // but requires external HTTP client integration
108        let output = format!(
109            "WebFetch is available but requires HTTP client integration.\n\n\
110             Requested URL: {}\n\
111             Prompt: {}\n\n\
112             To fully implement this tool, add the 'reqwest' crate and configure \
113             an AI API for content processing.",
114            params.url, params.prompt
115        );
116
117        ToolResult::success(output).with_metadata(json!({
118            "url": params.url,
119            "prompt": params.prompt,
120            "status": "stub_implementation"
121        }))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use tempfile::TempDir;
129
130    #[test]
131    fn test_web_fetch_properties() {
132        let tool = WebFetchTool::new();
133        assert_eq!(tool.name(), "WebFetch");
134        assert!(tool.description().contains("URL"));
135        assert!(tool.description().contains("content"));
136    }
137
138    #[test]
139    fn test_web_fetch_input_schema() {
140        let tool = WebFetchTool::new();
141        let schema = tool.input_schema();
142
143        assert_eq!(schema["type"], "object");
144        assert!(schema["properties"]["url"].is_object());
145        assert!(schema["properties"]["prompt"].is_object());
146        assert!(
147            schema["required"]
148                .as_array()
149                .unwrap()
150                .contains(&json!("url"))
151        );
152        assert!(
153            schema["required"]
154                .as_array()
155                .unwrap()
156                .contains(&json!("prompt"))
157        );
158    }
159
160    #[test]
161    fn test_validate_url() {
162        // Valid URLs
163        assert!(WebFetchTool::validate_url("https://example.com").is_ok());
164        assert!(WebFetchTool::validate_url("http://example.com/path").is_ok());
165        assert!(WebFetchTool::validate_url("https://api.example.com/v1/data").is_ok());
166
167        // Invalid URLs
168        assert!(WebFetchTool::validate_url("ftp://example.com").is_err());
169        assert!(WebFetchTool::validate_url("example.com").is_err());
170        assert!(WebFetchTool::validate_url("http://").is_err());
171    }
172
173    #[tokio::test]
174    async fn test_web_fetch_execute() {
175        let temp_dir = TempDir::new().unwrap();
176        let tool = WebFetchTool::new();
177        let context = ToolContext::new("test-session", temp_dir.path());
178
179        let result = tool
180            .execute(
181                json!({
182                    "url": "https://example.com",
183                    "prompt": "Extract the main content"
184                }),
185                &context,
186            )
187            .await;
188
189        // Should succeed (stub implementation)
190        assert!(!result.is_error);
191        assert!(result.content.contains("WebFetch"));
192        assert!(result.content.contains("https://example.com"));
193    }
194
195    #[tokio::test]
196    async fn test_web_fetch_invalid_url() {
197        let temp_dir = TempDir::new().unwrap();
198        let tool = WebFetchTool::new();
199        let context = ToolContext::new("test-session", temp_dir.path());
200
201        let result = tool
202            .execute(
203                json!({
204                    "url": "not-a-url",
205                    "prompt": "Extract content"
206                }),
207                &context,
208            )
209            .await;
210
211        assert!(result.is_error);
212        assert!(result.content.contains("http"));
213    }
214
215    #[tokio::test]
216    async fn test_web_fetch_empty_prompt() {
217        let temp_dir = TempDir::new().unwrap();
218        let tool = WebFetchTool::new();
219        let context = ToolContext::new("test-session", temp_dir.path());
220
221        let result = tool
222            .execute(
223                json!({
224                    "url": "https://example.com",
225                    "prompt": ""
226                }),
227                &context,
228            )
229            .await;
230
231        assert!(result.is_error);
232        assert!(result.content.contains("Prompt"));
233    }
234}