Spaces:
Sleeping
Sleeping
Add NewsAPI as third news source
Browse files- Add newsapi_search function for NewsAPI.org
- Update search_company_news to fetch from Tavily + NYT + NewsAPI in parallel
- 150,000+ sources coverage (24hr delay on free tier)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
mcp-servers/news-basket/server.py
CHANGED
|
@@ -11,6 +11,7 @@ Use cases:
|
|
| 11 |
Data Sources:
|
| 12 |
- Tavily API: https://docs.tavily.com/ (1,000 credits/month free)
|
| 13 |
- NYT Article Search API: https://developer.nytimes.com/ (500 req/day free)
|
|
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
import asyncio
|
|
@@ -55,6 +56,10 @@ TAVILY_BASE_URL = "https://api.tavily.com"
|
|
| 55 |
NYT_API_KEY = os.getenv("NYT_API_KEY")
|
| 56 |
NYT_BASE_URL = "https://api.nytimes.com/svc/search/v2/articlesearch.json"
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
# ============================================================
|
| 60 |
# SEARCH FUNCTIONS
|
|
@@ -229,20 +234,107 @@ async def nyt_search(
|
|
| 229 |
return {"error": str(e)}
|
| 230 |
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
async def search_company_news(ticker: str, company_name: str = None) -> dict:
|
| 233 |
"""
|
| 234 |
-
Search for recent news about a company using Tavily
|
| 235 |
-
Combines results from
|
| 236 |
"""
|
| 237 |
query = f"{ticker} stock news"
|
| 238 |
if company_name:
|
| 239 |
query = f"{company_name} ({ticker}) stock news"
|
| 240 |
|
| 241 |
-
# Fetch from
|
| 242 |
tavily_task = tavily_search(
|
| 243 |
query=query,
|
| 244 |
search_depth="basic",
|
| 245 |
-
max_results=
|
| 246 |
exclude_domains=["reddit.com", "twitter.com", "x.com"],
|
| 247 |
)
|
| 248 |
|
|
@@ -253,7 +345,16 @@ async def search_company_news(ticker: str, company_name: str = None) -> dict:
|
|
| 253 |
sort="newest",
|
| 254 |
)
|
| 255 |
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
# Combine results
|
| 259 |
all_results = []
|
|
@@ -269,6 +370,11 @@ async def search_company_news(ticker: str, company_name: str = None) -> dict:
|
|
| 269 |
all_results.extend(nyt_result["results"])
|
| 270 |
sources_used.append("NYT")
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
# Build combined result
|
| 273 |
result = {
|
| 274 |
"query": query,
|
|
|
|
| 11 |
Data Sources:
|
| 12 |
- Tavily API: https://docs.tavily.com/ (1,000 credits/month free)
|
| 13 |
- NYT Article Search API: https://developer.nytimes.com/ (500 req/day free)
|
| 14 |
+
- NewsAPI: https://newsapi.org/ (100 req/day free, 24hr delay)
|
| 15 |
"""
|
| 16 |
|
| 17 |
import asyncio
|
|
|
|
| 56 |
NYT_API_KEY = os.getenv("NYT_API_KEY")
|
| 57 |
NYT_BASE_URL = "https://api.nytimes.com/svc/search/v2/articlesearch.json"
|
| 58 |
|
| 59 |
+
# NewsAPI configuration (24hr lag on free tier)
|
| 60 |
+
NEWSAPI_API_KEY = os.getenv("NEWSAPI_API_KEY")
|
| 61 |
+
NEWSAPI_BASE_URL = "https://newsapi.org/v2/everything"
|
| 62 |
+
|
| 63 |
|
| 64 |
# ============================================================
|
| 65 |
# SEARCH FUNCTIONS
|
|
|
|
| 234 |
return {"error": str(e)}
|
| 235 |
|
| 236 |
|
| 237 |
+
async def newsapi_search(
|
| 238 |
+
query: str,
|
| 239 |
+
max_results: int = 5,
|
| 240 |
+
sort_by: str = "publishedAt",
|
| 241 |
+
language: str = "en",
|
| 242 |
+
) -> dict:
|
| 243 |
+
"""
|
| 244 |
+
Search NewsAPI.org for articles.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
query: Search query
|
| 248 |
+
max_results: Number of results (max 100)
|
| 249 |
+
sort_by: "publishedAt", "relevancy", or "popularity"
|
| 250 |
+
language: Language code (e.g., "en")
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
Dict with articles from 150,000+ sources
|
| 254 |
+
|
| 255 |
+
Note: Free tier has 24-hour delay on articles
|
| 256 |
+
"""
|
| 257 |
+
if not NEWSAPI_API_KEY:
|
| 258 |
+
return {
|
| 259 |
+
"error": "NEWSAPI_API_KEY not configured",
|
| 260 |
+
"message": "Add NEWSAPI_API_KEY to ~/.env file. Get free key at https://newsapi.org/"
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
try:
|
| 264 |
+
async with httpx.AsyncClient() as client:
|
| 265 |
+
params = {
|
| 266 |
+
"apiKey": NEWSAPI_API_KEY,
|
| 267 |
+
"q": query,
|
| 268 |
+
"sortBy": sort_by,
|
| 269 |
+
"language": language,
|
| 270 |
+
"pageSize": min(max_results, 100),
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
response = await client.get(
|
| 274 |
+
NEWSAPI_BASE_URL,
|
| 275 |
+
params=params,
|
| 276 |
+
timeout=30
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
if response.status_code == 426:
|
| 280 |
+
return {
|
| 281 |
+
"error": "NewsAPI requires paid plan for this request",
|
| 282 |
+
"message": "Free tier limited to 24hr old articles"
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if response.status_code != 200:
|
| 286 |
+
return {
|
| 287 |
+
"error": f"NewsAPI error: {response.status_code}",
|
| 288 |
+
"message": response.text
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
data = response.json()
|
| 292 |
+
|
| 293 |
+
if data.get("status") != "ok":
|
| 294 |
+
return {
|
| 295 |
+
"error": data.get("code", "Unknown error"),
|
| 296 |
+
"message": data.get("message", "")
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
# Format results
|
| 300 |
+
results = []
|
| 301 |
+
for art in data.get("articles", [])[:max_results]:
|
| 302 |
+
results.append({
|
| 303 |
+
"title": art.get("title", ""),
|
| 304 |
+
"url": art.get("url", ""),
|
| 305 |
+
"content": art.get("description", "") or art.get("content", ""),
|
| 306 |
+
"published_date": art.get("publishedAt", ""),
|
| 307 |
+
"source": art.get("source", {}).get("name", "NewsAPI"),
|
| 308 |
+
})
|
| 309 |
+
|
| 310 |
+
return {
|
| 311 |
+
"query": query,
|
| 312 |
+
"results": results,
|
| 313 |
+
"result_count": len(results),
|
| 314 |
+
"total_hits": data.get("totalResults", 0),
|
| 315 |
+
"source": "NewsAPI",
|
| 316 |
+
"as_of": datetime.now().isoformat()
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
except Exception as e:
|
| 320 |
+
logger.error(f"NewsAPI search error: {e}")
|
| 321 |
+
return {"error": str(e)}
|
| 322 |
+
|
| 323 |
+
|
| 324 |
async def search_company_news(ticker: str, company_name: str = None) -> dict:
|
| 325 |
"""
|
| 326 |
+
Search for recent news about a company using Tavily, NYT, and NewsAPI.
|
| 327 |
+
Combines results from all sources for comprehensive coverage.
|
| 328 |
"""
|
| 329 |
query = f"{ticker} stock news"
|
| 330 |
if company_name:
|
| 331 |
query = f"{company_name} ({ticker}) stock news"
|
| 332 |
|
| 333 |
+
# Fetch from all sources in parallel
|
| 334 |
tavily_task = tavily_search(
|
| 335 |
query=query,
|
| 336 |
search_depth="basic",
|
| 337 |
+
max_results=4,
|
| 338 |
exclude_domains=["reddit.com", "twitter.com", "x.com"],
|
| 339 |
)
|
| 340 |
|
|
|
|
| 345 |
sort="newest",
|
| 346 |
)
|
| 347 |
|
| 348 |
+
newsapi_query = company_name or ticker
|
| 349 |
+
newsapi_task = newsapi_search(
|
| 350 |
+
query=newsapi_query,
|
| 351 |
+
max_results=3,
|
| 352 |
+
sort_by="publishedAt",
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
tavily_result, nyt_result, newsapi_result = await asyncio.gather(
|
| 356 |
+
tavily_task, nyt_task, newsapi_task
|
| 357 |
+
)
|
| 358 |
|
| 359 |
# Combine results
|
| 360 |
all_results = []
|
|
|
|
| 370 |
all_results.extend(nyt_result["results"])
|
| 371 |
sources_used.append("NYT")
|
| 372 |
|
| 373 |
+
# Add NewsAPI results
|
| 374 |
+
if "results" in newsapi_result and newsapi_result["results"]:
|
| 375 |
+
all_results.extend(newsapi_result["results"])
|
| 376 |
+
sources_used.append("NewsAPI")
|
| 377 |
+
|
| 378 |
# Build combined result
|
| 379 |
result = {
|
| 380 |
"query": query,
|