Files
mcp-memory/server/src/handlers/http.rs
T

144 lines
4.3 KiB
Rust

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::<FetchUrlTool>(
"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<MemoryState>) -> crate::error::Result<String> {
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"));
}
}