Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ ANTHROPIC_MODEL_CHAT=claude-3-5-sonnet-latest
# --- Google Gemini (optional, used by MCP server) ---
GOOGLE_API_KEY=

# --- Search Provider ---
SEARCH_PROVIDER=duckduckgo # duckduckgo | tavily
TAVILY_API_KEY= # Required when SEARCH_PROVIDER=tavily

# --- Storage ---
CHROMA_DIR=.chroma
SQLITE_PATH=.sqlite/agent.db
Expand Down
6 changes: 3 additions & 3 deletions mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ async def summarize(req: SummarizeRequest) -> Dict[str, Any]:

@self.app.get("/search")
async def search(q: str, max_results: int = 5) -> Dict[str, Any]:
"""Perform a web search using DuckDuckGo."""
results = await webtools.search_ddg(q, max_results=max_results)
"""Perform a web search."""
results = await webtools.search_web(q, max_results=max_results)
return {"query": q, "results": results}

@self.app.get("/browse")
Expand All @@ -84,7 +84,7 @@ async def browse(url: str) -> Dict[str, Any]:
@self.app.get("/research")
async def research(q: str, max_results: int = 3) -> Dict[str, Any]:
"""Conduct a search and fetch the contents of top results."""
results = await webtools.search_ddg(q, max_results=max_results)
results = await webtools.search_web(q, max_results=max_results)
pages: List[Dict[str, str]] = []
for res in results:
url = res.get("href") or res.get("url")
Expand Down
29 changes: 29 additions & 0 deletions mcp/tools/web.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import os

import httpx
import trafilatura
from bs4 import BeautifulSoup
Expand All @@ -14,6 +16,33 @@ async def search_ddg(q: str, max_results: int = 5):
return []


async def search_tavily(q: str, max_results: int = 5):
"""Search using the Tavily API, returning results normalised to {title, href, body}."""
from tavily import AsyncTavilyClient

try:
client = AsyncTavilyClient(api_key=os.environ.get("TAVILY_API_KEY", ""))
response = await client.search(query=q, max_results=max_results)
except Exception:
return []
results = []
for r in response.get("results", []):
results.append({
"title": r.get("title", ""),
"href": r.get("url", ""),
"body": r.get("content", ""),
})
return results
Comment on lines +23 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation uses a broad except Exception block that silently returns an empty list, which masks configuration issues (like a missing TAVILY_API_KEY) or API errors. Additionally, processing the response outside the try block could lead to unhandled exceptions if the response is malformed.

Consider checking for the API key explicitly and wrapping the entire search and normalization logic in the try block, ideally with logging for better observability.

Suggested change
try:
client = AsyncTavilyClient(api_key=os.environ.get("TAVILY_API_KEY", ""))
response = await client.search(query=q, max_results=max_results)
except Exception:
return []
results = []
for r in response.get("results", []):
results.append({
"title": r.get("title", ""),
"href": r.get("url", ""),
"body": r.get("content", ""),
})
return results
api_key = os.environ.get("TAVILY_API_KEY")
if not api_key:
return []
try:
client = AsyncTavilyClient(api_key=api_key)
response = await client.search(query=q, max_results=max_results)
return [
{
"title": r.get("title", ""),
"href": r.get("url", ""),
"body": r.get("content", ""),
}
for r in response.get("results", [])
]
except Exception:
# Consider adding logging here to capture API or network errors
return []



async def search_web(q: str, max_results: int = 5):
"""Dispatch to the configured search provider (duckduckgo or tavily)."""
provider = os.environ.get("SEARCH_PROVIDER", "duckduckgo").lower()
if provider == "tavily":
return await search_tavily(q, max_results=max_results)
return await search_ddg(q, max_results=max_results)


async def fetch_page(url: str) -> str:
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=15)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ typing-extensions>=4.12.2

# Discovery
duckduckgo-search>=6.2.10
tavily-python>=0.3.0
httpx>=0.27.0
trafilatura>=1.10.0
beautifulsoup4>=4.12.3
Expand Down