use reqwest::header::CONTENT_TYPE;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::time::Duration;
use crate::coding::error::WebFetchError;
pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
pub const MAX_TIMEOUT_MS: u64 = 60_000;
pub const MAX_CONTENT_LENGTH: usize = 50_000;
const USER_AGENT: &str = "Mozilla/5.0 (compatible; MCP-Lexicon/1.0)";
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WebFetchInput {
pub url: String,
pub prompt: Option<String>,
pub timeout: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WebFetchOutput {
pub content: String,
pub final_url: String,
pub status_code: u16,
pub truncated: bool,
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub byline: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
#[schemars(skip)]
pub meta: Option<utils::display_meta::ToolResultMeta>,
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub final_url: String,
pub status_code: u16,
pub body: String,
pub content_type: Option<String>,
}
pub trait HttpClient: Send + Sync {
fn fetch(&self, url: &str, timeout: Duration) -> impl Future<Output = Result<HttpResponse, WebFetchError>> + Send;
}
#[derive(Debug, Clone)]
pub struct ReqwestClient {
client: reqwest::Client,
}
impl Default for ReqwestClient {
fn default() -> Self {
Self::new()
}
}
impl ReqwestClient {
pub fn new() -> Self {
let client = reqwest::Client::builder().user_agent(USER_AGENT).build().expect("Failed to build HTTP client");
Self { client }
}
}
impl HttpClient for ReqwestClient {
async fn fetch(&self, url: &str, timeout: Duration) -> Result<HttpResponse, WebFetchError> {
let response = self.client.get(url).timeout(timeout).send().await.map_err(|e| {
if e.is_timeout() {
WebFetchError::Timeout(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
} else {
WebFetchError::RequestFailed(e.to_string())
}
})?;
let final_url = response.url().to_string();
let status_code = response.status().as_u16();
let content_type =
response.headers().get(CONTENT_TYPE).and_then(|value| value.to_str().ok()).map(str::to_owned);
let body = response.text().await.map_err(|e| WebFetchError::RequestFailed(e.to_string()))?;
Ok(HttpResponse { final_url, status_code, body, content_type })
}
}