use crate::router::McpTool; use crate::state::MemoryState; use crate::tools::FetchUrlTool; use async_trait::async_trait; use reqwest::Client; use serde_json::Value; use std::sync::Arc; pub struct FetchUrlHandler; #[async_trait] impl McpTool for FetchUrlHandler { fn name(&self) -> &'static str { "fetch_url" } fn schema(&self) -> Value { crate::mcp::tool_def::( "fetch_url", "Fetch content from a URL via an HTTP GET request natively (supports proxy config via environment variables: HTTP_PROXY, HTTPS_PROXY, NO_PROXY)", ) } async fn execute(&self, args: Value, _state: Arc) -> crate::error::Result { let req: FetchUrlTool = serde_json::from_value(args).map_err(|e| { crate::error::AppError::BadRequest(format!( "Schema validation failed. Your JSON arguments do not match the expected tool schema: {}", e )) })?; let mut client_builder = Client::builder(); // Add optional user agent if let Some(ua) = req.user_agent { client_builder = client_builder.user_agent(ua); } else { client_builder = client_builder.user_agent("mcp-memory-server/1.0"); } // Allow bypassing SSL validation for self-signed certificates if req.ignore_ssl_errors.unwrap_or(false) { client_builder = client_builder.danger_accept_invalid_certs(true); } // Reqwest automatically uses HTTP_PROXY, HTTPS_PROXY, NO_PROXY // environment variables by default, so we don't need to manually // extract and apply them, the builder handles it. let client = client_builder.build().map_err(|e| { crate::error::AppError::Internal(format!("Failed to build HTTP client: {}", e)) })?; let res = client.get(&req.url).send().await.map_err(|e| { crate::error::AppError::Internal(format!("HTTP request to {} failed: {}", req.url, e)) })?; let status = res.status(); let content = res.text().await.map_err(|e| { crate::error::AppError::Internal(format!("Failed to read response body: {}", e)) })?; if !status.is_success() { return Ok(format!( "HTTP Error: {} {}\n\nResponse Body:\n{}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown"), content )); } Ok(content) } } #[cfg(test)] mod tests { use super::*; use axum::{routing::get, Router}; use tokio::net::TcpListener; async fn spawn_test_server() -> String { let app = Router::new() .route("/test", get(|| async { "Hello, World!" })) .route( "/not_found", get(|| async { ( axum::http::StatusCode::NOT_FOUND, "Not Found Error", ) }), ); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); format!("http://127.0.0.1:{}", port) } #[tokio::test] async fn test_fetch_url_success() { let base_url = spawn_test_server().await; let handler = FetchUrlHandler; let url = format!("{}/test", base_url); let res = handler .execute( serde_json::json!({ "url": url }), Arc::new(MemoryState::new("")), ) .await .unwrap(); assert_eq!(res, "Hello, World!"); } #[tokio::test] async fn test_fetch_url_not_found() { let base_url = spawn_test_server().await; let handler = FetchUrlHandler; let url = format!("{}/not_found", base_url); let res = handler .execute( serde_json::json!({ "url": url }), Arc::new(MemoryState::new("")), ) .await .unwrap(); assert!(res.contains("HTTP Error: 404 Not Found")); assert!(res.contains("Not Found Error")); } }