| 53 | } |
| 54 | |
| 55 | async fn execute(&self, args: &serde_json::Value, _ctx: &ToolContext) -> Result<ToolOutput> { |
| 56 | let url = match args.get("url").and_then(|v| v.as_str()) { |
| 57 | Some(u) => u, |
| 58 | None => return Ok(ToolOutput::error("url parameter is required")), |
| 59 | }; |
| 60 | |
| 61 | if !url.starts_with("http://") && !url.starts_with("https://") { |
| 62 | return Ok(ToolOutput::error("URL must start with http:// or https://")); |
| 63 | } |
| 64 | |
| 65 | let format = args |
| 66 | .get("format") |
| 67 | .and_then(|v| v.as_str()) |
| 68 | .unwrap_or("markdown"); |
| 69 | |
| 70 | let timeout_secs = args |
| 71 | .get("timeout") |
| 72 | .and_then(|v| v.as_u64()) |
| 73 | .unwrap_or(30) |
| 74 | .min(120); |
| 75 | |
| 76 | let client = reqwest::Client::builder() |
| 77 | .timeout(std::time::Duration::from_secs(timeout_secs)) |
| 78 | .user_agent("a3s-code/0.7") |
| 79 | .build() |
| 80 | .unwrap_or_else(|_| reqwest::Client::new()); |
| 81 | |
| 82 | let response = match client.get(url).send().await { |
| 83 | Ok(r) => r, |
| 84 | Err(e) => { |
| 85 | return Ok(ToolOutput::error(format!( |
| 86 | "Failed to fetch URL {}: {}", |
| 87 | url, e |
| 88 | ))) |
| 89 | } |
| 90 | }; |
| 91 | |
| 92 | let status = response.status(); |
| 93 | if !status.is_success() { |
| 94 | return Ok(ToolOutput::error(format!( |
| 95 | "HTTP {} for URL: {}", |
| 96 | status, url |
| 97 | ))); |
| 98 | } |
| 99 | |
| 100 | let content_type = response |
| 101 | .headers() |
| 102 | .get("content-type") |
| 103 | .and_then(|v| v.to_str().ok()) |
| 104 | .unwrap_or("") |
| 105 | .to_string(); |
| 106 | |
| 107 | let bytes = match response.bytes().await { |
| 108 | Ok(b) => b, |
| 109 | Err(e) => { |
| 110 | return Ok(ToolOutput::error(format!( |
| 111 | "Failed to read response body: {}", |
| 112 | e |