MCP DuckDuckGo Search Plugin
- mcp_duckduckgo
"""
MCP resource definitions for the DuckDuckGo search plugin.
"""
from mcp.server.fastmcp import Context
import httpx
from bs4 import BeautifulSoup
from .server import mcp
@mcp.resource("docs://search") # noqa: F401 # pragma: no cover
def get_search_docs() -> str: # vulture: ignore
"""
Provides documentation about the DuckDuckGo search functionality.
"""
return """
# DuckDuckGo Search API
This MCP server provides a web search capability using DuckDuckGo.
## Usage
Use the `duckduckgo_web_search` tool to search the web:
```python
result = await duckduckgo_web_search(
query="your search query",
count=10, # Number of results (1-20)
offset=0 # For pagination
)
```
## Response Format
The search returns a structured response with:
- `results`: List of search results, each containing:
- `title`: The title of the result
- `url`: The URL of the result
- `description`: A snippet or description of the result
- `published_date`: Publication date if available
- `total_results`: Total number of results found
## Limitations
- Maximum query length is 400 characters or 50 words
- Results are limited to 20 per request
- This is a simplified implementation using DuckDuckGo Lite
"""
@mcp.resource("search://{query}") # noqa: F401 # pragma: no cover
async def get_search_results(query: str) -> str: # vulture: ignore
"""
Provides search results for a specific query as a resource.
Args:
query: The search query
Returns:
Formatted search results as text
"""
# Create a simple search function that doesn't require the context
async def simple_search(query: str, count: int = 5):
url = "https://lite.duckduckgo.com/lite/"
async with httpx.AsyncClient(
timeout=10.0,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
}
) as client:
response = await client.post(
url,
data={
"q": query,
"kl": "wt-wt", # No region localization
},
timeout=10.0,
)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
results = []
# Find all result rows in the HTML
result_rows = soup.find_all("tr", class_="result-link")
result_snippets = soup.find_all("tr", class_="result-snippet")
total_results = len(result_rows)
# Extract only the requested number of results
for i in range(min(count, len(result_rows))):
title_elem = result_rows[i].find("a")
if not title_elem:
continue
title = title_elem.text.strip()
url = title_elem.get("href", "")
description = ""
if i < len(result_snippets):
description = result_snippets[i].text.strip()
results.append({
"title": title,
"url": url,
"description": description,
"published_date": None,
})
return {
"results": results,
"total_results": total_results,
}
# Perform the search
result = await simple_search(query)
# Format the results as markdown
formatted_results = f"# Search Results for: {query}\n\n"
for i, item in enumerate(result["results"], 1):
formatted_results += f"## {i}. {item['title']}\n"
formatted_results += f"URL: {item['url']}\n\n"
formatted_results += f"{item['description']}\n\n"
if item.get('published_date'):
formatted_results += f"Published: {item['published_date']}\n\n"
formatted_results += "---\n\n"
formatted_results += f"\nTotal results found: {result['total_results']}"
return formatted_results