vn6295337 Claude Opus 4.5 commited on
Commit
1707aaf
·
1 Parent(s): 4de32a0

Add NYT Article Search API as news source

Browse files

- Add nyt_search function for NYT Article Search API
- Update search_company_news to fetch from Tavily + NYT in parallel
- Add nyt_search as standalone MCP tool
- Rate limit: 5 req/min, 500 req/day

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. mcp-servers/news-basket/server.py +168 -8
mcp-servers/news-basket/server.py CHANGED
@@ -8,8 +8,9 @@ Use cases:
8
  - Competitor analysis
9
  - Going concern news coverage
10
 
11
- API Documentation: https://docs.tavily.com/
12
- Free tier: 1,000 API credits/month
 
13
  """
14
 
15
  import asyncio
@@ -50,6 +51,10 @@ server = Server("news-basket")
50
  TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
51
  TAVILY_BASE_URL = "https://api.tavily.com"
52
 
 
 
 
 
53
 
54
  # ============================================================
55
  # SEARCH FUNCTIONS
@@ -136,29 +141,153 @@ async def tavily_search(
136
  return {"error": str(e)}
137
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  async def search_company_news(ticker: str, company_name: str = None) -> dict:
140
  """
141
- Search for recent news about a company.
 
142
  """
143
  query = f"{ticker} stock news"
144
  if company_name:
145
  query = f"{company_name} ({ticker}) stock news"
146
 
147
- result = await tavily_search(
 
148
  query=query,
149
  search_depth="basic",
150
  max_results=5,
151
  exclude_domains=["reddit.com", "twitter.com", "x.com"],
152
  )
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  # Add SWOT categorization
155
- if "results" in result and result["results"]:
156
  swot_hints = {
157
  "opportunities": [],
158
  "threats": []
159
  }
160
 
161
- for r in result["results"]:
162
  content = (r.get("content") or "").lower()
163
  title = (r.get("title") or "").lower()
164
 
@@ -260,7 +389,7 @@ async def search_competitor_news(ticker: str, competitors: list) -> dict:
260
 
261
  @server.list_tools()
262
  async def list_tools():
263
- """List available Tavily tools."""
264
  return [
265
  Tool(
266
  name="tavily_search",
@@ -287,9 +416,34 @@ async def list_tools():
287
  "required": ["query"]
288
  }
289
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  Tool(
291
  name="search_company_news",
292
- description="Search for recent news about a company. Returns news with SWOT hints.",
293
  inputSchema={
294
  "type": "object",
295
  "properties": {
@@ -369,6 +523,12 @@ async def call_tool(name: str, arguments: dict):
369
  max_results = arguments.get("max_results", 5)
370
  result = await tavily_search(query, search_depth, max_results)
371
 
 
 
 
 
 
 
372
  elif name == "search_company_news":
373
  ticker = arguments.get("ticker", "").upper()
374
  company_name = arguments.get("company_name")
 
8
  - Competitor analysis
9
  - Going concern news coverage
10
 
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
 
51
  TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
52
  TAVILY_BASE_URL = "https://api.tavily.com"
53
 
54
+ # NYT Article Search API configuration
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
 
141
  return {"error": str(e)}
142
 
143
 
144
+ async def nyt_search(
145
+ query: str,
146
+ max_results: int = 5,
147
+ sort: str = "newest",
148
+ begin_date: str = None,
149
+ end_date: str = None,
150
+ ) -> dict:
151
+ """
152
+ Search NYT Article Search API.
153
+
154
+ Args:
155
+ query: Search query
156
+ max_results: Number of results (max 10 per page)
157
+ sort: "newest", "oldest", or "relevance"
158
+ begin_date: Filter start date (YYYYMMDD)
159
+ end_date: Filter end date (YYYYMMDD)
160
+
161
+ Returns:
162
+ Dict with articles from New York Times
163
+ """
164
+ if not NYT_API_KEY:
165
+ return {
166
+ "error": "NYT_API_KEY not configured",
167
+ "message": "Add NYT_API_KEY to ~/.env file. Get free key at https://developer.nytimes.com/"
168
+ }
169
+
170
+ try:
171
+ async with httpx.AsyncClient() as client:
172
+ params = {
173
+ "api-key": NYT_API_KEY,
174
+ "q": query,
175
+ "sort": sort,
176
+ "page": 0,
177
+ }
178
+
179
+ if begin_date:
180
+ params["begin_date"] = begin_date
181
+ if end_date:
182
+ params["end_date"] = end_date
183
+
184
+ response = await client.get(
185
+ NYT_BASE_URL,
186
+ params=params,
187
+ timeout=30
188
+ )
189
+
190
+ if response.status_code == 429:
191
+ return {
192
+ "error": "NYT rate limit exceeded",
193
+ "message": "Rate limit: 5 req/min, 500 req/day"
194
+ }
195
+
196
+ if response.status_code != 200:
197
+ return {
198
+ "error": f"NYT API error: {response.status_code}",
199
+ "message": response.text
200
+ }
201
+
202
+ data = response.json()
203
+ docs = data.get("response", {}).get("docs", [])
204
+
205
+ # Format results
206
+ results = []
207
+ for doc in docs[:max_results]:
208
+ headline = doc.get("headline", {})
209
+ results.append({
210
+ "title": headline.get("main", ""),
211
+ "url": doc.get("web_url", ""),
212
+ "content": doc.get("snippet", "") or doc.get("lead_paragraph", ""),
213
+ "published_date": doc.get("pub_date", ""),
214
+ "section": doc.get("section_name", ""),
215
+ "source": "New York Times",
216
+ })
217
+
218
+ return {
219
+ "query": query,
220
+ "results": results,
221
+ "result_count": len(results),
222
+ "total_hits": data.get("response", {}).get("meta", {}).get("hits", 0),
223
+ "source": "NYT Article Search API",
224
+ "as_of": datetime.now().isoformat()
225
+ }
226
+
227
+ except Exception as e:
228
+ logger.error(f"NYT search error: {e}")
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 and NYT APIs.
235
+ Combines results from both sources for comprehensive coverage.
236
  """
237
  query = f"{ticker} stock news"
238
  if company_name:
239
  query = f"{company_name} ({ticker}) stock news"
240
 
241
+ # Fetch from both sources in parallel
242
+ tavily_task = tavily_search(
243
  query=query,
244
  search_depth="basic",
245
  max_results=5,
246
  exclude_domains=["reddit.com", "twitter.com", "x.com"],
247
  )
248
 
249
+ nyt_query = company_name or ticker
250
+ nyt_task = nyt_search(
251
+ query=nyt_query,
252
+ max_results=3,
253
+ sort="newest",
254
+ )
255
+
256
+ tavily_result, nyt_result = await asyncio.gather(tavily_task, nyt_task)
257
+
258
+ # Combine results
259
+ all_results = []
260
+ sources_used = []
261
+
262
+ # Add Tavily results
263
+ if "results" in tavily_result and tavily_result["results"]:
264
+ all_results.extend(tavily_result["results"])
265
+ sources_used.append("Tavily")
266
+
267
+ # Add NYT results
268
+ if "results" in nyt_result and nyt_result["results"]:
269
+ all_results.extend(nyt_result["results"])
270
+ sources_used.append("NYT")
271
+
272
+ # Build combined result
273
+ result = {
274
+ "query": query,
275
+ "answer": tavily_result.get("answer"),
276
+ "results": all_results,
277
+ "result_count": len(all_results),
278
+ "sources": sources_used,
279
+ "source": " + ".join(sources_used) if sources_used else "None",
280
+ "as_of": datetime.now().isoformat()
281
+ }
282
+
283
  # Add SWOT categorization
284
+ if all_results:
285
  swot_hints = {
286
  "opportunities": [],
287
  "threats": []
288
  }
289
 
290
+ for r in all_results:
291
  content = (r.get("content") or "").lower()
292
  title = (r.get("title") or "").lower()
293
 
 
389
 
390
  @server.list_tools()
391
  async def list_tools():
392
+ """List available news search tools (Tavily + NYT)."""
393
  return [
394
  Tool(
395
  name="tavily_search",
 
416
  "required": ["query"]
417
  }
418
  ),
419
+ Tool(
420
+ name="nyt_search",
421
+ description="Search New York Times articles. High-quality financial journalism.",
422
+ inputSchema={
423
+ "type": "object",
424
+ "properties": {
425
+ "query": {
426
+ "type": "string",
427
+ "description": "Search query (company name, topic, etc.)"
428
+ },
429
+ "max_results": {
430
+ "type": "integer",
431
+ "description": "Number of results (1-10)",
432
+ "default": 5
433
+ },
434
+ "sort": {
435
+ "type": "string",
436
+ "enum": ["newest", "oldest", "relevance"],
437
+ "description": "Sort order",
438
+ "default": "newest"
439
+ }
440
+ },
441
+ "required": ["query"]
442
+ }
443
+ ),
444
  Tool(
445
  name="search_company_news",
446
+ description="Search for recent news about a company from Tavily + NYT. Returns news with SWOT hints.",
447
  inputSchema={
448
  "type": "object",
449
  "properties": {
 
523
  max_results = arguments.get("max_results", 5)
524
  result = await tavily_search(query, search_depth, max_results)
525
 
526
+ elif name == "nyt_search":
527
+ query = arguments.get("query", "")
528
+ max_results = arguments.get("max_results", 5)
529
+ sort = arguments.get("sort", "newest")
530
+ result = await nyt_search(query, max_results, sort)
531
+
532
  elif name == "search_company_news":
533
  ticker = arguments.get("ticker", "").upper()
534
  company_name = arguments.get("company_name")