Skip to main content
Glama
jmanek

google-news-trends-mcp

by jmanek
server.py13.4 kB
from typing import Annotated, Optional, Any, TYPE_CHECKING from fastmcp import FastMCP, Context from fastmcp.server.middleware.timing import TimingMiddleware from fastmcp.server.middleware.logging import LoggingMiddleware from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware from mcp.types import TextContent from pydantic import BaseModel, Field, model_serializer from google_news_trends_mcp import news from google_news_trends_mcp.news import BrowserManager from newspaper import settings as newspaper_settings from newspaper.article import Article from contextlib import asynccontextmanager class BaseModelClean(BaseModel): @model_serializer def serializer(self, **kwargs) -> dict[str, Any]: return { field: self.__getattribute__(field) for field in self.model_fields_set if self.__getattribute__(field) is not None } if TYPE_CHECKING: def model_dump(self, **kwargs) -> dict[str, Any]: ... class ArticleOut(BaseModelClean): title: Annotated[str, Field(description="Title of the article.")] url: Annotated[str, Field(description="Original article URL.")] read_more_link: Annotated[Optional[str], Field(description="Link to read more about the article.")] = None language: Annotated[Optional[str], Field(description="Language code of the article.")] = None meta_img: Annotated[Optional[str], Field(description="Meta image URL.")] = None movies: Annotated[Optional[list[str]], Field(description="List of movie URLs or IDs.")] = None meta_favicon: Annotated[Optional[str], Field(description="Favicon URL from meta data.")] = None meta_site_name: Annotated[Optional[str], Field(description="Site name from meta data.")] = None authors: Annotated[Optional[list[str]], Field(description="list of authors.")] = None publish_date: Annotated[Optional[str], Field(description="Publish date in ISO format.")] = None top_image: Annotated[Optional[str], Field(description="URL of the top image.")] = None images: Annotated[Optional[list[str]], Field(description="list of image URLs.")] = None text: Annotated[Optional[str], Field(description="Full text of the article.")] = None summary: Annotated[Optional[str], Field(description="Summary of the article.")] = None keywords: Annotated[Optional[list[str]], Field(description="Extracted keywords.")] = None tags: Annotated[Optional[list[str]], Field(description="Tags for the article.")] = None meta_keywords: Annotated[Optional[list[str]], Field(description="Meta keywords from the article.")] = None meta_description: Annotated[Optional[str], Field(description="Meta description from the article.")] = None canonical_link: Annotated[Optional[str], Field(description="Canonical link for the article.")] = None meta_data: Annotated[Optional[dict[str, str | int]], Field(description="Meta data dictionary.")] = None meta_lang: Annotated[Optional[str], Field(description="Language of the article.")] = None source_url: Annotated[Optional[str], Field(description="Source URL if different from original.")] = None class TrendingTermArticleOut(BaseModelClean): title: Annotated[str, Field(description="Article title.")] = "" url: Annotated[str, Field(description="Article URL.")] = "" source: Annotated[Optional[str], Field(description="News source name.")] = None picture: Annotated[Optional[str], Field(description="URL to article image.")] = None time: Annotated[Optional[str | int], Field(description="Publication time or timestamp.")] = None snippet: Annotated[Optional[str], Field(description="Article preview text.")] = None class TrendingTermOut(BaseModelClean): keyword: Annotated[str, Field(description="Trending keyword.")] volume: Annotated[Optional[str], Field(description="Search volume.")] = None trend_keywords: Annotated[Optional[list[str]], Field(description="Related keywords.")] = None link: Annotated[Optional[str], Field(description="URL to more information.")] = None started: Annotated[Optional[int], Field(description="Unix timestamp when the trend started.")] = None picture: Annotated[Optional[str], Field(description="URL to related image.")] = None picture_source: Annotated[Optional[str], Field(description="Source of the picture.")] = None news: Annotated[ Optional[list[TrendingTermArticleOut]], Field(description="Related news articles."), ] = None @asynccontextmanager async def lifespan(app: FastMCP): async with BrowserManager(): yield mcp = FastMCP( name="google-news-trends", instructions="This server provides tools to search, analyze, and summarize Google News articles and Google Trends", lifespan=lifespan, on_duplicate_tools="replace", ) mcp.add_middleware(ErrorHandlingMiddleware()) # Handle errors first mcp.add_middleware(RateLimitingMiddleware(max_requests_per_second=50)) mcp.add_middleware(TimingMiddleware()) # Time actual execution mcp.add_middleware(LoggingMiddleware()) # Log everything def set_newspaper_article_fields(full_data: bool = False): if full_data: newspaper_settings.article_json_fields = [ "url", "read_more_link", "language", "title", "top_image", "meta_img", "images", "movies", "keywords", "keyword_scores", "meta_keywords", "tags", "authors", "publish_date", "summary", "meta_description", "meta_lang", "meta_favicon", "meta_site_name", "canonical_link", "text", ] else: newspaper_settings.article_json_fields = [ "url", "title", "publish_date", "summary", ] async def llm_summarize_article(article: Article, ctx: Context) -> None: if article.text: prompt = f"Please provide a concise summary of the following news article:\n\n{article.text}" response = await ctx.sample(prompt) if isinstance(response, TextContent): if not response.text: await ctx.warning("LLM Sampling response is empty. Unable to summarize article.") article.summary = "No summary available." else: article.summary = response.text else: await ctx.warning("LLM Sampling response is not a TextContent object. Unable to summarize article.") article.summary = "No summary available." else: article.summary = "No summary available." async def summarize_articles(articles: list[Article], ctx: Context) -> None: total_articles = len(articles) try: for idx, article in enumerate(articles): await llm_summarize_article(article, ctx) await ctx.report_progress(idx, total_articles) except Exception as err: await ctx.debug(f"Failed to use LLM sampling for article summary:\n{err.args}") for idx, article in enumerate(articles): article.nlp() await ctx.report_progress(idx, total_articles) @mcp.tool( description=news.get_news_by_keyword.__doc__, tags={"news", "articles", "keyword"}, ) async def get_news_by_keyword( ctx: Context, keyword: Annotated[str, Field(description="Search term to find articles.")], period: Annotated[int, Field(description="Number of days to look back for articles.", ge=1)] = 7, max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10, full_data: Annotated[ bool, Field( description="Return full data for each article. If False a summary should be created by setting the summarize flag" ), ] = False, summarize: Annotated[ bool, Field( description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp" ), ] = True, ) -> list[ArticleOut]: set_newspaper_article_fields(full_data) articles = await news.get_news_by_keyword( keyword=keyword, period=period, max_results=max_results, nlp=False, report_progress=ctx.report_progress, ) if summarize: await summarize_articles(articles, ctx) await ctx.report_progress(progress=len(articles), total=len(articles)) return [ArticleOut(**a.to_json(False)) for a in articles] @mcp.tool( description=news.get_news_by_location.__doc__, tags={"news", "articles", "location"}, ) async def get_news_by_location( ctx: Context, location: Annotated[str, Field(description="Name of city/state/country.")], period: Annotated[int, Field(description="Number of days to look back for articles.", ge=1)] = 7, max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10, full_data: Annotated[ bool, Field( description="Return full data for each article. If False a summary should be created by setting the summarize flag" ), ] = False, summarize: Annotated[ bool, Field( description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp" ), ] = True, ) -> list[ArticleOut]: set_newspaper_article_fields(full_data) articles = await news.get_news_by_location( location=location, period=period, max_results=max_results, nlp=False, report_progress=ctx.report_progress, ) if summarize: await summarize_articles(articles, ctx) await ctx.report_progress(progress=len(articles), total=len(articles)) return [ArticleOut(**a.to_json(False)) for a in articles] @mcp.tool(description=news.get_news_by_topic.__doc__, tags={"news", "articles", "topic"}) async def get_news_by_topic( ctx: Context, topic: Annotated[str, Field(description="Topic to search for articles.")], period: Annotated[int, Field(description="Number of days to look back for articles.", ge=1)] = 7, max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10, full_data: Annotated[ bool, Field( description="Return full data for each article. If False a summary should be created by setting the summarize flag" ), ] = False, summarize: Annotated[ bool, Field( description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp" ), ] = True, ) -> list[ArticleOut]: set_newspaper_article_fields(full_data) articles = await news.get_news_by_topic( topic=topic, period=period, max_results=max_results, nlp=False, report_progress=ctx.report_progress, ) if summarize: await summarize_articles(articles, ctx) await ctx.report_progress(progress=len(articles), total=len(articles)) return [ArticleOut(**a.to_json(False)) for a in articles] @mcp.tool(description=news.get_top_news.__doc__, tags={"news", "articles", "top"}) async def get_top_news( ctx: Context, period: Annotated[int, Field(description="Number of days to look back for top articles.", ge=1)] = 3, max_results: Annotated[int, Field(description="Maximum number of results to return.", ge=1)] = 10, full_data: Annotated[ bool, Field( description="Return full data for each article. If False a summary should be created by setting the summarize flag" ), ] = False, summarize: Annotated[ bool, Field( description="Generate a summary of the article, will first try LLM Sampling but if unavailable will use nlp" ), ] = True, ) -> list[ArticleOut]: set_newspaper_article_fields(full_data) articles = await news.get_top_news( period=period, max_results=max_results, nlp=False, report_progress=ctx.report_progress, ) if summarize: await summarize_articles(articles, ctx) await ctx.report_progress(progress=len(articles), total=len(articles)) return [ArticleOut(**a.to_json(False)) for a in articles] @mcp.tool(description=news.get_trending_terms.__doc__, tags={"trends", "google", "trending"}) async def get_trending_terms( geo: Annotated[str, Field(description="Country code, e.g. 'US', 'GB', 'IN', etc.")] = "US", full_data: Annotated[ bool, Field(description="Return full data for each trend. Should be False for most use cases."), ] = False, ) -> list[TrendingTermOut]: if not full_data: trends = await news.get_trending_terms(geo=geo, full_data=False) return [TrendingTermOut(keyword=str(tt["keyword"]), volume=tt["volume"]) for tt in trends] trends = await news.get_trending_terms(geo=geo, full_data=True) trends_out = [] for trend in trends: trend = trend.__dict__ if "news" in trend: trend["news"] = [TrendingTermArticleOut(**article.__dict__) for article in trend["news"]] trends_out.append(TrendingTermOut(**trend)) return trends_out def main(): mcp.run()

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmanek/google-news-trends-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server