From f663305a3ad097e255d3f9545e17c9a80c4304f4 Mon Sep 17 00:00:00 2001 From: arjunjain Date: Thu, 13 Aug 2026 11:51:03 +0530 Subject: [PATCH] Rename market symbol filter to market_id; render market_id on articles Sync with python-client: the get_market_news and get_market_counts tools now take market_id instead of symbol. SYMBOL_FILTER becomes MARKET_ID_FILTER. The old filter was removed upstream, so no alias is kept. The article formatter also renders market_id, which the API returns alongside symbol. Both are separate response fields and both are shown. --- README.md | 4 ++-- src/newsdata_mcp/_mcp.py | 2 +- src/newsdata_mcp/formatters.py | 1 + src/newsdata_mcp/params.py | 6 +++--- src/newsdata_mcp/tools/market.py | 16 ++++++++-------- src/newsdata_mcp/tools/market_count.py | 10 +++++----- tests/test_formatters.py | 21 +++++++++++++++++++++ tests/test_integration.py | 4 ++-- tests/test_tools.py | 22 +++++++++++----------- 9 files changed, 54 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 0e86a06..63c9c5c 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ get_crypto_news( ```text get_market_news( - symbol=["AAPL", "NVDA"], + market_id=["AAPL", "NVDA"], country="us" ) ``` @@ -219,7 +219,7 @@ get_news_counts( get_market_counts( from_date="2024-01-01", to_date="2024-03-31", - symbol=["AAPL", "NVDA"], + market_id=["AAPL", "NVDA"], interval="hour" ) ``` diff --git a/src/newsdata_mcp/_mcp.py b/src/newsdata_mcp/_mcp.py index 8f293c3..b1b98c2 100644 --- a/src/newsdata_mcp/_mcp.py +++ b/src/newsdata_mcp/_mcp.py @@ -72,7 +72,7 @@ async def _lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]: - Never pass None, null, or empty string — omit optional parameters entirely. - Multi-value filters (country, exclude_country, language, exclude_language, category, exclude_category, tag, region, domain, exclude_domain, domainurl, - coin, symbol, organization, article_id, creator, datatype, excludefield) + coin, market_id, organization, article_id, creator, datatype, excludefield) accept EITHER a Python list `['us', 'gb']` OR a comma-separated string `'us,gb'`. Lists are preferred for clarity. - Boolean flags (image, video, full_content, removeduplicate) accept True/False (preferred) diff --git a/src/newsdata_mcp/formatters.py b/src/newsdata_mcp/formatters.py index b6dc4ad..bb8ad40 100644 --- a/src/newsdata_mcp/formatters.py +++ b/src/newsdata_mcp/formatters.py @@ -99,6 +99,7 @@ def _format_article_item(article: dict[str, Any]) -> list[str]: # Domain-specific. _append_field(lines, "coins", article.get("coin")) _append_field(lines, "symbols", article.get("symbol")) + _append_field(lines, "market_id", article.get("market_id")) _append_field(lines, "datatype", article.get("datatype")) # Media. _append_field(lines, "image_url", article.get("image_url")) diff --git a/src/newsdata_mcp/params.py b/src/newsdata_mcp/params.py index 6822296..bf9c96a 100644 --- a/src/newsdata_mcp/params.py +++ b/src/newsdata_mcp/params.py @@ -378,16 +378,16 @@ def _csv_pattern(*values: str) -> str: ), ] -SYMBOL_FILTER = Annotated[ +MARKET_ID_FILTER = Annotated[ str | list[str], Field( min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._-]+(?:,[A-Za-z0-9._-]+){0,9}$", description=( - "One or more market symbols or stock tickers. " + "One or more market identifiers or stock tickers. " "Accepts either a list (preferred): `['AAPL', 'MSFT']`, " - "or a comma-separated string: `'AAPL,MSFT'`. Max 10 symbols." + "or a comma-separated string: `'AAPL,MSFT'`. Max 10 market IDs." ), examples=["AAPL", ["AAPL", "MSFT"], "TSLA,NVDA,AMZN"], ), diff --git a/src/newsdata_mcp/tools/market.py b/src/newsdata_mcp/tools/market.py index 5bf1737..65a9e31 100644 --- a/src/newsdata_mcp/tools/market.py +++ b/src/newsdata_mcp/tools/market.py @@ -12,6 +12,7 @@ EXCLUDE_FIELD_FILTER, FLAG, LANGUAGE_FILTER, + MARKET_ID_FILTER, ORGANIZATION_FILTER, PAGE, PRIORITY_DOMAIN, @@ -21,7 +22,6 @@ SENTIMENT_SCORE, SIZE, SORT, - SYMBOL_FILTER, TAG_FILTER, TIMEFRAME, TIMEZONE, @@ -61,7 +61,7 @@ async def get_market_news( organization: ORGANIZATION_FILTER | None = None, url: URL | None = None, sort: SORT | None = None, - symbol: SYMBOL_FILTER | None = None, + market_id: MARKET_ID_FILTER | None = None, country: COUNTRY_FILTER | None = None, exclude_country: COUNTRY_FILTER | None = None, creator: CREATOR_FILTER | None = None, @@ -81,9 +81,9 @@ async def get_market_news( - `sentiment_score` requires `sentiment` to also be set. Other guidance: - - Use `symbol` for stock/market tickers (e.g. `AAPL`, `TSLA,NVDA`). + - Use `market_id` for stock/market tickers (e.g. `AAPL`, `TSLA,NVDA`). - Use `organization` for company name filtering (e.g. `tesla,apple`). - - `symbol` and `organization` can be combined for precision. + - `market_id` and `organization` can be combined for precision. - `category` is NOT available on this endpoint. - Use `timeframe` OR `from_date`/`to_date`, not both. - `creator` filters by author/byline name(s). @@ -93,11 +93,11 @@ async def get_market_news( only articles whose positive-sentiment score is at least 80. Examples: - - `symbol="AAPL,MSFT", language="en", sort="relevancy"` + - `market_id="AAPL,MSFT", language="en", sort="relevancy"` - `organization="tesla,nvidia", timeframe="48", sentiment="positive"` - - `q="earnings beat", symbol="NVDA", from_date="2025-01-01"` + - `q="earnings beat", market_id="NVDA", from_date="2025-01-01"` - `country="us", priority_domain="top", sort="pubdateasc"` - - `sentiment="positive", sentiment_score=80, symbol="NVDA"` + - `sentiment="positive", sentiment_score=80, market_id="NVDA"` """ error = ( check_mutex_groups(locals()) @@ -135,7 +135,7 @@ async def get_market_news( "organization": organization, "url": url, "sort": sort, - "symbol": symbol, + "market_id": market_id, "country": country, "excludecountry": exclude_country, "creator": creator, diff --git a/src/newsdata_mcp/tools/market_count.py b/src/newsdata_mcp/tools/market_count.py index 38717f8..0ffa472 100644 --- a/src/newsdata_mcp/tools/market_count.py +++ b/src/newsdata_mcp/tools/market_count.py @@ -11,6 +11,7 @@ FLAG, INTERVAL, LANGUAGE_FILTER, + MARKET_ID_FILTER, ORGANIZATION_FILTER, PAGE, PRIORITY_DOMAIN, @@ -20,7 +21,6 @@ SENTIMENT_SCORE, SIZE, SORT, - SYMBOL_FILTER, TAG_FILTER, ) from ..validators import ( @@ -47,7 +47,7 @@ async def get_market_counts( image: FLAG | None = None, video: FLAG | None = None, organization: ORGANIZATION_FILTER | None = None, - symbol: SYMBOL_FILTER | None = None, + market_id: MARKET_ID_FILTER | None = None, priority_domain: PRIORITY_DOMAIN | None = None, page: PAGE | None = None, sentiment: SENTIMENT | None = None, @@ -84,10 +84,10 @@ async def get_market_counts( - `sentiment_score` is a minimum confidence percentage (0–100) for the chosen `sentiment` label, e.g. `sentiment="positive", sentiment_score=70` filters the count to articles whose positive-sentiment score is at least 70. - - Use `symbol` and/or `organization` to narrow to specific tickers / companies. + - Use `market_id` and/or `organization` to narrow to specific tickers / companies. Examples: - - `from_date="2024-01-01", to_date="2024-01-31", symbol="AAPL", interval="day"` + - `from_date="2024-01-01", to_date="2024-01-31", market_id="AAPL", interval="day"` → daily Apple counts in January. - `from_date="2024-01-01", to_date="2024-03-31", interval="hour", organization=["tesla", "nvidia"]` → hourly Tesla+NVIDIA counts for Q1. @@ -120,7 +120,7 @@ async def get_market_counts( "image": image, "video": video, "organization": organization, - "symbol": symbol, + "market_id": market_id, "prioritydomain": priority_domain, "page": page, "sentiment": sentiment, diff --git a/tests/test_formatters.py b/tests/test_formatters.py index b75a028..03539af 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -97,6 +97,27 @@ def test_format_article_item_omits_missing_fields(): assert not any(line.startswith("description:") for line in lines) +def test_format_article_item_renders_symbol_and_market_id(): + """Market articles carry both `symbol` and `market_id`; the two are + separate response fields and both render.""" + lines = _format_article_item( + { + "article_id": "x", + "title": "Apple beats earnings", + "symbol": ["AAPL", "MSFT"], + "market_id": ["NASDAQ:AAPL", "NASDAQ:MSFT"], + } + ) + assert "symbols: AAPL, MSFT" in lines + assert "market_id: NASDAQ:AAPL, NASDAQ:MSFT" in lines + + +def test_format_article_item_market_id_absent_emits_no_line(): + lines = _format_article_item({"article_id": "x", "symbol": ["AAPL"]}) + assert "symbols: AAPL" in lines + assert not any(line.startswith("market_id:") for line in lines) + + # ---------- format_articles ---------- def test_format_articles_error_envelope(): diff --git a/tests/test_integration.py b/tests/test_integration.py index f04aba1..2aa30a5 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -88,7 +88,7 @@ async def test_live_get_crypto_news(): async def test_live_get_market_news(): - out = await get_market_news(symbol="AAPL", size=1) + out = await get_market_news(market_id="AAPL", size=1) _assert_renders_endpoint(out, "market") @@ -103,5 +103,5 @@ async def test_live_get_crypto_counts(): async def test_live_get_market_counts(): - out = await get_market_counts(from_date="2024-01-01", to_date="2024-01-07", symbol="AAPL") + out = await get_market_counts(from_date="2024-01-01", to_date="2024-01-07", market_id="AAPL") _assert_renders_endpoint(out, "market/count") diff --git a/tests/test_tools.py b/tests/test_tools.py index d420dc4..b196ae9 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -84,13 +84,13 @@ async def test_crypto_news_uses_crypto_endpoint_and_passes_coin(): @respx.mock -async def test_market_news_uses_market_endpoint_and_passes_symbol(): +async def test_market_news_uses_market_endpoint_and_passes_market_id(): route = respx.get("https://newsdata.io/api/1/market").mock( return_value=httpx.Response(200, json={"status": "success", "results": []}) ) - await get_market_news(symbol="AAPL,NVDA", country="us") + await get_market_news(market_id="AAPL,NVDA", country="us") qs = dict(route.calls[0].request.url.params) - assert qs["symbol"] == "AAPL,NVDA" + assert qs["market_id"] == "AAPL,NVDA" assert qs["country"] == "us" @@ -300,13 +300,13 @@ async def test_crypto_news_accepts_list_for_coin(): @respx.mock -async def test_market_news_accepts_list_for_symbol(): +async def test_market_news_accepts_list_for_market_id(): route = respx.get("https://newsdata.io/api/1/market").mock( return_value=httpx.Response(200, json={"status": "success", "results": []}) ) - await get_market_news(symbol=["AAPL", "MSFT"]) + await get_market_news(market_id=["AAPL", "MSFT"]) qs = dict(route.calls[0].request.url.params) - assert qs["symbol"] == "AAPL,MSFT" + assert qs["market_id"] == "AAPL,MSFT" @respx.mock @@ -462,7 +462,7 @@ async def test_archive_news_accepts_tag_region_organization(): @respx.mock(assert_all_called=False) async def test_market_news_sentiment_score_without_sentiment_short_circuits(): - out = await get_market_news(sentiment_score=80, symbol="NVDA") + out = await get_market_news(sentiment_score=80, market_id="NVDA") assert out.startswith("Error:") assert "'sentiment_score'" in out assert "'sentiment'" in out @@ -477,12 +477,12 @@ async def test_market_news_sentiment_pair_passes(): await get_market_news( sentiment="positive", sentiment_score=80, - symbol="NVDA", + market_id="NVDA", ) qs = dict(route.calls[0].request.url.params) assert qs["sentiment"] == "positive" assert qs["sentiment_score"] == "80" - assert qs["symbol"] == "NVDA" + assert qs["market_id"] == "NVDA" @respx.mock @@ -634,12 +634,12 @@ async def test_market_counts_hits_market_count_endpoint(): out = await get_market_counts( from_date="2024-01-01", to_date="2024-01-31", - symbol="AAPL", + market_id="AAPL", interval="day", ) assert route.called qs = dict(route.calls[0].request.url.params) - assert qs["symbol"] == "AAPL" + assert qs["market_id"] == "AAPL" assert qs["interval"] == "day" assert "endpoint: market/count" in out