use serde_json::{json, Value};
use serp_sdk::{SearchQuery, SerpClient};
use std::env;
pub struct SerpSearchTool {
client: SerpClient,
}
impl SerpSearchTool {
pub fn new(api_key: String) -> Result<Self, Box<dyn std::error::Error>> {
let client = SerpClient::builder().api_key(api_key).build()?;
Ok(Self { client })
}
pub async fn execute_search(&self, params: Value) -> Result<Value, Box<dyn std::error::Error>> {
let query = params["query"]
.as_str()
.ok_or("Missing 'query' parameter")?;
let language = params["language"].as_str();
let country = params["country"].as_str();
let limit = params["limit"].as_u64().map(|n| n as u32);
let search_type = params["type"].as_str();
let mut search_builder = SearchQuery::new(query);
if let Some(lang) = language {
search_builder = search_builder.language(lang);
}
if let Some(country_code) = country {
search_builder = search_builder.country(country_code);
}
if let Some(limit_val) = limit {
search_builder = search_builder.limit(limit_val)?;
}
search_builder = match search_type {
Some("images") => search_builder.images(),
Some("videos") => search_builder.videos(),
Some("news") => search_builder.news(),
Some("shopping") => search_builder.shopping(),
_ => search_builder, };
let results = self.client.search(search_builder).await?;
let mut mcp_results = json!({
"search_metadata": {
"id": results.search_metadata.id,
"status": results.search_metadata.status,
"total_time_taken": results.search_metadata.total_time_taken,
"query": query
}
});
if let Some(organic) = results.organic_results {
let organic_json: Vec<Value> = organic
.into_iter()
.map(|result| {
json!({
"position": result.position,
"title": result.title,
"link": result.link,
"displayed_link": result.displayed_link,
"snippet": result.snippet
})
})
.collect();
mcp_results["organic_results"] = json!(organic_json);
}
if let Some(answer_box) = results.answer_box {
mcp_results["answer_box"] = json!({
"type": answer_box.answer_type,
"title": answer_box.title,
"answer": answer_box.answer,
"snippet": answer_box.snippet
});
}
if let Some(kg) = results.knowledge_graph {
mcp_results["knowledge_graph"] = json!({
"title": kg.title,
"type": kg.knowledge_type,
"description": kg.description
});
}
if let Some(related) = results.related_searches {
let mut related_json: Vec<Value> = Vec::new();
for search in related {
match search {
serp_sdk::response::RelatedSearch::Simple { query, link, .. } => {
related_json.push(json!({
"query": query,
"link": link
}));
}
serp_sdk::response::RelatedSearch::Block { items, .. } => {
for item in items {
if let Some(name) = item.name {
related_json.push(json!({
"query": name,
"link": item.link
}));
}
}
}
}
}
mcp_results["related_searches"] = json!(related_json);
}
Ok(mcp_results)
}
}
pub fn get_tool_schema() -> Value {
json!({
"name": "web_search",
"description": "Search the web using SerpAPI and return structured results",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"language": {
"type": "string",
"description": "Interface language (e.g., 'en', 'es', 'fr')",
"default": "en"
},
"country": {
"type": "string",
"description": "Country for search results (e.g., 'us', 'uk', 'ca')",
"default": "us"
},
"limit": {
"type": "integer",
"description": "Number of results to return (1-100)",
"minimum": 1,
"maximum": 100,
"default": 10
},
"type": {
"type": "string",
"description": "Type of search",
"enum": ["web", "images", "videos", "news", "shopping"],
"default": "web"
}
},
"required": ["query"]
}
})
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let api_key = env::args()
.nth(1)
.or_else(|| env::var("SERP_API_KEY").ok())
.expect("Please provide API key as argument or set SERP_API_KEY environment variable");
let search_tool = SerpSearchTool::new(api_key)?;
println!("🔧 MCP Tool Schema:");
println!("{}", serde_json::to_string_pretty(&get_tool_schema())?);
println!();
println!("🔍 Executing MCP search tool...");
let search_params = json!({
"query": "Rust programming language",
"language": "en",
"country": "us",
"limit": 5,
"type": "web"
});
println!("📋 Input parameters:");
println!("{}", serde_json::to_string_pretty(&search_params)?);
println!();
let results = search_tool.execute_search(search_params).await?;
println!("✅ MCP search results:");
println!("{}", serde_json::to_string_pretty(&results)?);
println!("\n🖼️ Testing image search...");
let image_params = json!({
"query": "rust programming logo",
"type": "images",
"limit": 3
});
let image_results = search_tool.execute_search(image_params).await?;
println!("📸 Image search results:");
println!("{}", serde_json::to_string_pretty(&image_results)?);
println!("\n🎯 MCP integration example completed!");
println!("💡 This tool can be integrated into any MCP-compatible system");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_schema() {
let schema = get_tool_schema();
assert_eq!(schema["name"], "web_search");
assert!(schema["parameters"]["properties"]["query"].is_object());
}
#[tokio::test]
async fn test_search_tool_creation() {
let result = SerpSearchTool::new("test-key".to_string());
assert!(result.is_ok());
}
}