Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ get_crypto_news(

```text
get_market_news(
symbol=["AAPL", "NVDA"],
market_id=["AAPL", "NVDA"],
country="us"
)
```
Expand All @@ -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"
)
```
Expand Down
2 changes: 1 addition & 1 deletion src/newsdata_mcp/_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/newsdata_mcp/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
6 changes: 3 additions & 3 deletions src/newsdata_mcp/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
),
Expand Down
16 changes: 8 additions & 8 deletions src/newsdata_mcp/tools/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
EXCLUDE_FIELD_FILTER,
FLAG,
LANGUAGE_FILTER,
MARKET_ID_FILTER,
ORGANIZATION_FILTER,
PAGE,
PRIORITY_DOMAIN,
Expand All @@ -21,7 +22,6 @@
SENTIMENT_SCORE,
SIZE,
SORT,
SYMBOL_FILTER,
TAG_FILTER,
TIMEFRAME,
TIMEZONE,
Expand Down Expand Up @@ -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,
Expand All @@ -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).
Expand All @@ -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())
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions src/newsdata_mcp/tools/market_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FLAG,
INTERVAL,
LANGUAGE_FILTER,
MARKET_ID_FILTER,
ORGANIZATION_FILTER,
PAGE,
PRIORITY_DOMAIN,
Expand All @@ -20,7 +21,6 @@
SENTIMENT_SCORE,
SIZE,
SORT,
SYMBOL_FILTER,
TAG_FILTER,
)
from ..validators import (
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
4 changes: 2 additions & 2 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand All @@ -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")
22 changes: 11 additions & 11 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading