google-news-trends-mcp 0.1.9__py3-none-any.whl → 0.2.0__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/__init__.py +1 -0
- google_news_trends_mcp/cli.py +43 -20
- google_news_trends_mcp/news.py +54 -51
- google_news_trends_mcp/server.py +9 -1
- {google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/METADATA +1 -3
- google_news_trends_mcp-0.2.0.dist-info/RECORD +11 -0
- google_news_trends_mcp-0.1.9.dist-info/RECORD +0 -11
- {google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/top_level.txt +0 -0
google_news_trends_mcp/cli.py
CHANGED
@@ -7,6 +7,7 @@ from google_news_trends_mcp.news import (
|
|
7
7
|
get_trending_terms,
|
8
8
|
get_top_news,
|
9
9
|
save_article_to_json,
|
10
|
+
BrowserManager,
|
10
11
|
)
|
11
12
|
|
12
13
|
|
@@ -27,9 +28,13 @@ def cli():
|
|
27
28
|
)
|
28
29
|
@click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
|
29
30
|
def keyword(keyword, period, max_results, no_nlp):
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
@BrowserManager()
|
32
|
+
async def _keyword():
|
33
|
+
articles = await get_news_by_keyword(keyword, period=period, max_results=max_results, nlp=not no_nlp)
|
34
|
+
print_articles(articles)
|
35
|
+
print(f"Found {len(articles)} articles for keyword '{keyword}'.")
|
36
|
+
|
37
|
+
asyncio.run(_keyword())
|
33
38
|
|
34
39
|
|
35
40
|
@cli.command(help=get_news_by_location.__doc__)
|
@@ -44,8 +49,13 @@ def keyword(keyword, period, max_results, no_nlp):
|
|
44
49
|
)
|
45
50
|
@click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
|
46
51
|
def location(location, period, max_results, no_nlp):
|
47
|
-
|
48
|
-
|
52
|
+
@BrowserManager()
|
53
|
+
async def _location():
|
54
|
+
articles = await get_news_by_location(location, period=period, max_results=max_results, nlp=not no_nlp)
|
55
|
+
print_articles(articles)
|
56
|
+
print(f"Found {len(articles)} articles for location '{location}'.")
|
57
|
+
|
58
|
+
asyncio.run(_location())
|
49
59
|
|
50
60
|
|
51
61
|
@cli.command(help=get_news_by_topic.__doc__)
|
@@ -60,8 +70,13 @@ def location(location, period, max_results, no_nlp):
|
|
60
70
|
)
|
61
71
|
@click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
|
62
72
|
def topic(topic, period, max_results, no_nlp):
|
63
|
-
|
64
|
-
|
73
|
+
@BrowserManager()
|
74
|
+
async def _topic():
|
75
|
+
articles = await get_news_by_topic(topic, period=period, max_results=max_results, nlp=not no_nlp)
|
76
|
+
print_articles(articles)
|
77
|
+
print(f"Found {len(articles)} articles for topic '{topic}'.")
|
78
|
+
|
79
|
+
asyncio.run(_topic())
|
65
80
|
|
66
81
|
|
67
82
|
@cli.command(help=get_trending_terms.__doc__)
|
@@ -75,16 +90,20 @@ def topic(topic, period, max_results, no_nlp):
|
|
75
90
|
help="Maximum number of results to return.",
|
76
91
|
)
|
77
92
|
def trending(geo, full_data, max_results):
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
93
|
+
# Browser not used for Google Trends
|
94
|
+
async def _trending():
|
95
|
+
trending_terms = await get_trending_terms(geo=geo, full_data=full_data, max_results=max_results)
|
96
|
+
if trending_terms:
|
97
|
+
print("Trending terms:")
|
98
|
+
for term in trending_terms:
|
99
|
+
if isinstance(term, dict):
|
100
|
+
print(f"{term['keyword']:<40} - {term['volume']}")
|
101
|
+
else:
|
102
|
+
print(term)
|
103
|
+
else:
|
104
|
+
print("No trending terms found.")
|
105
|
+
|
106
|
+
asyncio.run(_trending())
|
88
107
|
|
89
108
|
|
90
109
|
@cli.command(help=get_top_news.__doc__)
|
@@ -98,9 +117,13 @@ def trending(geo, full_data, max_results):
|
|
98
117
|
)
|
99
118
|
@click.option("--no-nlp", is_flag=True, default=False, help="Disable NLP processing for articles.")
|
100
119
|
def top(period, max_results, no_nlp):
|
101
|
-
|
102
|
-
|
103
|
-
|
120
|
+
@BrowserManager()
|
121
|
+
async def _top():
|
122
|
+
articles = await get_top_news(max_results=max_results, period=period, nlp=not no_nlp)
|
123
|
+
print_articles(articles)
|
124
|
+
print(f"Found {len(articles)} top articles.")
|
125
|
+
|
126
|
+
asyncio.run(_top())
|
104
127
|
|
105
128
|
|
106
129
|
def print_articles(articles):
|
google_news_trends_mcp/news.py
CHANGED
@@ -16,8 +16,7 @@ import cloudscraper
|
|
16
16
|
from playwright.async_api import async_playwright, Browser, Playwright
|
17
17
|
from trendspy import Trends, TrendKeyword
|
18
18
|
from typing import Optional, cast, overload, Literal, Awaitable
|
19
|
-
import
|
20
|
-
from contextlib import asynccontextmanager
|
19
|
+
from contextlib import asynccontextmanager, AsyncContextDecorator
|
21
20
|
import logging
|
22
21
|
from collections.abc import Callable
|
23
22
|
|
@@ -55,34 +54,56 @@ browser: Optional[Browser] = None
|
|
55
54
|
ProgressCallback = Callable[[float, Optional[float]], Awaitable[None]]
|
56
55
|
|
57
56
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
async def
|
73
|
-
|
74
|
-
await
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
57
|
+
class BrowserManager(AsyncContextDecorator):
|
58
|
+
playwright: Optional[Playwright] = None
|
59
|
+
browser: Optional[Browser] = None
|
60
|
+
_lock = asyncio.Lock()
|
61
|
+
|
62
|
+
@classmethod
|
63
|
+
async def _get_browser(cls) -> Browser:
|
64
|
+
if cls.browser is None:
|
65
|
+
async with cls._lock:
|
66
|
+
if cls.browser is None:
|
67
|
+
await cls._startup()
|
68
|
+
return cast(Browser, cls.browser)
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
async def _startup(cls):
|
72
|
+
logger.info("Starting browser...")
|
73
|
+
cls.playwright = await async_playwright().start()
|
74
|
+
cls.browser = await cls.playwright.chromium.launch(headless=True)
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
async def _shutdown(cls):
|
78
|
+
logger.info("Shutting down browser...")
|
79
|
+
if cls.browser:
|
80
|
+
await cls.browser.close()
|
81
|
+
cls.browser = None
|
82
|
+
if cls.playwright:
|
83
|
+
await cls.playwright.stop()
|
84
|
+
cls.playwright = None
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
def browser_context(cls):
|
88
|
+
@asynccontextmanager
|
89
|
+
async def _browser_context_cm():
|
90
|
+
browser_inst = await cls._get_browser()
|
91
|
+
context = await browser_inst.new_context()
|
92
|
+
logging.debug("Created browser context...")
|
93
|
+
try:
|
94
|
+
yield context
|
95
|
+
finally:
|
96
|
+
logging.debug("Closing browser context...")
|
97
|
+
await context.close()
|
98
|
+
|
99
|
+
return _browser_context_cm()
|
100
|
+
|
101
|
+
async def __aenter__(self):
|
102
|
+
return self
|
103
|
+
|
104
|
+
async def __aexit__(self, *exc):
|
105
|
+
await self._shutdown()
|
106
|
+
return False
|
86
107
|
|
87
108
|
|
88
109
|
async def download_article_with_playwright(url) -> newspaper.Article | None:
|
@@ -90,7 +111,7 @@ async def download_article_with_playwright(url) -> newspaper.Article | None:
|
|
90
111
|
Download an article using Playwright to handle complex websites (async).
|
91
112
|
"""
|
92
113
|
try:
|
93
|
-
async with browser_context() as context:
|
114
|
+
async with BrowserManager.browser_context() as context:
|
94
115
|
page = await context.new_page()
|
95
116
|
await page.goto(url, wait_until="domcontentloaded")
|
96
117
|
await asyncio.sleep(2) # Wait for the page to load completely
|
@@ -144,6 +165,7 @@ async def download_article(url: str) -> newspaper.Article | None:
|
|
144
165
|
return None
|
145
166
|
article = download_article_with_scraper(url)
|
146
167
|
if article is None or not article.text:
|
168
|
+
logger.debug("Attempting to download article with playwright")
|
147
169
|
article = await download_article_with_playwright(url)
|
148
170
|
return article
|
149
171
|
|
@@ -182,10 +204,6 @@ async def get_news_by_keyword(
|
|
182
204
|
) -> list[newspaper.Article]:
|
183
205
|
"""
|
184
206
|
Find articles by keyword using Google News.
|
185
|
-
keyword: is the search term to find articles.
|
186
|
-
period: is the number of days to look back for articles.
|
187
|
-
max_results: is the maximum number of results to return.
|
188
|
-
nlp: If True, will perform NLP on the articles to extract keywords and summary.
|
189
207
|
"""
|
190
208
|
google_news.period = f"{period}d"
|
191
209
|
google_news.max_results = max_results
|
@@ -204,9 +222,6 @@ async def get_top_news(
|
|
204
222
|
) -> list[newspaper.Article]:
|
205
223
|
"""
|
206
224
|
Get top news stories from Google News.
|
207
|
-
period: is the number of days to look back for top articles.
|
208
|
-
max_results: is the maximum number of results to return.
|
209
|
-
nlp: If True, will perform NLP on the articles to extract keywords and summary.
|
210
225
|
"""
|
211
226
|
google_news.period = f"{period}d"
|
212
227
|
google_news.max_results = max_results
|
@@ -224,12 +239,7 @@ async def get_news_by_location(
|
|
224
239
|
nlp: bool = True,
|
225
240
|
report_progress: Optional[ProgressCallback] = None,
|
226
241
|
) -> list[newspaper.Article]:
|
227
|
-
"""Find articles by location using Google News.
|
228
|
-
location: is the name of city/state/country
|
229
|
-
period: is the number of days to look back for articles.
|
230
|
-
max_results: is the maximum number of results to return.
|
231
|
-
nlp: If True, will perform NLP on the articles to extract keywords and summary.
|
232
|
-
"""
|
242
|
+
"""Find articles by location using Google News."""
|
233
243
|
google_news.period = f"{period}d"
|
234
244
|
google_news.max_results = max_results
|
235
245
|
gnews_articles = google_news.get_news_by_location(location)
|
@@ -256,9 +266,6 @@ async def get_news_by_topic(
|
|
256
266
|
PUBLIC HEALTH, MENTAL HEALTH, MEDICINE, SPACE, WILDLIFE, ENVIRONMENT, NEUROSCIENCE, PHYSICS,
|
257
267
|
GEOLOGY, PALEONTOLOGY, SOCIAL SCIENCES, EDUCATION, JOBS, ONLINE EDUCATION, HIGHER EDUCATION,
|
258
268
|
VEHICLES, ARTS-DESIGN, BEAUTY, FOOD, TRAVEL, SHOPPING, HOME, OUTDOORS, FASHION.
|
259
|
-
period: is the number of days to look back for articles.
|
260
|
-
max_results: is the maximum number of results to return.
|
261
|
-
nlp: If True, will perform NLP on the articles to extract keywords and summary.
|
262
269
|
"""
|
263
270
|
google_news.period = f"{period}d"
|
264
271
|
google_news.max_results = max_results
|
@@ -288,10 +295,6 @@ async def get_trending_terms(
|
|
288
295
|
) -> list[dict[str, int]] | list[TrendKeyword]:
|
289
296
|
"""
|
290
297
|
Returns google trends for a specific geo location.
|
291
|
-
Default is US.
|
292
|
-
geo: is the country code, e.g. 'US', 'GB', 'IN', etc.
|
293
|
-
full_data: if True, returns full data for each trend, otherwise returns only the trend and volume.
|
294
|
-
max_results: is the maximum number of results to return, default is 100.
|
295
298
|
"""
|
296
299
|
try:
|
297
300
|
trends = list(tr.trending_now(geo=geo))
|
google_news_trends_mcp/server.py
CHANGED
@@ -7,8 +7,10 @@ from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
|
|
7
7
|
from mcp.types import TextContent
|
8
8
|
from pydantic import BaseModel, Field, model_serializer
|
9
9
|
from google_news_trends_mcp import news
|
10
|
+
from google_news_trends_mcp.news import BrowserManager
|
10
11
|
from newspaper import settings as newspaper_settings
|
11
12
|
from newspaper.article import Article
|
13
|
+
from contextlib import asynccontextmanager
|
12
14
|
|
13
15
|
|
14
16
|
class BaseModelClean(BaseModel):
|
@@ -82,9 +84,16 @@ class TrendingTermOut(BaseModelClean):
|
|
82
84
|
normalized_keyword: Annotated[Optional[str], Field(description="Normalized form of the keyword.")] = None
|
83
85
|
|
84
86
|
|
87
|
+
@asynccontextmanager
|
88
|
+
async def lifespan(app: FastMCP):
|
89
|
+
async with BrowserManager():
|
90
|
+
yield
|
91
|
+
|
92
|
+
|
85
93
|
mcp = FastMCP(
|
86
94
|
name="google-news-trends",
|
87
95
|
instructions="This server provides tools to search, analyze, and summarize Google News articles and Google Trends",
|
96
|
+
lifespan=lifespan,
|
88
97
|
on_duplicate_tools="replace",
|
89
98
|
)
|
90
99
|
|
@@ -328,7 +337,6 @@ async def get_trending_terms(
|
|
328
337
|
] = False,
|
329
338
|
max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 100,
|
330
339
|
) -> list[TrendingTermOut]:
|
331
|
-
|
332
340
|
if not full_data:
|
333
341
|
trends = await news.get_trending_terms(geo=geo, full_data=False, max_results=max_results)
|
334
342
|
return [TrendingTermOut(keyword=str(tt["keyword"]), volume=tt["volume"]) for tt in trends]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: google-news-trends-mcp
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
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
|
@@ -22,8 +22,6 @@ Requires-Dist: newspaper4k>=0.9.3.1
|
|
22
22
|
Requires-Dist: nltk>=3.9.1
|
23
23
|
Requires-Dist: playwright>=1.53.0
|
24
24
|
Requires-Dist: pydantic>=2.11.7
|
25
|
-
Requires-Dist: pytest>=8.4.1
|
26
|
-
Requires-Dist: pytest-asyncio>=1.0.0
|
27
25
|
Requires-Dist: trendspy>=0.1.6
|
28
26
|
Dynamic: license-file
|
29
27
|
|
@@ -0,0 +1,11 @@
|
|
1
|
+
google_news_trends_mcp/__init__.py,sha256=nDWNd6_TSf4vDQuHVBoAf4QfZCB3ZUFQ0M7XvifNJ-g,78
|
2
|
+
google_news_trends_mcp/__main__.py,sha256=ysiAk_xpnnW3lrLlzdIQQa71tuGBRT8WocbecBsY2Fs,87
|
3
|
+
google_news_trends_mcp/cli.py,sha256=3Z916898HXTigmQYEfvb7ybfbuUE7bjMC6yjT5-l6u0,4558
|
4
|
+
google_news_trends_mcp/news.py,sha256=MPNZlzI7KXkhQ2uj7233N2i9kFHGUgGMdRBCAbj-B44,12471
|
5
|
+
google_news_trends_mcp/server.py,sha256=S-tlFY1wiFm9VPeb4NDnV0NGtczaQDmx20kIrZZQHto,15031
|
6
|
+
google_news_trends_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
|
7
|
+
google_news_trends_mcp-0.2.0.dist-info/METADATA,sha256=rok_3L-eDVXQJSuG6ze1Vuicnh-kpcWyJxaF2DbqZ1s,4454
|
8
|
+
google_news_trends_mcp-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
9
|
+
google_news_trends_mcp-0.2.0.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
|
10
|
+
google_news_trends_mcp-0.2.0.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
|
11
|
+
google_news_trends_mcp-0.2.0.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
google_news_trends_mcp/__init__.py,sha256=NkmudPEEuKk8Geah4EtzeEHQ-ChqR66lZEO5VrMwXNo,77
|
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=CpNIOJ4NA-BFmiE0d4Jadn20apMTf8vNDMsqZjFVl6A,12707
|
5
|
-
google_news_trends_mcp/server.py,sha256=h8GP_XUPqiPw4vFu1jy9MFv0i384rBARePvm15YOZJo,14807
|
6
|
-
google_news_trends_mcp-0.1.9.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
|
7
|
-
google_news_trends_mcp-0.1.9.dist-info/METADATA,sha256=t76FntOxc0t_CFvzcaWB0lVdXmcv5J9SnLCcIYMwcfY,4520
|
8
|
-
google_news_trends_mcp-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
9
|
-
google_news_trends_mcp-0.1.9.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
|
10
|
-
google_news_trends_mcp-0.1.9.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
|
11
|
-
google_news_trends_mcp-0.1.9.dist-info/RECORD,,
|
File without changes
|
{google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/entry_points.txt
RENAMED
File without changes
|
{google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{google_news_trends_mcp-0.1.9.dist-info → google_news_trends_mcp-0.2.0.dist-info}/top_level.txt
RENAMED
File without changes
|