google-news-trends-mcp 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,10 +17,11 @@ import cloudscraper
17
17
  from playwright.async_api import async_playwright, Browser, Playwright
18
18
  from trendspy import Trends, TrendKeyword
19
19
  import click
20
- from typing import Optional, cast
20
+ from typing import Optional, cast, overload, Literal, Awaitable
21
21
  import atexit
22
22
  from contextlib import asynccontextmanager
23
23
  import logging
24
+ from collections.abc import Callable
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
@@ -53,6 +54,8 @@ google_news = GNews(
53
54
  playwright: Optional[Playwright] = None
54
55
  browser: Optional[Browser] = None
55
56
 
57
+ ProgressCallback = Callable[[float, Optional[float]], Awaitable[None]]
58
+
56
59
 
57
60
  async def startup_browser():
58
61
  global playwright, browser
@@ -97,7 +100,9 @@ async def download_article_with_playwright(url) -> newspaper.Article | None:
97
100
  article = newspaper.article(url, input_html=content, language="en")
98
101
  return article
99
102
  except Exception as e:
100
- logging.warning(f"Error downloading article with Playwright from {url}\n {e.args}")
103
+ logging.warning(
104
+ f"Error downloading article with Playwright from {url}\n {e.args}"
105
+ )
101
106
  return None
102
107
 
103
108
 
@@ -130,7 +135,9 @@ async def download_article(url: str, nlp: bool = True) -> newspaper.Article | No
130
135
  f"Failed to download article with cloudscraper from {url}, status code: {response.status_code}"
131
136
  )
132
137
  except Exception as e:
133
- logging.debug(f"Error downloading article with cloudscraper from {url}\n {e.args}")
138
+ logging.debug(
139
+ f"Error downloading article with cloudscraper from {url}\n {e.args}"
140
+ )
134
141
 
135
142
  try:
136
143
  if article is None or not article.text:
@@ -148,23 +155,35 @@ async def download_article(url: str, nlp: bool = True) -> newspaper.Article | No
148
155
 
149
156
 
150
157
  async def process_gnews_articles(
151
- gnews_articles: list[dict], nlp: bool = True
152
- ) -> list["newspaper.Article"]:
158
+ gnews_articles: list[dict],
159
+ nlp: bool = True,
160
+ report_progress: Optional[ProgressCallback] = None,
161
+ ) -> list[newspaper.Article]:
153
162
  """
154
163
  Process a list of Google News articles and download them (async).
164
+ Optionally report progress via report_progress callback.
155
165
  """
156
166
  articles = []
157
- for gnews_article in gnews_articles:
167
+ total = len(gnews_articles)
168
+ for idx, gnews_article in enumerate(gnews_articles):
158
169
  article = await download_article(gnews_article["url"], nlp=nlp)
159
170
  if article is None or not article.text:
160
- logging.debug(f"Failed to download article from {gnews_article['url']}:\n{article}")
171
+ logging.debug(
172
+ f"Failed to download article from {gnews_article['url']}:\n{article}"
173
+ )
161
174
  continue
162
175
  articles.append(article)
176
+ if report_progress:
177
+ await report_progress(idx, total)
163
178
  return articles
164
179
 
165
180
 
166
181
  async def get_news_by_keyword(
167
- keyword: str, period=7, max_results: int = 10, nlp: bool = True
182
+ keyword: str,
183
+ period=7,
184
+ max_results: int = 10,
185
+ nlp: bool = True,
186
+ report_progress: Optional[ProgressCallback] = None,
168
187
  ) -> list[newspaper.Article]:
169
188
  """
170
189
  Find articles by keyword using Google News.
@@ -177,14 +196,21 @@ async def get_news_by_keyword(
177
196
  google_news.max_results = max_results
178
197
  gnews_articles = google_news.get_news(keyword)
179
198
  if not gnews_articles:
180
- logging.debug(f"No articles found for keyword '{keyword}' in the last {period} days.")
199
+ logging.debug(
200
+ f"No articles found for keyword '{keyword}' in the last {period} days."
201
+ )
181
202
  return []
182
- return await process_gnews_articles(gnews_articles, nlp=nlp)
203
+ return await process_gnews_articles(
204
+ gnews_articles, nlp=nlp, report_progress=report_progress
205
+ )
183
206
 
184
207
 
185
208
  async def get_top_news(
186
- period: int = 3, max_results: int = 10, nlp: bool = True
187
- ) -> list["newspaper.Article"]:
209
+ period: int = 3,
210
+ max_results: int = 10,
211
+ nlp: bool = True,
212
+ report_progress: Optional[ProgressCallback] = None,
213
+ ) -> list[newspaper.Article]:
188
214
  """
189
215
  Get top news stories from Google News.
190
216
  period: is the number of days to look back for top articles.
@@ -197,11 +223,17 @@ async def get_top_news(
197
223
  if not gnews_articles:
198
224
  logging.debug("No top news articles found.")
199
225
  return []
200
- return await process_gnews_articles(gnews_articles, nlp=nlp)
226
+ return await process_gnews_articles(
227
+ gnews_articles, nlp=nlp, report_progress=report_progress
228
+ )
201
229
 
202
230
 
203
231
  async def get_news_by_location(
204
- location: str, period=7, max_results: int = 10, nlp: bool = True
232
+ location: str,
233
+ period=7,
234
+ max_results: int = 10,
235
+ nlp: bool = True,
236
+ report_progress: Optional[ProgressCallback] = None,
205
237
  ) -> list[newspaper.Article]:
206
238
  """Find articles by location using Google News.
207
239
  location: is the name of city/state/country
@@ -213,13 +245,21 @@ async def get_news_by_location(
213
245
  google_news.max_results = max_results
214
246
  gnews_articles = google_news.get_news_by_location(location)
215
247
  if not gnews_articles:
216
- logging.debug(f"No articles found for location '{location}' in the last {period} days.")
248
+ logging.debug(
249
+ f"No articles found for location '{location}' in the last {period} days."
250
+ )
217
251
  return []
218
- return await process_gnews_articles(gnews_articles, nlp=nlp)
252
+ return await process_gnews_articles(
253
+ gnews_articles, nlp=nlp, report_progress=report_progress
254
+ )
219
255
 
220
256
 
221
257
  async def get_news_by_topic(
222
- topic: str, period=7, max_results: int = 10, nlp: bool = True
258
+ topic: str,
259
+ period=7,
260
+ max_results: int = 10,
261
+ nlp: bool = True,
262
+ report_progress: Optional[ProgressCallback] = None,
223
263
  ) -> list[newspaper.Article]:
224
264
  """Find articles by topic using Google News.
225
265
  topic is one of
@@ -239,9 +279,27 @@ async def get_news_by_topic(
239
279
  google_news.max_results = max_results
240
280
  gnews_articles = google_news.get_news_by_topic(topic)
241
281
  if not gnews_articles:
242
- logging.debug(f"No articles found for topic '{topic}' in the last {period} days.")
282
+ logging.debug(
283
+ f"No articles found for topic '{topic}' in the last {period} days."
284
+ )
243
285
  return []
244
- return await process_gnews_articles(gnews_articles, nlp=nlp)
286
+ return await process_gnews_articles(
287
+ gnews_articles, nlp=nlp, report_progress=report_progress
288
+ )
289
+
290
+
291
+ @overload
292
+ async def get_trending_terms(
293
+ geo: str = "US", full_data: Literal[False] = False, max_results: int = 100
294
+ ) -> list[dict[str, int]]:
295
+ pass
296
+
297
+
298
+ @overload
299
+ async def get_trending_terms(
300
+ geo: str = "US", full_data: Literal[True] = True, max_results: int = 100
301
+ ) -> list[TrendKeyword]:
302
+ pass
245
303
 
246
304
 
247
305
  async def get_trending_terms(
@@ -260,7 +318,9 @@ async def get_trending_terms(
260
318
  :max_results
261
319
  ]
262
320
  if not full_data:
263
- return [{"keyword": trend.keyword, "volume": trend.volume} for trend in trends]
321
+ return [
322
+ {"keyword": trend.keyword, "volume": trend.volume} for trend in trends
323
+ ]
264
324
  return trends
265
325
  except Exception as e:
266
326
  logging.warning(f"Error fetching trending terms: {e}")
@@ -1,10 +1,11 @@
1
- from fastmcp import FastMCP
1
+ from fastmcp import FastMCP, Context
2
2
  from fastmcp.exceptions import ToolError
3
3
  from fastmcp.server.dependencies import get_context
4
4
  from pydantic import BaseModel, Field
5
5
  from typing import Optional
6
6
  from google_news_trends_mcp import news
7
7
  from typing import Annotated
8
+ from newspaper import settings as newspaper_settings
8
9
  from fastmcp.server.middleware.timing import TimingMiddleware
9
10
  from fastmcp.server.middleware.logging import LoggingMiddleware
10
11
  from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
@@ -132,11 +133,49 @@ mcp.add_middleware(TimingMiddleware()) # Time actual execution
132
133
  mcp.add_middleware(LoggingMiddleware()) # Log everything
133
134
 
134
135
 
136
+ # Configure newspaper settings for article extraction
137
+ def set_newspaper_article_fields(full_data: bool = False):
138
+ if full_data:
139
+ newspaper_settings.article_json_fields = [
140
+ "url",
141
+ "read_more_link",
142
+ "language",
143
+ "title",
144
+ "top_image",
145
+ "meta_img",
146
+ "images",
147
+ "movies",
148
+ "keywords",
149
+ "keyword_scores",
150
+ "meta_keywords",
151
+ "tags",
152
+ "authors",
153
+ "publish_date",
154
+ "summary",
155
+ "meta_description",
156
+ "meta_lang",
157
+ "meta_favicon",
158
+ "meta_site_name",
159
+ "canonical_link",
160
+ "text",
161
+ ]
162
+ else:
163
+ newspaper_settings.article_json_fields = [
164
+ "url",
165
+ "title",
166
+ "text",
167
+ "publish_date",
168
+ "summary",
169
+ "keywords",
170
+ ]
171
+
172
+
135
173
  @mcp.tool(
136
174
  description=news.get_news_by_keyword.__doc__,
137
175
  tags={"news", "articles", "keyword"},
138
176
  )
139
177
  async def get_news_by_keyword(
178
+ ctx: Context,
140
179
  keyword: Annotated[str, Field(description="Search term to find articles.")],
141
180
  period: Annotated[
142
181
  int, Field(description="Number of days to look back for articles.", ge=1)
@@ -146,14 +185,20 @@ async def get_news_by_keyword(
146
185
  ] = 10,
147
186
  nlp: Annotated[
148
187
  bool, Field(description="Whether to perform NLP on the articles.")
149
- ] = True,
188
+ ] = False,
189
+ full_data: Annotated[
190
+ bool, Field(description="Return full data for each article.")
191
+ ] = False,
150
192
  ) -> list[ArticleOut]:
193
+ set_newspaper_article_fields(full_data)
151
194
  articles = await news.get_news_by_keyword(
152
195
  keyword=keyword,
153
196
  period=period,
154
197
  max_results=max_results,
155
198
  nlp=nlp,
199
+ report_progress=ctx.report_progress,
156
200
  )
201
+ await ctx.report_progress(progress=len(articles), total=len(articles))
157
202
  return [ArticleOut(**a.to_json(False)) for a in articles]
158
203
 
159
204
 
@@ -162,6 +207,7 @@ async def get_news_by_keyword(
162
207
  tags={"news", "articles", "location"},
163
208
  )
164
209
  async def get_news_by_location(
210
+ ctx: Context,
165
211
  location: Annotated[str, Field(description="Name of city/state/country.")],
166
212
  period: Annotated[
167
213
  int, Field(description="Number of days to look back for articles.", ge=1)
@@ -171,14 +217,20 @@ async def get_news_by_location(
171
217
  ] = 10,
172
218
  nlp: Annotated[
173
219
  bool, Field(description="Whether to perform NLP on the articles.")
174
- ] = True,
220
+ ] = False,
221
+ full_data: Annotated[
222
+ bool, Field(description="Return full data for each article.")
223
+ ] = False,
175
224
  ) -> list[ArticleOut]:
225
+ set_newspaper_article_fields(full_data)
176
226
  articles = await news.get_news_by_location(
177
227
  location=location,
178
228
  period=period,
179
229
  max_results=max_results,
180
230
  nlp=nlp,
231
+ report_progress=ctx.report_progress,
181
232
  )
233
+ await ctx.report_progress(progress=len(articles), total=len(articles))
182
234
  return [ArticleOut(**a.to_json(False)) for a in articles]
183
235
 
184
236
 
@@ -186,6 +238,7 @@ async def get_news_by_location(
186
238
  description=news.get_news_by_topic.__doc__, tags={"news", "articles", "topic"}
187
239
  )
188
240
  async def get_news_by_topic(
241
+ ctx: Context,
189
242
  topic: Annotated[str, Field(description="Topic to search for articles.")],
190
243
  period: Annotated[
191
244
  int, Field(description="Number of days to look back for articles.", ge=1)
@@ -195,19 +248,26 @@ async def get_news_by_topic(
195
248
  ] = 10,
196
249
  nlp: Annotated[
197
250
  bool, Field(description="Whether to perform NLP on the articles.")
198
- ] = True,
251
+ ] = False,
252
+ full_data: Annotated[
253
+ bool, Field(description="Return full data for each article.")
254
+ ] = False,
199
255
  ) -> list[ArticleOut]:
256
+ set_newspaper_article_fields(full_data)
200
257
  articles = await news.get_news_by_topic(
201
258
  topic=topic,
202
259
  period=period,
203
260
  max_results=max_results,
204
261
  nlp=nlp,
262
+ report_progress=ctx.report_progress,
205
263
  )
264
+ await ctx.report_progress(progress=len(articles), total=len(articles))
206
265
  return [ArticleOut(**a.to_json(False)) for a in articles]
207
266
 
208
267
 
209
268
  @mcp.tool(description=news.get_top_news.__doc__, tags={"news", "articles", "top"})
210
269
  async def get_top_news(
270
+ ctx: Context,
211
271
  period: Annotated[
212
272
  int, Field(description="Number of days to look back for top articles.", ge=1)
213
273
  ] = 3,
@@ -216,13 +276,19 @@ async def get_top_news(
216
276
  ] = 10,
217
277
  nlp: Annotated[
218
278
  bool, Field(description="Whether to perform NLP on the articles.")
219
- ] = True,
279
+ ] = False,
280
+ full_data: Annotated[
281
+ bool, Field(description="Return full data for each article.")
282
+ ] = False,
220
283
  ) -> list[ArticleOut]:
284
+ set_newspaper_article_fields(full_data)
221
285
  articles = await news.get_top_news(
222
286
  period=period,
223
287
  max_results=max_results,
224
288
  nlp=nlp,
289
+ report_progress=ctx.report_progress,
225
290
  )
291
+ await ctx.report_progress(progress=len(articles), total=len(articles))
226
292
  return [ArticleOut(**a.to_json(False)) for a in articles]
227
293
 
228
294
 
@@ -243,17 +309,21 @@ async def get_trending_terms(
243
309
  int, Field(description="Maximum number of results to return.", ge=1)
244
310
  ] = 100,
245
311
  ) -> list[TrendingTermOut]:
246
- trends = await news.get_trending_terms(
247
- geo=geo, full_data=full_data, max_results=max_results
248
- )
312
+
249
313
  if not full_data:
250
- # Only return keyword and volume fields
314
+ trends = await news.get_trending_terms(
315
+ geo=geo, full_data=False, max_results=max_results
316
+ )
251
317
  return [
252
- TrendingTermOut(keyword=tt["keyword"], volume=tt["volume"]) for tt in trends
318
+ TrendingTermOut(keyword=str(tt["keyword"]), volume=tt["volume"])
319
+ for tt in trends
253
320
  ]
254
- else:
255
- # Assume each tt is a TrendingTerm object
256
- return [TrendingTermOut(**tt.__dict__) for tt in trends]
321
+
322
+ trends = await news.get_trending_terms(
323
+ geo=geo, full_data=True, max_results=max_results
324
+ )
325
+ return [TrendingTermOut(**tt.__dict__) for tt in trends]
326
+
257
327
 
258
328
  def main():
259
329
  mcp.run()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-news-trends-mcp
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: An MCP server to access Google News and Google Trends.
5
5
  Author-email: Jesse Manek <jesse.manek@gmail.com>
6
6
  License-Expression: MIT
@@ -30,7 +30,6 @@ Dynamic: license-file
30
30
  # Google News Trends MCP
31
31
 
32
32
  An MCP server to access Google News and Google Trends. Does not rely on any paid APIs.
33
- The returned data currently uses a lot of tokens, so it is recommended to always use limits when making requests.
34
33
 
35
34
  ## Features
36
35
 
@@ -0,0 +1,11 @@
1
+ google_news_trends_mcp/__init__.py,sha256=J9O5WNvC9cNDaxecveSUvzLGOXOYO-pCHbiGopfYoIc,76
2
+ google_news_trends_mcp/__main__.py,sha256=ysiAk_xpnnW3lrLlzdIQQa71tuGBRT8WocbecBsY2Fs,87
3
+ google_news_trends_mcp/cli.py,sha256=XJNnRVpDXX2MCb8dPfDcQJWYYA4CxTuxbhvpJGeVQgs,4133
4
+ google_news_trends_mcp/news.py,sha256=2xmlwe4txaqiB8MljbhbBLmpb6tM35autGJVQ144k0s,13107
5
+ google_news_trends_mcp/server.py,sha256=7hau48vQr_a2YbLgz4MqkwsTHMuSIU8jYEkjInID4gY,11553
6
+ google_news_trends_mcp-0.1.7.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
7
+ google_news_trends_mcp-0.1.7.dist-info/METADATA,sha256=qkJh_vuB7gIH2Pp0TorRH_9J6ZvkKmU4JOvjsZqwtoY,4464
8
+ google_news_trends_mcp-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ google_news_trends_mcp-0.1.7.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
10
+ google_news_trends_mcp-0.1.7.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
11
+ google_news_trends_mcp-0.1.7.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- google_news_trends_mcp/__init__.py,sha256=J9O5WNvC9cNDaxecveSUvzLGOXOYO-pCHbiGopfYoIc,76
2
- google_news_trends_mcp/__main__.py,sha256=ysiAk_xpnnW3lrLlzdIQQa71tuGBRT8WocbecBsY2Fs,87
3
- google_news_trends_mcp/cli.py,sha256=XJNnRVpDXX2MCb8dPfDcQJWYYA4CxTuxbhvpJGeVQgs,4133
4
- google_news_trends_mcp/news.py,sha256=FYz1guxLZThMmh_9uN3VcdHBjLHZF5brhk7Bw7QxeDo,11780
5
- google_news_trends_mcp/server.py,sha256=MdEWk9QVark4z00UlTIckdAM3hPW7eRQgZRZ2h8WUPk,9363
6
- google_news_trends_mcp-0.1.6.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
7
- google_news_trends_mcp-0.1.6.dist-info/METADATA,sha256=HewouWHDlGkCPzEM_Nq7_s2KE66FVvtLLdHYToz9WgE,4580
8
- google_news_trends_mcp-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- google_news_trends_mcp-0.1.6.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
10
- google_news_trends_mcp-0.1.6.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
11
- google_news_trends_mcp-0.1.6.dist-info/RECORD,,