We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ciphernaut/katamari-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Web search capability implementation.
"""
import asyncio
import aiohttp
from typing import Dict, Any, List, Optional
from urllib.parse import quote_plus
import logging
logger = logging.getLogger(__name__)
class WebSearchCapability:
"""Tokenless web search using multiple search engines."""
def __init__(self):
self.session: Optional[aiohttp.ClientSession] = None
async def _ensure_session(self):
"""Ensure HTTP session is available."""
if self.session is None or self.session.closed:
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
headers={'User-Agent': 'Mozilla/5.0 (compatible; KatamariMCP/1.0)'}
)
async def search_duckduckgo(self, query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""Search using DuckDuckGo HTML version."""
await self._ensure_session()
try:
url = f"https://html.duckduckgo.com/html/?q={quote_plus(query)}"
async with self.session.get(url) as response:
if response.status != 200:
raise Exception(f"DuckDuckGo search failed: {response.status}")
html = await response.text()
return self._parse_duckduckgo_results(html, max_results)
except Exception as e:
logger.error(f"DuckDuckGo search error: {e}")
return []
def _parse_duckduckgo_results(self, html: str, max_results: int) -> List[Dict[str, Any]]:
"""Parse DuckDuckGo HTML results."""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
results = []
# Find result divs
result_divs = soup.find_all('div', class_='result')
for div in result_divs[:max_results]:
try:
title_tag = div.find('a', class_='result__a')
snippet_tag = div.find('a', class_='result__snippet')
if title_tag:
title = title_tag.get_text(strip=True)
url = title_tag.get('href', '')
snippet = snippet_tag.get_text(strip=True) if snippet_tag else ''
results.append({
'title': title,
'url': url,
'snippet': snippet,
'source': 'duckduckgo'
})
except Exception as e:
logger.warning(f"Error parsing result: {e}")
continue
return results
async def search_brave(self, query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""Search using Brave Search API (free tier)."""
await self._ensure_session()
try:
# Brave Search API - no key required for basic usage
url = f"https://api.search.brave.com/res/v1/web/search"
params = {
'q': query,
'count': max_results,
'text_decorations': 'false'
}
headers = {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (compatible; KatamariMCP/1.0)'
}
async with self.session.get(url, params=params, headers=headers) as response:
if response.status != 200:
raise Exception(f"Brave search failed: {response.status}")
data = await response.json()
return self._parse_brave_results(data)
except Exception as e:
logger.error(f"Brave search error: {e}")
return []
def _parse_brave_results(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Parse Brave Search API results."""
results = []
if 'web' in data and 'results' in data['web']:
for item in data['web']['results']:
results.append({
'title': item.get('title', ''),
'url': item.get('url', ''),
'snippet': item.get('description', ''),
'source': 'brave'
})
return results
async def search(self, query: str, max_results: int = 5) -> Dict[str, Any]:
"""Perform web search using multiple engines."""
if not query or not query.strip():
raise ValueError("Query cannot be empty")
# Try multiple search engines in parallel
tasks = [
self.search_duckduckgo(query, max_results),
self.search_brave(query, max_results)
]
results_lists = await asyncio.gather(*tasks, return_exceptions=True)
all_results = []
sources_used = []
for i, results in enumerate(results_lists):
if isinstance(results, Exception):
logger.warning(f"Search engine {i} failed: {results}")
continue
if results and isinstance(results, list):
all_results.extend(results)
sources_used.append(['duckduckgo', 'brave'][i])
# Remove duplicates based on URL
seen_urls = set()
unique_results = []
for result in all_results:
url = result.get('url', '')
if url and url not in seen_urls:
seen_urls.add(url)
unique_results.append(result)
# Limit results
final_results = unique_results[:max_results]
return {
'query': query,
'results': final_results,
'total_results': len(final_results),
'sources_used': sources_used,
'status': 'success' if final_results else 'no_results'
}
async def close(self):
"""Close HTTP session."""
if self.session and not self.session.closed:
await self.session.close()
# Global instance
_web_search = WebSearchCapability()
async def web_search(query: str, max_results: int = 5) -> Dict[str, Any]:
"""Main web search function."""
return await _web_search.search(query, max_results)
async def close_web_search():
"""Cleanup function."""
if _web_search.session and not _web_search.session.closed:
await _web_search.session.close()