google-news-trends-mcp 0.1.6__py3-none-any.whl → 0.1.8__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,9 +17,7 @@ def cli():
17
17
 
18
18
  @cli.command(help=get_news_by_keyword.__doc__)
19
19
  @click.argument("keyword")
20
- @click.option(
21
- "--period", type=int, default=7, help="Period in days to search for articles."
22
- )
20
+ @click.option("--period", type=int, default=7, help="Period in days to search for articles.")
23
21
  @click.option(
24
22
  "--max-results",
25
23
  "max_results",
@@ -27,24 +25,16 @@ def cli():
27
25
  default=10,
28
26
  help="Maximum number of results to return.",
29
27
  )
30
- @click.option(
31
- "--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles."
32
- )
28
+ @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
33
29
  def keyword(keyword, period, max_results, no_nlp):
34
- articles = asyncio.run(
35
- get_news_by_keyword(
36
- keyword, period=period, max_results=max_results, nlp=not no_nlp
37
- )
38
- )
30
+ articles = asyncio.run(get_news_by_keyword(keyword, period=period, max_results=max_results, nlp=not no_nlp))
39
31
  # asyncio.run(articles) # Ensure the articles are fetched asynchronously
40
32
  print_articles(articles)
41
33
 
42
34
 
43
35
  @cli.command(help=get_news_by_location.__doc__)
44
36
  @click.argument("location")
45
- @click.option(
46
- "--period", type=int, default=7, help="Period in days to search for articles."
47
- )
37
+ @click.option("--period", type=int, default=7, help="Period in days to search for articles.")
48
38
  @click.option(
49
39
  "--max-results",
50
40
  "max_results",
@@ -52,23 +42,15 @@ def keyword(keyword, period, max_results, no_nlp):
52
42
  default=10,
53
43
  help="Maximum number of results to return.",
54
44
  )
55
- @click.option(
56
- "--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles."
57
- )
45
+ @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
58
46
  def location(location, period, max_results, no_nlp):
59
- articles = asyncio.run(
60
- get_news_by_location(
61
- location, period=period, max_results=max_results, nlp=not no_nlp
62
- )
63
- )
47
+ articles = asyncio.run(get_news_by_location(location, period=period, max_results=max_results, nlp=not no_nlp))
64
48
  print_articles(articles)
65
49
 
66
50
 
67
51
  @cli.command(help=get_news_by_topic.__doc__)
68
52
  @click.argument("topic")
69
- @click.option(
70
- "--period", type=int, default=7, help="Period in days to search for articles."
71
- )
53
+ @click.option("--period", type=int, default=7, help="Period in days to search for articles.")
72
54
  @click.option(
73
55
  "--max-results",
74
56
  "max_results",
@@ -76,23 +58,15 @@ def location(location, period, max_results, no_nlp):
76
58
  default=10,
77
59
  help="Maximum number of results to return.",
78
60
  )
79
- @click.option(
80
- "--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles."
81
- )
61
+ @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
82
62
  def topic(topic, period, max_results, no_nlp):
83
- articles = asyncio.run(
84
- get_news_by_topic(topic, period=period, max_results=max_results, nlp=not no_nlp)
85
- )
63
+ articles = asyncio.run(get_news_by_topic(topic, period=period, max_results=max_results, nlp=not no_nlp))
86
64
  print_articles(articles)
87
65
 
88
66
 
89
67
  @cli.command(help=get_trending_terms.__doc__)
90
- @click.option(
91
- "--geo", type=str, default="US", help="Country code, e.g. 'US', 'GB', 'IN', etc."
92
- )
93
- @click.option(
94
- "--full-data", is_flag=True, default=False, help="Return full data for each trend."
95
- )
68
+ @click.option("--geo", type=str, default="US", help="Country code, e.g. 'US', 'GB', 'IN', etc.")
69
+ @click.option("--full-data", is_flag=True, default=False, help="Return full data for each trend.")
96
70
  @click.option(
97
71
  "--max-results",
98
72
  "max_results",
@@ -101,9 +75,7 @@ def topic(topic, period, max_results, no_nlp):
101
75
  help="Maximum number of results to return.",
102
76
  )
103
77
  def trending(geo, full_data, max_results):
104
- trending_terms = asyncio.run(
105
- get_trending_terms(geo=geo, full_data=full_data, max_results=max_results)
106
- )
78
+ trending_terms = asyncio.run(get_trending_terms(geo=geo, full_data=full_data, max_results=max_results))
107
79
  if trending_terms:
108
80
  print("Trending terms:")
109
81
  for term in trending_terms:
@@ -116,9 +88,7 @@ def trending(geo, full_data, max_results):
116
88
 
117
89
 
118
90
  @cli.command(help=get_top_news.__doc__)
119
- @click.option(
120
- "--period", type=int, default=3, help="Period in days to search for top articles."
121
- )
91
+ @click.option("--period", type=int, default=3, help="Period in days to search for top articles.")
122
92
  @click.option(
123
93
  "--max-results",
124
94
  "max_results",
@@ -126,13 +96,9 @@ def trending(geo, full_data, max_results):
126
96
  default=10,
127
97
  help="Maximum number of results to return.",
128
98
  )
129
- @click.option(
130
- "--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles."
131
- )
99
+ @click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
132
100
  def top(period, max_results, no_nlp):
133
- articles = asyncio.run(
134
- get_top_news(max_results=max_results, period=period, nlp=not no_nlp)
135
- )
101
+ articles = asyncio.run(get_top_news(max_results=max_results, period=period, nlp=not no_nlp))
136
102
  print_articles(articles)
137
103
  print(f"Found {len(articles)} top articles.")
138
104
 
@@ -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
@@ -148,23 +151,33 @@ async def download_article(url: str, nlp: bool = True) -> newspaper.Article | No
148
151
 
149
152
 
150
153
  async def process_gnews_articles(
151
- gnews_articles: list[dict], nlp: bool = True
152
- ) -> list["newspaper.Article"]:
154
+ gnews_articles: list[dict],
155
+ nlp: bool = True,
156
+ report_progress: Optional[ProgressCallback] = None,
157
+ ) -> list[newspaper.Article]:
153
158
  """
154
159
  Process a list of Google News articles and download them (async).
160
+ Optionally report progress via report_progress callback.
155
161
  """
156
162
  articles = []
157
- for gnews_article in gnews_articles:
163
+ total = len(gnews_articles)
164
+ for idx, gnews_article in enumerate(gnews_articles):
158
165
  article = await download_article(gnews_article["url"], nlp=nlp)
159
166
  if article is None or not article.text:
160
167
  logging.debug(f"Failed to download article from {gnews_article['url']}:\n{article}")
161
168
  continue
162
169
  articles.append(article)
170
+ if report_progress:
171
+ await report_progress(idx, total)
163
172
  return articles
164
173
 
165
174
 
166
175
  async def get_news_by_keyword(
167
- keyword: str, period=7, max_results: int = 10, nlp: bool = True
176
+ keyword: str,
177
+ period=7,
178
+ max_results: int = 10,
179
+ nlp: bool = True,
180
+ report_progress: Optional[ProgressCallback] = None,
168
181
  ) -> list[newspaper.Article]:
169
182
  """
170
183
  Find articles by keyword using Google News.
@@ -179,12 +192,15 @@ async def get_news_by_keyword(
179
192
  if not gnews_articles:
180
193
  logging.debug(f"No articles found for keyword '{keyword}' in the last {period} days.")
181
194
  return []
182
- return await process_gnews_articles(gnews_articles, nlp=nlp)
195
+ return await process_gnews_articles(gnews_articles, nlp=nlp, report_progress=report_progress)
183
196
 
184
197
 
185
198
  async def get_top_news(
186
- period: int = 3, max_results: int = 10, nlp: bool = True
187
- ) -> list["newspaper.Article"]:
199
+ period: int = 3,
200
+ max_results: int = 10,
201
+ nlp: bool = True,
202
+ report_progress: Optional[ProgressCallback] = None,
203
+ ) -> list[newspaper.Article]:
188
204
  """
189
205
  Get top news stories from Google News.
190
206
  period: is the number of days to look back for top articles.
@@ -197,11 +213,15 @@ async def get_top_news(
197
213
  if not gnews_articles:
198
214
  logging.debug("No top news articles found.")
199
215
  return []
200
- return await process_gnews_articles(gnews_articles, nlp=nlp)
216
+ return await process_gnews_articles(gnews_articles, nlp=nlp, report_progress=report_progress)
201
217
 
202
218
 
203
219
  async def get_news_by_location(
204
- location: str, period=7, max_results: int = 10, nlp: bool = True
220
+ location: str,
221
+ period=7,
222
+ max_results: int = 10,
223
+ nlp: bool = True,
224
+ report_progress: Optional[ProgressCallback] = None,
205
225
  ) -> list[newspaper.Article]:
206
226
  """Find articles by location using Google News.
207
227
  location: is the name of city/state/country
@@ -215,11 +235,15 @@ async def get_news_by_location(
215
235
  if not gnews_articles:
216
236
  logging.debug(f"No articles found for location '{location}' in the last {period} days.")
217
237
  return []
218
- return await process_gnews_articles(gnews_articles, nlp=nlp)
238
+ return await process_gnews_articles(gnews_articles, nlp=nlp, report_progress=report_progress)
219
239
 
220
240
 
221
241
  async def get_news_by_topic(
222
- topic: str, period=7, max_results: int = 10, nlp: bool = True
242
+ topic: str,
243
+ period=7,
244
+ max_results: int = 10,
245
+ nlp: bool = True,
246
+ report_progress: Optional[ProgressCallback] = None,
223
247
  ) -> list[newspaper.Article]:
224
248
  """Find articles by topic using Google News.
225
249
  topic is one of
@@ -241,7 +265,21 @@ async def get_news_by_topic(
241
265
  if not gnews_articles:
242
266
  logging.debug(f"No articles found for topic '{topic}' in the last {period} days.")
243
267
  return []
244
- return await process_gnews_articles(gnews_articles, nlp=nlp)
268
+ return await process_gnews_articles(gnews_articles, nlp=nlp, report_progress=report_progress)
269
+
270
+
271
+ @overload
272
+ async def get_trending_terms(
273
+ geo: str = "US", full_data: Literal[False] = False, max_results: int = 100
274
+ ) -> list[dict[str, int]]:
275
+ pass
276
+
277
+
278
+ @overload
279
+ async def get_trending_terms(
280
+ geo: str = "US", full_data: Literal[True] = True, max_results: int = 100
281
+ ) -> list[TrendKeyword]:
282
+ pass
245
283
 
246
284
 
247
285
  async def get_trending_terms(
@@ -256,9 +294,7 @@ async def get_trending_terms(
256
294
  """
257
295
  try:
258
296
  trends = list(tr.trending_now(geo=geo))
259
- trends = list(sorted(trends, key=lambda tt: tt.volume, reverse=True))[
260
- :max_results
261
- ]
297
+ trends = list(sorted(trends, key=lambda tt: tt.volume, reverse=True))[:max_results]
262
298
  if not full_data:
263
299
  return [{"keyword": trend.keyword, "volume": trend.volume} for trend in trends]
264
300
  return trends
@@ -1,123 +1,87 @@
1
- from fastmcp import FastMCP
1
+ from typing import Annotated, cast, Optional, Any, Literal, TYPE_CHECKING
2
+ from fastmcp import FastMCP, Context
2
3
  from fastmcp.exceptions import ToolError
3
4
  from fastmcp.server.dependencies import get_context
4
- from pydantic import BaseModel, Field
5
- from typing import Optional
6
- from google_news_trends_mcp import news
7
- from typing import Annotated
8
5
  from fastmcp.server.middleware.timing import TimingMiddleware
9
6
  from fastmcp.server.middleware.logging import LoggingMiddleware
10
7
  from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
11
8
  from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
9
+ from mcp.types import TextContent
10
+ from pydantic import BaseModel, Field, model_serializer
11
+ from google_news_trends_mcp import news
12
+ from newspaper import settings as newspaper_settings
13
+ from newspaper.article import Article
12
14
 
13
15
 
14
- class ArticleOut(BaseModel):
15
- read_more_link: Annotated[
16
- Optional[str], Field(description="Link to read more about the article.")
17
- ] = None
18
- language: Annotated[
19
- Optional[str], Field(description="Language code of the article.")
20
- ] = None
21
- meta_img: Annotated[Optional[str], Field(description="Meta image URL.")] = None
22
- movies: Annotated[
23
- Optional[list[str]], Field(description="List of movie URLs or IDs.")
24
- ] = None
25
- meta_favicon: Annotated[
26
- Optional[str], Field(description="Favicon URL from meta data.")
27
- ] = None
28
- meta_site_name: Annotated[
29
- Optional[str], Field(description="Site name from meta data.")
30
- ] = None
16
+ class BaseModelClean(BaseModel):
17
+ @model_serializer
18
+ def serializer(self, **kwargs) -> dict[str, Any]:
19
+ return {
20
+ field: self.__getattribute__(field)
21
+ for field in self.model_fields_set
22
+ if self.__getattribute__(field) is not None
23
+ }
24
+
25
+ if TYPE_CHECKING:
26
+
27
+ def model_dump(self, **kwargs) -> dict[str, Any]: ...
28
+
29
+
30
+ class ArticleOut(BaseModelClean):
31
31
  title: Annotated[str, Field(description="Title of the article.")]
32
- authors: Annotated[Optional[list[str]], Field(description="list of authors.")] = (
33
- None
34
- )
35
- publish_date: Annotated[
36
- Optional[str], Field(description="Publish date in ISO format.")
37
- ] = None
38
- top_image: Annotated[Optional[str], Field(description="URL of the top image.")] = (
39
- None
40
- )
41
- images: Annotated[Optional[list[str]], Field(description="list of image URLs.")] = (
42
- None
43
- )
44
- text: Annotated[str, Field(description="Full text of the article.")]
45
32
  url: Annotated[str, Field(description="Original article URL.")]
46
- summary: Annotated[Optional[str], Field(description="Summary of the article.")] = (
47
- None
48
- )
49
- keywords: Annotated[
50
- Optional[list[str]], Field(description="Extracted keywords.")
51
- ] = None
52
- tags: Annotated[Optional[list[str]], Field(description="Tags for the article.")] = (
53
- None
54
- )
55
- meta_keywords: Annotated[
56
- Optional[list[str]], Field(description="Meta keywords from the article.")
57
- ] = None
58
- meta_description: Annotated[
59
- Optional[str], Field(description="Meta description from the article.")
60
- ] = None
61
- canonical_link: Annotated[
62
- Optional[str], Field(description="Canonical link for the article.")
63
- ] = None
64
- meta_data: Annotated[
65
- Optional[dict[str, str | int]], Field(description="Meta data dictionary.")
66
- ] = None
67
- meta_lang: Annotated[
68
- Optional[str], Field(description="Language of the article.")
69
- ] = None
70
- source_url: Annotated[
71
- Optional[str], Field(description="Source URL if different from original.")
72
- ] = None
33
+ read_more_link: Annotated[Optional[str], Field(description="Link to read more about the article.")] = None
34
+ language: Annotated[Optional[str], Field(description="Language code of the article.")] = None
35
+ meta_img: Annotated[Optional[str], Field(description="Meta image URL.")] = None
36
+ movies: Annotated[Optional[list[str]], Field(description="List of movie URLs or IDs.")] = None
37
+ meta_favicon: Annotated[Optional[str], Field(description="Favicon URL from meta data.")] = None
38
+ meta_site_name: Annotated[Optional[str], Field(description="Site name from meta data.")] = None
39
+ authors: Annotated[Optional[list[str]], Field(description="list of authors.")] = None
40
+ publish_date: Annotated[Optional[str], Field(description="Publish date in ISO format.")] = None
41
+ top_image: Annotated[Optional[str], Field(description="URL of the top image.")] = None
42
+ images: Annotated[Optional[list[str]], Field(description="list of image URLs.")] = None
43
+ text: Annotated[Optional[str], Field(description="Full text of the article.")] = None
44
+ summary: Annotated[Optional[str], Field(description="Summary of the article.")] = None
45
+ keywords: Annotated[Optional[list[str]], Field(description="Extracted keywords.")] = None
46
+ tags: Annotated[Optional[list[str]], Field(description="Tags for the article.")] = None
47
+ meta_keywords: Annotated[Optional[list[str]], Field(description="Meta keywords from the article.")] = None
48
+ meta_description: Annotated[Optional[str], Field(description="Meta description from the article.")] = None
49
+ canonical_link: Annotated[Optional[str], Field(description="Canonical link for the article.")] = None
50
+ meta_data: Annotated[Optional[dict[str, str | int]], Field(description="Meta data dictionary.")] = None
51
+ meta_lang: Annotated[Optional[str], Field(description="Language of the article.")] = None
52
+ source_url: Annotated[Optional[str], Field(description="Source URL if different from original.")] = None
73
53
 
74
54
 
75
- class TrendingTermArticleOut(BaseModel):
55
+ class TrendingTermArticleOut(BaseModelClean):
76
56
  title: Annotated[str, Field(description="Article title.")] = ""
77
57
  url: Annotated[str, Field(description="Article URL.")] = ""
78
58
  source: Annotated[Optional[str], Field(description="News source name.")] = None
79
59
  picture: Annotated[Optional[str], Field(description="URL to article image.")] = None
80
- time: Annotated[
81
- Optional[str | int], Field(description="Publication time or timestamp.")
82
- ] = None
60
+ time: Annotated[Optional[str | int], Field(description="Publication time or timestamp.")] = None
83
61
  snippet: Annotated[Optional[str], Field(description="Article preview text.")] = None
84
62
 
85
63
 
86
- class TrendingTermOut(BaseModel):
64
+ class TrendingTermOut(BaseModelClean):
87
65
  keyword: Annotated[str, Field(description="Trending keyword.")]
88
66
  volume: Annotated[Optional[int], Field(description="Search volume.")] = None
89
67
  geo: Annotated[Optional[str], Field(description="Geographic location code.")] = None
90
68
  started_timestamp: Annotated[
91
69
  Optional[list],
92
- Field(
93
- description="When the trend started (year, month, day, hour, minute, second)."
94
- ),
70
+ Field(description="When the trend started (year, month, day, hour, minute, second)."),
95
71
  ] = None
96
72
  ended_timestamp: Annotated[
97
- Optional[tuple[int, int]],
98
- Field(
99
- description="When the trend ended (year, month, day, hour, minute, second)."
100
- ),
101
- ] = None
102
- volume_growth_pct: Annotated[
103
- Optional[float], Field(description="Percentage growth in search volume.")
104
- ] = None
105
- trend_keywords: Annotated[
106
- Optional[list[str]], Field(description="Related keywords.")
107
- ] = None
108
- topics: Annotated[
109
- Optional[list[str | int]], Field(description="Related topics.")
73
+ Optional[list],
74
+ Field(description="When the trend ended (year, month, day, hour, minute, second)."),
110
75
  ] = None
76
+ volume_growth_pct: Annotated[Optional[float], Field(description="Percentage growth in search volume.")] = None
77
+ trend_keywords: Annotated[Optional[list[str]], Field(description="Related keywords.")] = None
78
+ topics: Annotated[Optional[list[str | int]], Field(description="Related topics.")] = None
111
79
  news: Annotated[
112
80
  Optional[list[TrendingTermArticleOut]],
113
81
  Field(description="Related news articles."),
114
82
  ] = None
115
- news_tokens: Annotated[
116
- Optional[list], Field(description="Associated news tokens.")
117
- ] = None
118
- normalized_keyword: Annotated[
119
- Optional[str], Field(description="Normalized form of the keyword.")
120
- ] = None
83
+ news_tokens: Annotated[Optional[list], Field(description="Associated news tokens.")] = None
84
+ normalized_keyword: Annotated[Optional[str], Field(description="Normalized form of the keyword.")] = None
121
85
 
122
86
 
123
87
  mcp = FastMCP(
@@ -132,28 +96,100 @@ mcp.add_middleware(TimingMiddleware()) # Time actual execution
132
96
  mcp.add_middleware(LoggingMiddleware()) # Log everything
133
97
 
134
98
 
99
+ def set_newspaper_article_fields(full_data: bool = False):
100
+ if full_data:
101
+ newspaper_settings.article_json_fields = [
102
+ "url",
103
+ "read_more_link",
104
+ "language",
105
+ "title",
106
+ "top_image",
107
+ "meta_img",
108
+ "images",
109
+ "movies",
110
+ "keywords",
111
+ "keyword_scores",
112
+ "meta_keywords",
113
+ "tags",
114
+ "authors",
115
+ "publish_date",
116
+ "summary",
117
+ "meta_description",
118
+ "meta_lang",
119
+ "meta_favicon",
120
+ "meta_site_name",
121
+ "canonical_link",
122
+ "text",
123
+ ]
124
+ else:
125
+ newspaper_settings.article_json_fields = [
126
+ "url",
127
+ "title",
128
+ "publish_date",
129
+ "summary",
130
+ ]
131
+
132
+
133
+ async def summarize_article(article: Article, ctx: Context) -> None:
134
+ if article.text:
135
+ prompt = f"Please provide a concise summary of the following news article:\n\n{article.text}"
136
+ response = await ctx.sample(prompt)
137
+ # response = cast(TextContent, response)
138
+ if isinstance(response, TextContent):
139
+ if not response.text:
140
+ await ctx.warning("NLP response is empty. Unable to summarize article.")
141
+ article.summary = "No summary available."
142
+ else:
143
+ article.summary = response.text
144
+ else:
145
+ await ctx.warning("NLP response is not a TextContent object. Unable to summarize article.")
146
+ article.summary = "No summary available."
147
+ else:
148
+ article.summary = "No summary available."
149
+
150
+
135
151
  @mcp.tool(
136
152
  description=news.get_news_by_keyword.__doc__,
137
153
  tags={"news", "articles", "keyword"},
138
154
  )
139
155
  async def get_news_by_keyword(
156
+ ctx: Context,
140
157
  keyword: Annotated[str, Field(description="Search term to find articles.")],
141
- period: Annotated[
142
- int, Field(description="Number of days to look back for articles.", ge=1)
143
- ] = 7,
144
- max_results: Annotated[
145
- int, Field(description="Maximum number of results to return.", ge=1)
146
- ] = 10,
147
- nlp: Annotated[
148
- bool, Field(description="Whether to perform NLP on the articles.")
158
+ period: Annotated[int, Field(description="Number of days to look back for articles.", ge=1)] = 7,
159
+ max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10,
160
+ full_data: Annotated[
161
+ bool,
162
+ Field(
163
+ description="Return full data for each article. If False a summary should be created by setting the summarize flag"
164
+ ),
165
+ ] = False,
166
+ summarize: Annotated[
167
+ bool,
168
+ Field(
169
+ description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp"
170
+ ),
149
171
  ] = True,
150
172
  ) -> list[ArticleOut]:
173
+ set_newspaper_article_fields(full_data)
151
174
  articles = await news.get_news_by_keyword(
152
175
  keyword=keyword,
153
176
  period=period,
154
177
  max_results=max_results,
155
- nlp=nlp,
178
+ nlp=False,
179
+ report_progress=ctx.report_progress,
156
180
  )
181
+ if summarize:
182
+ total_articles = len(articles)
183
+ try:
184
+ for idx, article in enumerate(articles):
185
+ await summarize_article(article, ctx)
186
+ await ctx.report_progress(idx, total_articles)
187
+ except Exception as err:
188
+ await ctx.debug(f"Failed to use LLM sampling for article summary:\n{err.args}")
189
+ for idx, article in enumerate(articles):
190
+ article.nlp()
191
+ await ctx.report_progress(idx, total_articles)
192
+ await ctx.report_progress(progress=len(articles), total=len(articles))
157
193
  return [ArticleOut(**a.to_json(False)) for a in articles]
158
194
 
159
195
 
@@ -162,98 +198,147 @@ async def get_news_by_keyword(
162
198
  tags={"news", "articles", "location"},
163
199
  )
164
200
  async def get_news_by_location(
201
+ ctx: Context,
165
202
  location: Annotated[str, Field(description="Name of city/state/country.")],
166
- period: Annotated[
167
- int, Field(description="Number of days to look back for articles.", ge=1)
168
- ] = 7,
169
- max_results: Annotated[
170
- int, Field(description="Maximum number of results to return.", ge=1)
171
- ] = 10,
172
- nlp: Annotated[
173
- bool, Field(description="Whether to perform NLP on the articles.")
203
+ period: Annotated[int, Field(description="Number of days to look back for articles.", ge=1)] = 7,
204
+ max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10,
205
+ full_data: Annotated[
206
+ bool,
207
+ Field(
208
+ description="Return full data for each article. If False a summary should be created by setting the summarize flag"
209
+ ),
210
+ ] = False,
211
+ summarize: Annotated[
212
+ bool,
213
+ Field(
214
+ description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp"
215
+ ),
174
216
  ] = True,
175
217
  ) -> list[ArticleOut]:
218
+ set_newspaper_article_fields(full_data)
176
219
  articles = await news.get_news_by_location(
177
220
  location=location,
178
221
  period=period,
179
222
  max_results=max_results,
180
- nlp=nlp,
223
+ nlp=False,
224
+ report_progress=ctx.report_progress,
181
225
  )
226
+ if summarize:
227
+ total_articles = len(articles)
228
+ try:
229
+ for idx, article in enumerate(articles):
230
+ await summarize_article(article, ctx)
231
+ await ctx.report_progress(idx, total_articles)
232
+ except Exception as err:
233
+ await ctx.debug(f"Failed to use LLM sampling for article summary:\n{err.args}")
234
+ for idx, article in enumerate(articles):
235
+ article.nlp()
236
+ await ctx.report_progress(idx, total_articles)
237
+ await ctx.report_progress(progress=len(articles), total=len(articles))
182
238
  return [ArticleOut(**a.to_json(False)) for a in articles]
183
239
 
184
240
 
185
- @mcp.tool(
186
- description=news.get_news_by_topic.__doc__, tags={"news", "articles", "topic"}
187
- )
241
+ @mcp.tool(description=news.get_news_by_topic.__doc__, tags={"news", "articles", "topic"})
188
242
  async def get_news_by_topic(
243
+ ctx: Context,
189
244
  topic: Annotated[str, Field(description="Topic to search for articles.")],
190
- period: Annotated[
191
- int, Field(description="Number of days to look back for articles.", ge=1)
192
- ] = 7,
193
- max_results: Annotated[
194
- int, Field(description="Maximum number of results to return.", ge=1)
195
- ] = 10,
196
- nlp: Annotated[
197
- bool, Field(description="Whether to perform NLP on the articles.")
245
+ period: Annotated[int, Field(description="Number of days to look back for articles.", ge=1)] = 7,
246
+ max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10,
247
+ full_data: Annotated[
248
+ bool,
249
+ Field(
250
+ description="Return full data for each article. If False a summary should be created by setting the summarize flag"
251
+ ),
252
+ ] = False,
253
+ summarize: Annotated[
254
+ bool,
255
+ Field(
256
+ description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp"
257
+ ),
198
258
  ] = True,
199
259
  ) -> list[ArticleOut]:
260
+ set_newspaper_article_fields(full_data)
200
261
  articles = await news.get_news_by_topic(
201
262
  topic=topic,
202
263
  period=period,
203
264
  max_results=max_results,
204
- nlp=nlp,
265
+ nlp=False,
266
+ report_progress=ctx.report_progress,
205
267
  )
268
+ if summarize:
269
+ total_articles = len(articles)
270
+ try:
271
+ for idx, article in enumerate(articles):
272
+ await summarize_article(article, ctx)
273
+ await ctx.report_progress(idx, total_articles)
274
+ except Exception as err:
275
+ await ctx.debug(f"Failed to use LLM sampling for article summary:\n{err.args}")
276
+ for idx, article in enumerate(articles):
277
+ article.nlp()
278
+ await ctx.report_progress(idx, total_articles)
279
+
280
+ await ctx.report_progress(progress=len(articles), total=len(articles))
206
281
  return [ArticleOut(**a.to_json(False)) for a in articles]
207
282
 
208
283
 
209
284
  @mcp.tool(description=news.get_top_news.__doc__, tags={"news", "articles", "top"})
210
285
  async def get_top_news(
211
- period: Annotated[
212
- int, Field(description="Number of days to look back for top articles.", ge=1)
213
- ] = 3,
214
- max_results: Annotated[
215
- int, Field(description="Maximum number of results to return.", ge=1)
216
- ] = 10,
217
- nlp: Annotated[
218
- bool, Field(description="Whether to perform NLP on the articles.")
286
+ ctx: Context,
287
+ period: Annotated[int, Field(description="Number of days to look back for top articles.", ge=1)] = 3,
288
+ max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10,
289
+ full_data: Annotated[
290
+ bool,
291
+ Field(
292
+ description="Return full data for each article. If False a summary should be created by setting the summarize flag"
293
+ ),
294
+ ] = False,
295
+ summarize: Annotated[
296
+ bool,
297
+ Field(
298
+ description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp"
299
+ ),
219
300
  ] = True,
220
301
  ) -> list[ArticleOut]:
302
+ set_newspaper_article_fields(full_data)
221
303
  articles = await news.get_top_news(
222
304
  period=period,
223
305
  max_results=max_results,
224
- nlp=nlp,
306
+ nlp=False,
307
+ report_progress=ctx.report_progress,
225
308
  )
309
+ if summarize:
310
+ total_articles = len(articles)
311
+ try:
312
+ for idx, article in enumerate(articles):
313
+ await summarize_article(article, ctx)
314
+ await ctx.report_progress(idx, total_articles)
315
+ except Exception as err:
316
+ await ctx.debug(f"Failed to use LLM sampling for article summary:\n{err.args}")
317
+ for idx, article in enumerate(articles):
318
+ article.nlp()
319
+ await ctx.report_progress(idx, total_articles)
320
+
321
+ await ctx.report_progress(progress=len(articles), total=len(articles))
226
322
  return [ArticleOut(**a.to_json(False)) for a in articles]
227
323
 
228
324
 
229
- @mcp.tool(
230
- description=news.get_trending_terms.__doc__, tags={"trends", "google", "trending"}
231
- )
325
+ @mcp.tool(description=news.get_trending_terms.__doc__, tags={"trends", "google", "trending"})
232
326
  async def get_trending_terms(
233
- geo: Annotated[
234
- str, Field(description="Country code, e.g. 'US', 'GB', 'IN', etc.")
235
- ] = "US",
327
+ geo: Annotated[str, Field(description="Country code, e.g. 'US', 'GB', 'IN', etc.")] = "US",
236
328
  full_data: Annotated[
237
329
  bool,
238
- Field(
239
- description="Return full data for each trend. Should be False for most use cases."
240
- ),
330
+ Field(description="Return full data for each trend. Should be False for most use cases."),
241
331
  ] = False,
242
- max_results: Annotated[
243
- int, Field(description="Maximum number of results to return.", ge=1)
244
- ] = 100,
332
+ max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 100,
245
333
  ) -> list[TrendingTermOut]:
246
- trends = await news.get_trending_terms(
247
- geo=geo, full_data=full_data, max_results=max_results
248
- )
334
+
249
335
  if not full_data:
250
- # Only return keyword and volume fields
251
- return [
252
- TrendingTermOut(keyword=tt["keyword"], volume=tt["volume"]) for tt in trends
253
- ]
254
- else:
255
- # Assume each tt is a TrendingTerm object
256
- return [TrendingTermOut(**tt.__dict__) for tt in trends]
336
+ trends = await news.get_trending_terms(geo=geo, full_data=False, max_results=max_results)
337
+ return [TrendingTermOut(keyword=str(tt["keyword"]), volume=tt["volume"]) for tt in trends]
338
+
339
+ trends = await news.get_trending_terms(geo=geo, full_data=True, max_results=max_results)
340
+ return [TrendingTermOut(**tt.__dict__) for tt in trends]
341
+
257
342
 
258
343
  def main():
259
344
  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.8
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,14 +30,13 @@ 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
 
37
36
  - Search Google News articles based on keyword, location, topic
38
37
  - Get top news stories from Google News
39
38
  - Google Trends keywords base on location
40
- - Optional NLP to summarize articles and extract keywords
39
+ - Optional LLM Sampling and NLP to summarize articles and extract keywords
41
40
 
42
41
  ## Installation
43
42
 
@@ -71,7 +70,7 @@ Add to your Claude settings:
71
70
  "mcpServers": {
72
71
  "google-news-trends": {
73
72
  "command": "uvx",
74
- "args": ["google-news-trends-mcp"]
73
+ "args": ["google-news-trends-mcp@latest"]
75
74
  }
76
75
  }
77
76
  }
@@ -104,7 +103,7 @@ Add to your Claude settings:
104
103
  "servers": {
105
104
  "google-news-trends": {
106
105
  "command": "uvx",
107
- "args": ["google-news-trends-mcp"]
106
+ "args": ["google-news-trends-mcp@latest"]
108
107
  }
109
108
  }
110
109
  }
@@ -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=-Cith02x6-9o91rXpgMM0lrhArPDMB9d3h8AAE1rimw,3959
4
+ google_news_trends_mcp/news.py,sha256=Anxs65Fxq1Qz_tkmVyTDY3Fn-I0dv0xR3ipDrLBc6gw,12851
5
+ google_news_trends_mcp/server.py,sha256=promIVXRcd1ZUSgFClZ73l2scXlsS-joRHv1AZs73SE,14946
6
+ google_news_trends_mcp-0.1.8.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
7
+ google_news_trends_mcp-0.1.8.dist-info/METADATA,sha256=j0yuLLp3OaCYS07o-Q5vNVs3xCk5HHAMcGVUn7kT2TI,4495
8
+ google_news_trends_mcp-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ google_news_trends_mcp-0.1.8.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
10
+ google_news_trends_mcp-0.1.8.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
11
+ google_news_trends_mcp-0.1.8.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,,