use crate::router::McpTool; use crate::state::MemoryState; use crate::tools::FetchUrlTool; use async_trait::async_trait; 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 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 method_str = req.method.unwrap_or_else(|| "GET".to_string()); let headers: Vec = req.headers.unwrap_or_default().into_iter() .map(|(k, v)| format!("{}={}", k, v)) .collect(); // Pass to our agentic-tools-rs library directly let res = agentic_tools_rs::commands::llmfetch::run( &req.url, &method_str, req.body.as_ref(), &headers, true, // Always condense for MCP ).await.map_err(|e| { crate::error::AppError::Internal(format!("Agentic fetch failed: {}", e)) })?; Ok(res) } } #[cfg(test)] mod tests { use super::*; use axum::{routing::{get, post}, 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", ) }), ) .route("/echo", post(|body: String| async move { format!("ECHO: {}", body) })); 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_in_memory()), ) .await .unwrap(); assert!(res.contains("HTTP Status: 200 OK")); assert!(res.contains("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_in_memory()), ) .await .unwrap(); assert!(res.contains("HTTP Status: 404 Not Found")); assert!(res.contains("Not Found Error")); } #[tokio::test] async fn test_fetch_url_post() { let base_url = spawn_test_server().await; let handler = FetchUrlHandler; let url = format!("{}/echo", base_url); let res = handler .execute( serde_json::json!({ "url": url, "method": "POST", "body": "test_body_content" }), Arc::new(MemoryState::new_in_memory()), ) .await .unwrap(); assert!(res.contains("HTTP Status: 200 OK")); assert!(res.contains("ECHO: test_body_content")); } }