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.
- google_news_trends_mcp/cli.py +15 -49
- google_news_trends_mcp/news.py +52 -16
- google_news_trends_mcp/server.py +235 -150
- {google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/METADATA +4 -5
- google_news_trends_mcp-0.1.8.dist-info/RECORD +11 -0
- google_news_trends_mcp-0.1.6.dist-info/RECORD +0 -11
- {google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/WHEEL +0 -0
- {google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/entry_points.txt +0 -0
- {google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/top_level.txt +0 -0
google_news_trends_mcp/cli.py
CHANGED
@@ -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
|
-
|
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
|
|
google_news_trends_mcp/news.py
CHANGED
@@ -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],
|
152
|
-
|
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
|
-
|
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,
|
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,
|
187
|
-
|
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,
|
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,
|
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
|
google_news_trends_mcp/server.py
CHANGED
@@ -1,123 +1,87 @@
|
|
1
|
-
from
|
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
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
47
|
-
|
48
|
-
)
|
49
|
-
|
50
|
-
|
51
|
-
] = None
|
52
|
-
|
53
|
-
|
54
|
-
)
|
55
|
-
|
56
|
-
|
57
|
-
] = None
|
58
|
-
|
59
|
-
|
60
|
-
] = None
|
61
|
-
|
62
|
-
|
63
|
-
] = None
|
64
|
-
|
65
|
-
|
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(
|
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(
|
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[
|
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
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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=
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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=
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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=
|
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
|
-
|
212
|
-
|
213
|
-
] =
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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=
|
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
|
-
|
247
|
-
geo=geo, full_data=full_data, max_results=max_results
|
248
|
-
)
|
334
|
+
|
249
335
|
if not full_data:
|
250
|
-
|
251
|
-
return [
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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.
|
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,,
|
File without changes
|
{google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/entry_points.txt
RENAMED
File without changes
|
{google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{google_news_trends_mcp-0.1.6.dist-info → google_news_trends_mcp-0.1.8.dist-info}/top_level.txt
RENAMED
File without changes
|