use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{Value, json};
use super::base::Tool;
use crate::mcp::registry::{ToolContext, ToolResult};
#[derive(Debug, Deserialize)]
struct WebFetchInput {
url: String,
prompt: String,
}
#[derive(Debug, Default)]
pub struct WebFetchTool;
impl WebFetchTool {
pub fn new() -> Self {
Self
}
fn validate_url(url: &str) -> Result<(), String> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err("URL must start with http:// or https://".to_string());
}
if url.len() < 10 {
return Err("URL is too short".to_string());
}
Ok(())
}
}
#[async_trait]
impl Tool for WebFetchTool {
fn name(&self) -> &str {
"WebFetch"
}
fn description(&self) -> &str {
"Fetches content from a specified URL and processes it using an AI model. \
Takes a URL and a prompt as input, fetches the URL content, converts HTML to markdown, \
and processes the content with the prompt. Use this tool when you need to retrieve \
and analyze web content."
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"required": ["url", "prompt"],
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "The URL to fetch content from"
},
"prompt": {
"type": "string",
"description": "The prompt to run on the fetched content"
}
}
})
}
async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
let params: WebFetchInput = match serde_json::from_value(input) {
Ok(p) => p,
Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
};
if let Err(e) = Self::validate_url(¶ms.url) {
return ToolResult::error(e);
}
if params.prompt.trim().is_empty() {
return ToolResult::error("Prompt cannot be empty");
}
tracing::info!(
"WebFetch request for URL: {} with prompt: {} (session: {})",
params.url,
params.prompt,
context.session_id
);
let output = format!(
"WebFetch is available but requires HTTP client integration.\n\n\
Requested URL: {}\n\
Prompt: {}\n\n\
To fully implement this tool, add the 'reqwest' crate and configure \
an AI API for content processing.",
params.url, params.prompt
);
ToolResult::success(output).with_metadata(json!({
"url": params.url,
"prompt": params.prompt,
"status": "stub_implementation"
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_web_fetch_properties() {
let tool = WebFetchTool::new();
assert_eq!(tool.name(), "WebFetch");
assert!(tool.description().contains("URL"));
assert!(tool.description().contains("content"));
}
#[test]
fn test_web_fetch_input_schema() {
let tool = WebFetchTool::new();
let schema = tool.input_schema();
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["url"].is_object());
assert!(schema["properties"]["prompt"].is_object());
assert!(
schema["required"]
.as_array()
.unwrap()
.contains(&json!("url"))
);
assert!(
schema["required"]
.as_array()
.unwrap()
.contains(&json!("prompt"))
);
}
#[test]
fn test_validate_url() {
assert!(WebFetchTool::validate_url("https://example.com").is_ok());
assert!(WebFetchTool::validate_url("http://example.com/path").is_ok());
assert!(WebFetchTool::validate_url("https://api.example.com/v1/data").is_ok());
assert!(WebFetchTool::validate_url("ftp://example.com").is_err());
assert!(WebFetchTool::validate_url("example.com").is_err());
assert!(WebFetchTool::validate_url("http://").is_err());
}
#[tokio::test]
async fn test_web_fetch_execute() {
let temp_dir = TempDir::new().unwrap();
let tool = WebFetchTool::new();
let context = ToolContext::new("test-session", temp_dir.path());
let result = tool
.execute(
json!({
"url": "https://example.com",
"prompt": "Extract the main content"
}),
&context,
)
.await;
assert!(!result.is_error);
assert!(result.content.contains("WebFetch"));
assert!(result.content.contains("https://example.com"));
}
#[tokio::test]
async fn test_web_fetch_invalid_url() {
let temp_dir = TempDir::new().unwrap();
let tool = WebFetchTool::new();
let context = ToolContext::new("test-session", temp_dir.path());
let result = tool
.execute(
json!({
"url": "not-a-url",
"prompt": "Extract content"
}),
&context,
)
.await;
assert!(result.is_error);
assert!(result.content.contains("http"));
}
#[tokio::test]
async fn test_web_fetch_empty_prompt() {
let temp_dir = TempDir::new().unwrap();
let tool = WebFetchTool::new();
let context = ToolContext::new("test-session", temp_dir.path());
let result = tool
.execute(
json!({
"url": "https://example.com",
"prompt": ""
}),
&context,
)
.await;
assert!(result.is_error);
assert!(result.content.contains("Prompt"));
}
}