google-news-trends-mcp 0.1.10__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 -33
- google_news_trends_mcp/server.py +9 -1
- {google_news_trends_mcp-0.1.10.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.10.dist-info/RECORD +0 -11
- {google_news_trends_mcp-0.1.10.dist-info → google_news_trends_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {google_news_trends_mcp-0.1.10.dist-info → google_news_trends_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {google_news_trends_mcp-0.1.10.dist-info → google_news_trends_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {google_news_trends_mcp-0.1.10.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
|
|
@@ -217,8 +239,7 @@ async def get_news_by_location(
|
|
217
239
|
nlp: bool = True,
|
218
240
|
report_progress: Optional[ProgressCallback] = None,
|
219
241
|
) -> list[newspaper.Article]:
|
220
|
-
"""Find articles by location using Google News.
|
221
|
-
"""
|
242
|
+
"""Find articles by location using Google News."""
|
222
243
|
google_news.period = f"{period}d"
|
223
244
|
google_news.max_results = max_results
|
224
245
|
gnews_articles = google_news.get_news_by_location(location)
|
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=wmFzikDEx_HNVO0vm84gnkgV-LZBOAuW7mNr2uhurEE,11524
|
5
|
-
google_news_trends_mcp/server.py,sha256=h8GP_XUPqiPw4vFu1jy9MFv0i384rBARePvm15YOZJo,14807
|
6
|
-
google_news_trends_mcp-0.1.10.dist-info/licenses/LICENSE,sha256=5dsv2ZI5EZIer0a9MktVmILVrlp5vqH_0tPIe3bRLgE,1067
|
7
|
-
google_news_trends_mcp-0.1.10.dist-info/METADATA,sha256=rbYqd15smnZA0sgOU4Fk6iqvsr8h638U69Ki4VRMddI,4521
|
8
|
-
google_news_trends_mcp-0.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
9
|
-
google_news_trends_mcp-0.1.10.dist-info/entry_points.txt,sha256=eVT3xd6YJQgsWAUBwhnffuwhXNF7yyt_uco6fjBy-1o,130
|
10
|
-
google_news_trends_mcp-0.1.10.dist-info/top_level.txt,sha256=RFheDbzhNnEV_Y3iFNm7jhRhY1P1wQgfiYqVpXCTD_U,23
|
11
|
-
google_news_trends_mcp-0.1.10.dist-info/RECORD,,
|
File without changes
|
{google_news_trends_mcp-0.1.10.dist-info → google_news_trends_mcp-0.2.0.dist-info}/entry_points.txt
RENAMED
File without changes
|
{google_news_trends_mcp-0.1.10.dist-info → google_news_trends_mcp-0.2.0.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{google_news_trends_mcp-0.1.10.dist-info → google_news_trends_mcp-0.2.0.dist-info}/top_level.txt
RENAMED
File without changes
|