claude_code_acp/mcp/tools/
web_fetch.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 WebFetchInput {
16 url: String,
18 prompt: String,
20}
21
22#[derive(Debug, Default)]
24pub struct WebFetchTool;
25
26impl WebFetchTool {
27 pub fn new() -> Self {
29 Self
30 }
31
32 fn validate_url(url: &str) -> Result<(), String> {
34 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 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 if let Err(e) = Self::validate_url(¶ms.url) {
85 return ToolResult::error(e);
86 }
87
88 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 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 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 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 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}