mcp_servers/coding/tools/web_fetch/
http_client.rs1use futures::future::BoxFuture;
2use reqwest::header::CONTENT_TYPE;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7use crate::coding::error::WebFetchError;
8
9pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
10pub const MAX_TIMEOUT_MS: u64 = 60_000;
11const USER_AGENT: &str = "Mozilla/5.0 (compatible; MCP-Lexicon/1.0)";
12
13#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "camelCase")]
16pub struct WebFetchInput {
17 pub url: String,
19
20 pub prompt: Option<String>,
23
24 pub timeout: Option<u64>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "camelCase")]
31pub struct WebFetchOutput {
32 pub content: String,
34
35 pub final_url: String,
37
38 pub status_code: u16,
40
41 pub title: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub byline: Option<String>,
47}
48
49#[derive(Debug, Clone)]
51pub struct HttpResponse {
52 pub final_url: String,
53 pub status_code: u16,
54 pub body: String,
55 pub content_type: Option<String>,
56}
57
58pub trait HttpClient: Send + Sync {
60 fn fetch<'a>(&'a self, url: &'a str, timeout: Duration) -> BoxFuture<'a, Result<HttpResponse, WebFetchError>>;
61}
62
63#[derive(Debug, Clone)]
65pub struct ReqwestClient {
66 client: reqwest::Client,
67}
68
69impl Default for ReqwestClient {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl ReqwestClient {
76 pub fn new() -> Self {
77 let client = reqwest::Client::builder().user_agent(USER_AGENT).build().expect("Failed to build HTTP client");
78
79 Self { client }
80 }
81}
82
83impl HttpClient for ReqwestClient {
84 fn fetch<'a>(&'a self, url: &'a str, timeout: Duration) -> BoxFuture<'a, Result<HttpResponse, WebFetchError>> {
85 Box::pin(async move {
86 let response = self.client.get(url).timeout(timeout).send().await.map_err(|e| {
87 if e.is_timeout() {
88 WebFetchError::Timeout(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
89 } else {
90 WebFetchError::RequestFailed(e.to_string())
91 }
92 })?;
93
94 let final_url = response.url().to_string();
95 let status_code = response.status().as_u16();
96 let content_type =
97 response.headers().get(CONTENT_TYPE).and_then(|value| value.to_str().ok()).map(str::to_owned);
98
99 let body = response.text().await.map_err(|e| WebFetchError::RequestFailed(e.to_string()))?;
100
101 Ok(HttpResponse { final_url, status_code, body, content_type })
102 })
103 }
104}