From 0c40fc3c5bea8e91615c36ffeb1337784a1edada Mon Sep 17 00:00:00 2001 From: Alex Novotny Date: Mon, 17 Nov 2025 15:23:49 -0600 Subject: [PATCH 1/6] updating all refs to massive and updating benzinga to be real time --- pyproject.toml | 3 +- src/mcp_massive/__init__.py | 4 + src/mcp_massive/server.py | 194 ++++++++++++++++-------------------- tests/test_benzinga_news.py | 183 ++++++++++++++++++++++++++++++++++ uv.lock | 36 +++---- 5 files changed, 296 insertions(+), 124 deletions(-) create mode 100644 tests/test_benzinga_news.py diff --git a/pyproject.toml b/pyproject.toml index 002a13b..e9230e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "mcp[cli]>=1.15.0", - "polygon-api-client>=1.15.4", + "massive>=2.0.1", + "python-dotenv>=1.0.0", ] [[project.authors]] name = "Massive" diff --git a/src/mcp_massive/__init__.py b/src/mcp_massive/__init__.py index e828ba6..989ca37 100644 --- a/src/mcp_massive/__init__.py +++ b/src/mcp_massive/__init__.py @@ -1,7 +1,11 @@ import os from typing import Literal +from dotenv import load_dotenv from .server import run +# Load environment variables from .env file if it exists +load_dotenv() + __all__ = ["run", "main"] diff --git a/src/mcp_massive/server.py b/src/mcp_massive/server.py index 7e798c2..1de611a 100644 --- a/src/mcp_massive/server.py +++ b/src/mcp_massive/server.py @@ -2,15 +2,20 @@ from typing import Optional, Any, Dict, Union, List, Literal from mcp.server.fastmcp import FastMCP from mcp.types import ToolAnnotations -from polygon import RESTClient +from massive import RESTClient from importlib.metadata import version, PackageNotFoundError +from dotenv import load_dotenv from .formatters import json_to_csv from datetime import datetime, date +# Load environment variables from .env file if it exists +load_dotenv() + MASSIVE_API_KEY = os.environ.get("MASSIVE_API_KEY", "") if not MASSIVE_API_KEY: print("Warning: MASSIVE_API_KEY environment variable not set.") + print("Please set it in your environment or create a .env file with MASSIVE_API_KEY=your_key") version_number = "MCP-Massive/unknown" try: @@ -18,8 +23,8 @@ except PackageNotFoundError: pass -polygon_client = RESTClient(MASSIVE_API_KEY) -polygon_client.headers["User-Agent"] += f" {version_number}" +massive_client = RESTClient(MASSIVE_API_KEY) +massive_client.headers["User-Agent"] += f" {version_number}" poly_mcp = FastMCP("Massive") @@ -40,7 +45,7 @@ async def get_aggs( List aggregate bars for a ticker over a given date range in custom time window sizes. """ try: - results = polygon_client.get_aggs( + results = massive_client.get_aggs( ticker=ticker, multiplier=multiplier, timespan=timespan, @@ -75,7 +80,7 @@ async def list_aggs( Iterate through aggregate bars for a ticker over a given date range. """ try: - results = polygon_client.list_aggs( + results = massive_client.list_aggs( ticker=ticker, multiplier=multiplier, timespan=timespan, @@ -106,7 +111,7 @@ async def get_grouped_daily_aggs( Get grouped daily bars for entire market for a specific date. """ try: - results = polygon_client.get_grouped_daily_aggs( + results = massive_client.get_grouped_daily_aggs( date=date, adjusted=adjusted, include_otc=include_otc, @@ -132,7 +137,7 @@ async def get_daily_open_close_agg( Get daily open, close, high, and low for a specific ticker and date. """ try: - results = polygon_client.get_daily_open_close_agg( + results = massive_client.get_daily_open_close_agg( ticker=ticker, date=date, adjusted=adjusted, params=params, raw=True ) @@ -151,7 +156,7 @@ async def get_previous_close_agg( Get previous day's open, close, high, and low for a specific ticker. """ try: - results = polygon_client.get_previous_close_agg( + results = massive_client.get_previous_close_agg( ticker=ticker, adjusted=adjusted, params=params, raw=True ) @@ -177,7 +182,7 @@ async def list_trades( Get trades for a ticker symbol. """ try: - results = polygon_client.list_trades( + results = massive_client.list_trades( ticker=ticker, timestamp=timestamp, timestamp_lt=timestamp_lt, @@ -205,7 +210,7 @@ async def get_last_trade( Get the most recent trade for a ticker symbol. """ try: - results = polygon_client.get_last_trade(ticker=ticker, params=params, raw=True) + results = massive_client.get_last_trade(ticker=ticker, params=params, raw=True) return json_to_csv(results.data.decode("utf-8")) except Exception as e: @@ -222,7 +227,7 @@ async def get_last_crypto_trade( Get the most recent trade for a crypto pair. """ try: - results = polygon_client.get_last_crypto_trade( + results = massive_client.get_last_crypto_trade( from_=from_, to=to, params=params, raw=True ) @@ -248,7 +253,7 @@ async def list_quotes( Get quotes for a ticker symbol. """ try: - results = polygon_client.list_quotes( + results = massive_client.list_quotes( ticker=ticker, timestamp=timestamp, timestamp_lt=timestamp_lt, @@ -276,7 +281,7 @@ async def get_last_quote( Get the most recent quote for a ticker symbol. """ try: - results = polygon_client.get_last_quote(ticker=ticker, params=params, raw=True) + results = massive_client.get_last_quote(ticker=ticker, params=params, raw=True) return json_to_csv(results.data.decode("utf-8")) except Exception as e: @@ -293,7 +298,7 @@ async def get_last_forex_quote( Get the most recent forex quote. """ try: - results = polygon_client.get_last_forex_quote( + results = massive_client.get_last_forex_quote( from_=from_, to=to, params=params, raw=True ) @@ -314,7 +319,7 @@ async def get_real_time_currency_conversion( Get real-time currency conversion. """ try: - results = polygon_client.get_real_time_currency_conversion( + results = massive_client.get_real_time_currency_conversion( from_=from_, to=to, amount=amount, @@ -341,7 +346,7 @@ async def list_universal_snapshots( Get universal snapshots for multiple assets of a specific type. """ try: - results = polygon_client.list_universal_snapshots( + results = massive_client.list_universal_snapshots( type=type, ticker_any_of=ticker_any_of, order=order, @@ -367,7 +372,7 @@ async def get_snapshot_all( Get a snapshot of all tickers in a market. """ try: - results = polygon_client.get_snapshot_all( + results = massive_client.get_snapshot_all( market_type=market_type, tickers=tickers, include_otc=include_otc, @@ -391,7 +396,7 @@ async def get_snapshot_direction( Get gainers or losers for a market. """ try: - results = polygon_client.get_snapshot_direction( + results = massive_client.get_snapshot_direction( market_type=market_type, direction=direction, include_otc=include_otc, @@ -414,7 +419,7 @@ async def get_snapshot_ticker( Get snapshot for a specific ticker. """ try: - results = polygon_client.get_snapshot_ticker( + results = massive_client.get_snapshot_ticker( market_type=market_type, ticker=ticker, params=params, raw=True ) @@ -433,7 +438,7 @@ async def get_snapshot_option( Get snapshot for a specific option contract. """ try: - results = polygon_client.get_snapshot_option( + results = massive_client.get_snapshot_option( underlying_asset=underlying_asset, option_contract=option_contract, params=params, @@ -454,7 +459,7 @@ async def get_snapshot_crypto_book( Get snapshot for a crypto ticker's order book. """ try: - results = polygon_client.get_snapshot_crypto_book( + results = massive_client.get_snapshot_crypto_book( ticker=ticker, params=params, raw=True ) @@ -471,7 +476,7 @@ async def get_market_holidays( Get upcoming market holidays and their open/close times. """ try: - results = polygon_client.get_market_holidays(params=params, raw=True) + results = massive_client.get_market_holidays(params=params, raw=True) return json_to_csv(results.data.decode("utf-8")) except Exception as e: @@ -486,7 +491,7 @@ async def get_market_status( Get current trading status of exchanges and financial markets. """ try: - results = polygon_client.get_market_status(params=params, raw=True) + results = massive_client.get_market_status(params=params, raw=True) return json_to_csv(results.data.decode("utf-8")) except Exception as e: @@ -513,7 +518,7 @@ async def list_tickers( Query supported ticker symbols across stocks, indices, forex, and crypto. """ try: - results = polygon_client.list_tickers( + results = massive_client.list_tickers( ticker=ticker, type=type, market=market, @@ -545,7 +550,7 @@ async def get_ticker_details( Get detailed information about a specific ticker. """ try: - results = polygon_client.get_ticker_details( + results = massive_client.get_ticker_details( ticker=ticker, date=date, params=params, raw=True ) @@ -567,7 +572,7 @@ async def list_ticker_news( Get recent news articles for a stock ticker. """ try: - results = polygon_client.list_ticker_news( + results = massive_client.list_ticker_news( ticker=ticker, published_utc=published_utc, limit=limit, @@ -592,7 +597,7 @@ async def get_ticker_types( List all ticker types supported by Massive.com. """ try: - results = polygon_client.get_ticker_types( + results = massive_client.get_ticker_types( asset_class=asset_class, locale=locale, params=params, raw=True ) @@ -613,7 +618,7 @@ async def list_splits( Get historical stock splits. """ try: - results = polygon_client.list_splits( + results = massive_client.list_splits( ticker=ticker, execution_date=execution_date, reverse_split=reverse_split, @@ -640,7 +645,7 @@ async def list_dividends( Get historical cash dividends. """ try: - results = polygon_client.list_dividends( + results = massive_client.list_dividends( ticker=ticker, ex_dividend_date=ex_dividend_date, frequency=frequency, @@ -667,7 +672,7 @@ async def list_conditions( List conditions used by Massive.com. """ try: - results = polygon_client.list_conditions( + results = massive_client.list_conditions( asset_class=asset_class, data_type=data_type, id=id, @@ -691,7 +696,7 @@ async def get_exchanges( List exchanges known by Massive.com. """ try: - results = polygon_client.get_exchanges( + results = massive_client.get_exchanges( asset_class=asset_class, locale=locale, params=params, raw=True ) @@ -728,7 +733,7 @@ async def list_stock_financials( Get fundamental financial data for companies. """ try: - results = polygon_client.vx.list_stock_financials( + results = massive_client.vx.list_stock_financials( ticker=ticker, cik=cik, company_name=company_name, @@ -776,7 +781,7 @@ async def list_ipos( Retrieve upcoming or historical IPOs. """ try: - results = polygon_client.vx.list_ipos( + results = massive_client.vx.list_ipos( ticker=ticker, listing_date=listing_date, listing_date_lt=listing_date_lt, @@ -813,7 +818,7 @@ async def list_short_interest( Retrieve short interest data for stocks. """ try: - results = polygon_client.list_short_interest( + results = massive_client.list_short_interest( ticker=ticker, settlement_date=settlement_date, settlement_date_lt=settlement_date_lt, @@ -849,7 +854,7 @@ async def list_short_volume( Retrieve short volume data for stocks. """ try: - results = polygon_client.list_short_volume( + results = massive_client.list_short_volume( ticker=ticker, date=date, date_lt=date_lt, @@ -885,7 +890,7 @@ async def list_treasury_yields( Retrieve treasury yield data. """ try: - results = polygon_client.list_treasury_yields( + results = massive_client.list_treasury_yields( date=date, date_lt=date_lt, date_lte=date_lte, @@ -919,7 +924,7 @@ async def list_inflation( Get inflation data from the Federal Reserve. """ try: - results = polygon_client.list_inflation( + results = massive_client.list_inflation( date=date, date_any_of=date_any_of, date_gt=date_gt, @@ -989,7 +994,7 @@ async def list_benzinga_analyst_insights( List Benzinga analyst insights. """ try: - results = polygon_client.list_benzinga_analyst_insights( + results = massive_client.list_benzinga_analyst_insights( date=date, date_any_of=date_any_of, date_gt=date_gt, @@ -1077,7 +1082,7 @@ async def list_benzinga_analysts( List Benzinga analysts. """ try: - results = polygon_client.list_benzinga_analysts( + results = massive_client.list_benzinga_analysts( benzinga_id=benzinga_id, benzinga_id_any_of=benzinga_id_any_of, benzinga_id_gt=benzinga_id_gt, @@ -1128,7 +1133,7 @@ async def list_benzinga_consensus_ratings( List Benzinga consensus ratings for a ticker. """ try: - results = polygon_client.list_benzinga_consensus_ratings( + results = massive_client.list_benzinga_consensus_ratings( ticker=ticker, date=date, date_gt=date_gt, @@ -1209,7 +1214,7 @@ async def list_benzinga_earnings( List Benzinga earnings. """ try: - results = polygon_client.list_benzinga_earnings( + results = massive_client.list_benzinga_earnings( date=date, date_any_of=date_any_of, date_gt=date_gt, @@ -1291,7 +1296,7 @@ async def list_benzinga_firms( List Benzinga firms. """ try: - results = polygon_client.list_benzinga_firms( + results = massive_client.list_benzinga_firms( benzinga_id=benzinga_id, benzinga_id_any_of=benzinga_id_any_of, benzinga_id_gt=benzinga_id_gt, @@ -1361,7 +1366,7 @@ async def list_benzinga_guidance( List Benzinga guidance. """ try: - results = polygon_client.list_benzinga_guidance( + results = massive_client.list_benzinga_guidance( date=date, date_any_of=date_any_of, date_gt=date_gt, @@ -1418,71 +1423,48 @@ async def list_benzinga_guidance( @poly_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True)) async def list_benzinga_news( published: Optional[str] = None, - published_any_of: Optional[str] = None, - published_gt: Optional[str] = None, - published_gte: Optional[str] = None, - published_lt: Optional[str] = None, - published_lte: Optional[str] = None, - last_updated: Optional[str] = None, - last_updated_any_of: Optional[str] = None, - last_updated_gt: Optional[str] = None, - last_updated_gte: Optional[str] = None, - last_updated_lt: Optional[str] = None, - last_updated_lte: Optional[str] = None, - tickers: Optional[str] = None, - tickers_all_of: Optional[str] = None, - tickers_any_of: Optional[str] = None, channels: Optional[str] = None, - channels_all_of: Optional[str] = None, - channels_any_of: Optional[str] = None, tags: Optional[str] = None, - tags_all_of: Optional[str] = None, - tags_any_of: Optional[str] = None, author: Optional[str] = None, - author_any_of: Optional[str] = None, - author_gt: Optional[str] = None, - author_gte: Optional[str] = None, - author_lt: Optional[str] = None, - author_lte: Optional[str] = None, - limit: Optional[int] = 10, + stocks: Optional[str] = None, + tickers: Optional[str] = None, + limit: Optional[int] = 100, sort: Optional[str] = None, - params: Optional[Dict[str, Any]] = None, ) -> str: """ - List Benzinga news. + Retrieve real-time structured, timestamped news articles from Benzinga v2 API, including headlines, + full-text content, tickers, categories, and more. Each article entry contains metadata such as author, + publication time, and topic channels, as well as optional elements like teaser summaries, article body text, + and images. Articles can be filtered by ticker and time, and are returned in a consistent format for easy + parsing and integration. This endpoint is ideal for building alerting systems, autonomous risk analysis, + and sentiment-driven trading strategies. + + Args: + published: The timestamp (formatted as an ISO 8601 timestamp) when the news article was originally + published. Value must be an integer timestamp in seconds or formatted 'yyyy-mm-dd'. + channels: Filter for arrays that contain the value (e.g., 'News', 'Price Target'). + tags: Filter for arrays that contain the value. + author: The name of the journalist or entity that authored the news article. + stocks: Filter for arrays that contain the value. + tickers: Filter for arrays that contain the value. + limit: Limit the maximum number of results returned. Defaults to 100 if not specified. + The maximum allowed limit is 50000. + sort: A comma separated list of sort columns. For each column, append '.asc' or '.desc' to specify + the sort direction. The sort column defaults to 'published' if not specified. + The sort order defaults to 'desc' if not specified. """ try: - results = polygon_client.list_benzinga_news( + # Use the v2-specific method from the massive client library + # This calls the /benzinga/v2/news endpoint + results = massive_client.list_benzinga_news_v2( published=published, - published_any_of=published_any_of, - published_gt=published_gt, - published_gte=published_gte, - published_lt=published_lt, - published_lte=published_lte, - last_updated=last_updated, - last_updated_any_of=last_updated_any_of, - last_updated_gt=last_updated_gt, - last_updated_gte=last_updated_gte, - last_updated_lt=last_updated_lt, - last_updated_lte=last_updated_lte, - tickers=tickers, - tickers_all_of=tickers_all_of, - tickers_any_of=tickers_any_of, channels=channels, - channels_all_of=channels_all_of, - channels_any_of=channels_any_of, tags=tags, - tags_all_of=tags_all_of, - tags_any_of=tags_any_of, author=author, - author_any_of=author_any_of, - author_gt=author_gt, - author_gte=author_gte, - author_lt=author_lt, - author_lte=author_lte, + stocks=stocks, + tickers=tickers, limit=limit, sort=sort, - params=params, raw=True, ) @@ -1555,7 +1537,7 @@ async def list_benzinga_ratings( List Benzinga ratings. """ try: - results = polygon_client.list_benzinga_ratings( + results = massive_client.list_benzinga_ratings( date=date, date_any_of=date_any_of, date_gt=date_gt, @@ -1638,7 +1620,7 @@ async def list_futures_aggregates( Get aggregates for a futures contract in a given time range. """ try: - results = polygon_client.list_futures_aggregates( + results = massive_client.list_futures_aggregates( ticker=ticker, resolution=resolution, window_start=window_start, @@ -1673,7 +1655,7 @@ async def list_futures_contracts( Get a paginated list of futures contracts. """ try: - results = polygon_client.list_futures_contracts( + results = massive_client.list_futures_contracts( product_code=product_code, first_trade_date=first_trade_date, last_trade_date=last_trade_date, @@ -1701,7 +1683,7 @@ async def get_futures_contract_details( Get details for a single futures contract at a specified point in time. """ try: - results = polygon_client.get_futures_contract_details( + results = massive_client.get_futures_contract_details( ticker=ticker, as_of=as_of, params=params, @@ -1732,7 +1714,7 @@ async def list_futures_products( Get a list of futures products (including combos). """ try: - results = polygon_client.list_futures_products( + results = massive_client.list_futures_products( name=name, name_search=name_search, as_of=as_of, @@ -1764,7 +1746,7 @@ async def get_futures_product_details( Get details for a single futures product as it was at a specific day. """ try: - results = polygon_client.get_futures_product_details( + results = massive_client.get_futures_product_details( product_code=product_code, type=type, as_of=as_of, @@ -1798,7 +1780,7 @@ async def list_futures_quotes( Get quotes for a futures contract in a given time range. """ try: - results = polygon_client.list_futures_quotes( + results = massive_client.list_futures_quotes( ticker=ticker, timestamp=timestamp, timestamp_lt=timestamp_lt, @@ -1842,7 +1824,7 @@ async def list_futures_trades( Get trades for a futures contract in a given time range. """ try: - results = polygon_client.list_futures_trades( + results = massive_client.list_futures_trades( ticker=ticker, timestamp=timestamp, timestamp_lt=timestamp_lt, @@ -1877,7 +1859,7 @@ async def list_futures_schedules( Get trading schedules for multiple futures products on a specific date. """ try: - results = polygon_client.list_futures_schedules( + results = massive_client.list_futures_schedules( session_end_date=session_end_date, trading_venue=trading_venue, limit=limit, @@ -1907,7 +1889,7 @@ async def list_futures_schedules_by_product_code( Get schedule data for a single futures product across many trading dates. """ try: - results = polygon_client.list_futures_schedules_by_product_code( + results = massive_client.list_futures_schedules_by_product_code( product_code=product_code, session_end_date=session_end_date, session_end_date_lt=session_end_date_lt, @@ -1937,7 +1919,7 @@ async def list_futures_market_statuses( Get market statuses for futures products. """ try: - results = polygon_client.list_futures_market_statuses( + results = massive_client.list_futures_market_statuses( product_code_any_of=product_code_any_of, product_code=product_code, limit=limit, @@ -1973,7 +1955,7 @@ async def get_futures_snapshot( Get snapshots for futures contracts. """ try: - results = polygon_client.get_futures_snapshot( + results = massive_client.get_futures_snapshot( ticker=ticker, ticker_any_of=ticker_any_of, ticker_gt=ticker_gt, diff --git a/tests/test_benzinga_news.py b/tests/test_benzinga_news.py new file mode 100644 index 0000000..22cadfb --- /dev/null +++ b/tests/test_benzinga_news.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +""" +Test script for list_benzinga_news v2 API integration. + +This script tests the updated list_benzinga_news function to verify it works +with the v2 API endpoint. + +Usage: + # Option 1: Using .env file (recommended) + # Create a .env file with: MASSIVE_API_KEY=your_api_key_here + uv run python -m tests.test_benzinga_news + + # Option 2: Using environment variable + MASSIVE_API_KEY=your_api_key_here uv run python -m tests.test_benzinga_news +""" +import os +import asyncio +from dotenv import load_dotenv +from mcp_massive.server import list_benzinga_news + +# Load .env file if it exists +load_dotenv() + + +async def test_basic_query(): + """Test basic query with default parameters.""" + print("=" * 60) + print("Test 1: Basic query with default parameters") + print("=" * 60) + try: + result = await list_benzinga_news(limit=5) + print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows (excluding header)") + print(f"First 500 characters of result:\n{result[:500]}...") + return True + except Exception as e: + print(f"✗ Failed: {e}") + return False + + +async def test_with_ticker_filter(): + """Test query with ticker filter.""" + print("\n" + "=" * 60) + print("Test 2: Query with ticker filter (AAPL)") + print("=" * 60) + try: + result = await list_benzinga_news(tickers="AAPL", limit=3) + print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows") + print(f"First 500 characters of result:\n{result[:500]}...") + return True + except Exception as e: + print(f"✗ Failed: {e}") + return False + + +async def test_with_published_date(): + """Test query with published date filter.""" + print("\n" + "=" * 60) + print("Test 3: Query with published date filter (today)") + print("=" * 60) + from datetime import date + today = date.today().isoformat() + try: + result = await list_benzinga_news(published=today, limit=3) + row_count = len(result.splitlines()) - 1 if result.strip() else 0 + if row_count > 0: + print(f"✓ Success! Returned {row_count} rows") + print(f"First 500 characters of result:\n{result[:500]}...") + else: + print(f"⚠ No results for today ({today}). This is normal if there are no articles published today.") + print("The API call succeeded, but returned no matching articles.") + return True + except Exception as e: + print(f"✗ Failed: {e}") + return False + + +async def test_with_channels(): + """Test query with channels filter.""" + print("\n" + "=" * 60) + print("Test 4: Query with channels filter") + print("=" * 60) + try: + result = await list_benzinga_news(channels="News", limit=3) + row_count = len(result.splitlines()) - 1 if result.strip() else 0 + if row_count > 0: + print(f"✓ Success! Returned {row_count} rows") + print(f"First 500 characters of result:\n{result[:500]}...") + else: + print(f"⚠ No results for channel 'News'. This might mean:") + print(" - The channel name doesn't match exactly") + print(" - There are no articles in that channel currently") + print(" - Try checking available channels from a basic query result") + print("The API call succeeded, but returned no matching articles.") + return True + except Exception as e: + print(f"✗ Failed: {e}") + return False + + +async def test_with_sort(): + """Test query with sort parameter.""" + print("\n" + "=" * 60) + print("Test 5: Query with sort parameter") + print("=" * 60) + try: + result = await list_benzinga_news(sort="published.desc", limit=3) + print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows") + print(f"First 500 characters of result:\n{result[:500]}...") + return True + except Exception as e: + print(f"✗ Failed: {e}") + return False + + +async def test_multiple_filters(): + """Test query with multiple filters.""" + print("\n" + "=" * 60) + print("Test 6: Query with multiple filters (ticker)") + print("=" * 60) + try: + result = await list_benzinga_news( + tickers="TSLA", + limit=2 + ) + print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows") + print(f"First 500 characters of result:\n{result[:500]}...") + return True + except Exception as e: + print(f"✗ Failed: {e}") + return False + + +async def main(): + """Run all tests.""" + # Check for API key + api_key = os.environ.get("MASSIVE_API_KEY") + if not api_key: + print("ERROR: MASSIVE_API_KEY environment variable not set!") + print("Please set it before running tests:") + print(" Option 1: Create a .env file with: MASSIVE_API_KEY=your_api_key_here") + print(" Option 2: Export it: export MASSIVE_API_KEY=your_api_key_here") + return + + print("Testing list_benzinga_news v2 API integration") + print(f"API Key: {api_key[:10]}...{api_key[-4:] if len(api_key) > 14 else '****'}") + print() + + tests = [ + test_basic_query, + test_with_ticker_filter, + test_with_published_date, + test_with_channels, + test_with_sort, + test_multiple_filters, + ] + + results = [] + for test in tests: + result = await test() + results.append(result) + # Small delay between tests to avoid rate limiting + await asyncio.sleep(0.5) + + # Summary + print("\n" + "=" * 60) + print("Test Summary") + print("=" * 60) + passed = sum(results) + total = len(results) + print(f"Passed: {passed}/{total}") + + if passed == total: + print("✓ All tests passed!") + else: + print(f"✗ {total - passed} test(s) failed") + + return passed == total + + +if __name__ == "__main__": + success = asyncio.run(main()) + exit(0 if success else 1) + diff --git a/uv.lock b/uv.lock index 4d8a107..2463ee2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -180,6 +180,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[[package]] +name = "massive" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/ee/49823c94a368173fd6b7d592b8e5e9be3a9df403cdbdada57af4d2ff6453/massive-2.0.1.tar.gz", hash = "sha256:419cd341570435b0998122919edc34cc7031f9d7a9c9ae128e1f228012160d81", size = 44064, upload-time = "2025-10-30T22:44:13.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/3e/2b02f58960029301a7b5969931efece0f0532d6efa66afcc92a4d801ba3b/massive-2.0.1-py3-none-any.whl", hash = "sha256:666d14f4401b30d7c25d8360817ef040c1a616df109775f8662f3e6078375e76", size = 61893, upload-time = "2025-10-30T22:44:12.476Z" }, +] + [[package]] name = "mcp" version = "1.15.0" @@ -213,8 +227,9 @@ name = "mcp-massive" version = "0.6.0" source = { editable = "." } dependencies = [ + { name = "massive" }, { name = "mcp", extra = ["cli"] }, - { name = "polygon-api-client" }, + { name = "python-dotenv" }, ] [package.dev-dependencies] @@ -226,8 +241,9 @@ dev = [ [package.metadata] requires-dist = [ + { name = "massive", specifier = ">=2.0.1" }, { name = "mcp", extras = ["cli"], specifier = ">=1.15.0" }, - { name = "polygon-api-client", specifier = ">=1.15.4" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, ] [package.metadata.requires-dev] @@ -264,20 +280,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "polygon-api-client" -version = "1.15.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2c/39/8f2423b65926d7f815370e83acf9b8c09d0965c7cb5b55a78eee52e679f1/polygon_api_client-1.15.4.tar.gz", hash = "sha256:2adb920dac5788732fc95a25dc8650cc843591552c8969c40f5eaaf22aaf7072", size = 37986, upload-time = "2025-09-23T23:25:14.842Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/b9/57a53ce17aeb46432b5df3e3ec607d9a0daece740d707798f4f7c423d5b5/polygon_api_client-1.15.4-py3-none-any.whl", hash = "sha256:561b990add5c9a251f42ba1ec85244829a15c09b02f7b08dbf149df8bb509cde", size = 54598, upload-time = "2025-09-23T23:25:13.837Z" }, -] - [[package]] name = "pydantic" version = "2.11.9" From 8e77c0e152501a73095ca4d42a52bdb0c2fb49e8 Mon Sep 17 00:00:00 2001 From: Alex Novotny Date: Tue, 18 Nov 2025 08:40:20 -0600 Subject: [PATCH 2/6] remove temp test class --- tests/test_benzinga_news.py | 183 ------------------------------------ 1 file changed, 183 deletions(-) delete mode 100644 tests/test_benzinga_news.py diff --git a/tests/test_benzinga_news.py b/tests/test_benzinga_news.py deleted file mode 100644 index 22cadfb..0000000 --- a/tests/test_benzinga_news.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python -""" -Test script for list_benzinga_news v2 API integration. - -This script tests the updated list_benzinga_news function to verify it works -with the v2 API endpoint. - -Usage: - # Option 1: Using .env file (recommended) - # Create a .env file with: MASSIVE_API_KEY=your_api_key_here - uv run python -m tests.test_benzinga_news - - # Option 2: Using environment variable - MASSIVE_API_KEY=your_api_key_here uv run python -m tests.test_benzinga_news -""" -import os -import asyncio -from dotenv import load_dotenv -from mcp_massive.server import list_benzinga_news - -# Load .env file if it exists -load_dotenv() - - -async def test_basic_query(): - """Test basic query with default parameters.""" - print("=" * 60) - print("Test 1: Basic query with default parameters") - print("=" * 60) - try: - result = await list_benzinga_news(limit=5) - print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows (excluding header)") - print(f"First 500 characters of result:\n{result[:500]}...") - return True - except Exception as e: - print(f"✗ Failed: {e}") - return False - - -async def test_with_ticker_filter(): - """Test query with ticker filter.""" - print("\n" + "=" * 60) - print("Test 2: Query with ticker filter (AAPL)") - print("=" * 60) - try: - result = await list_benzinga_news(tickers="AAPL", limit=3) - print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows") - print(f"First 500 characters of result:\n{result[:500]}...") - return True - except Exception as e: - print(f"✗ Failed: {e}") - return False - - -async def test_with_published_date(): - """Test query with published date filter.""" - print("\n" + "=" * 60) - print("Test 3: Query with published date filter (today)") - print("=" * 60) - from datetime import date - today = date.today().isoformat() - try: - result = await list_benzinga_news(published=today, limit=3) - row_count = len(result.splitlines()) - 1 if result.strip() else 0 - if row_count > 0: - print(f"✓ Success! Returned {row_count} rows") - print(f"First 500 characters of result:\n{result[:500]}...") - else: - print(f"⚠ No results for today ({today}). This is normal if there are no articles published today.") - print("The API call succeeded, but returned no matching articles.") - return True - except Exception as e: - print(f"✗ Failed: {e}") - return False - - -async def test_with_channels(): - """Test query with channels filter.""" - print("\n" + "=" * 60) - print("Test 4: Query with channels filter") - print("=" * 60) - try: - result = await list_benzinga_news(channels="News", limit=3) - row_count = len(result.splitlines()) - 1 if result.strip() else 0 - if row_count > 0: - print(f"✓ Success! Returned {row_count} rows") - print(f"First 500 characters of result:\n{result[:500]}...") - else: - print(f"⚠ No results for channel 'News'. This might mean:") - print(" - The channel name doesn't match exactly") - print(" - There are no articles in that channel currently") - print(" - Try checking available channels from a basic query result") - print("The API call succeeded, but returned no matching articles.") - return True - except Exception as e: - print(f"✗ Failed: {e}") - return False - - -async def test_with_sort(): - """Test query with sort parameter.""" - print("\n" + "=" * 60) - print("Test 5: Query with sort parameter") - print("=" * 60) - try: - result = await list_benzinga_news(sort="published.desc", limit=3) - print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows") - print(f"First 500 characters of result:\n{result[:500]}...") - return True - except Exception as e: - print(f"✗ Failed: {e}") - return False - - -async def test_multiple_filters(): - """Test query with multiple filters.""" - print("\n" + "=" * 60) - print("Test 6: Query with multiple filters (ticker)") - print("=" * 60) - try: - result = await list_benzinga_news( - tickers="TSLA", - limit=2 - ) - print(f"✓ Success! Returned {len(result.splitlines()) - 1} rows") - print(f"First 500 characters of result:\n{result[:500]}...") - return True - except Exception as e: - print(f"✗ Failed: {e}") - return False - - -async def main(): - """Run all tests.""" - # Check for API key - api_key = os.environ.get("MASSIVE_API_KEY") - if not api_key: - print("ERROR: MASSIVE_API_KEY environment variable not set!") - print("Please set it before running tests:") - print(" Option 1: Create a .env file with: MASSIVE_API_KEY=your_api_key_here") - print(" Option 2: Export it: export MASSIVE_API_KEY=your_api_key_here") - return - - print("Testing list_benzinga_news v2 API integration") - print(f"API Key: {api_key[:10]}...{api_key[-4:] if len(api_key) > 14 else '****'}") - print() - - tests = [ - test_basic_query, - test_with_ticker_filter, - test_with_published_date, - test_with_channels, - test_with_sort, - test_multiple_filters, - ] - - results = [] - for test in tests: - result = await test() - results.append(result) - # Small delay between tests to avoid rate limiting - await asyncio.sleep(0.5) - - # Summary - print("\n" + "=" * 60) - print("Test Summary") - print("=" * 60) - passed = sum(results) - total = len(results) - print(f"Passed: {passed}/{total}") - - if passed == total: - print("✓ All tests passed!") - else: - print(f"✗ {total - passed} test(s) failed") - - return passed == total - - -if __name__ == "__main__": - success = asyncio.run(main()) - exit(0 if success else 1) - From 5fe2481bf437c30f70794928f821dd8aa734c127 Mon Sep 17 00:00:00 2001 From: Alex Novotny Date: Tue, 18 Nov 2025 10:04:46 -0600 Subject: [PATCH 3/6] updating the get last trade tool --- src/mcp_massive/formatters.py | 37 ++++++++++++++++++++++++++++------- src/mcp_massive/server.py | 4 +--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/mcp_massive/formatters.py b/src/mcp_massive/formatters.py index bb8f49f..55fe5f3 100644 --- a/src/mcp_massive/formatters.py +++ b/src/mcp_massive/formatters.py @@ -18,18 +18,40 @@ def json_to_csv(json_input: str | dict) -> str: """ # Parse JSON if it's a string if isinstance(json_input, str): - data = json.loads(json_input) + try: + data = json.loads(json_input) + except json.JSONDecodeError: + # If JSON parsing fails, return empty CSV + return "" else: data = json_input if isinstance(data, dict) and "results" in data: - records = data["results"] + results_value = data["results"] + # Handle both list and single object responses + if isinstance(results_value, list): + records = results_value + elif isinstance(results_value, dict): + # Single object response (e.g., get_last_trade returns results as object) + records = [results_value] + else: + records = [results_value] + elif isinstance(data, dict) and "last" in data: + # Handle responses with "last" key (e.g., get_last_trade, get_last_quote) + records = [data["last"]] if isinstance(data["last"], dict) else [data] elif isinstance(data, list): records = data else: records = [data] - flattened_records = [_flatten_dict(record) for record in records] + # Only flatten dict records, skip non-dict items + flattened_records = [] + for record in records: + if isinstance(record, dict): + flattened_records.append(_flatten_dict(record)) + else: + # If it's not a dict, wrap it in a dict with a 'value' key + flattened_records.append({"value": str(record)}) if not flattened_records: return "" @@ -38,10 +60,11 @@ def json_to_csv(json_input: str | dict) -> str: all_keys = [] seen = set() for record in flattened_records: - for key in record.keys(): - if key not in seen: - all_keys.append(key) - seen.add(key) + if isinstance(record, dict): + for key in record.keys(): + if key not in seen: + all_keys.append(key) + seen.add(key) output = io.StringIO() writer = csv.DictWriter(output, fieldnames=all_keys, lineterminator="\n") diff --git a/src/mcp_massive/server.py b/src/mcp_massive/server.py index 1de611a..6ec2528 100644 --- a/src/mcp_massive/server.py +++ b/src/mcp_massive/server.py @@ -204,14 +204,12 @@ async def list_trades( @poly_mcp.tool(annotations=ToolAnnotations(readOnlyHint=True)) async def get_last_trade( ticker: str, - params: Optional[Dict[str, Any]] = None, ) -> str: """ Get the most recent trade for a ticker symbol. """ try: - results = massive_client.get_last_trade(ticker=ticker, params=params, raw=True) - + results = massive_client.get_last_trade(ticker=ticker, raw=True) return json_to_csv(results.data.decode("utf-8")) except Exception as e: return f"Error: {e}" From f1ec980d228776bfbac940b08c8625a7244cdf6b Mon Sep 17 00:00:00 2001 From: Alex Novotny Date: Tue, 18 Nov 2025 10:06:51 -0600 Subject: [PATCH 4/6] updating test --- tests/test_formatters.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index d8885e9..5013d24 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -372,9 +372,10 @@ def test_list_with_nested_objects(self): assert "tag2" in rows[0]["tags"] def test_invalid_json_string(self): - """Test that invalid JSON string raises appropriate error.""" - with pytest.raises(json.JSONDecodeError): - json_to_csv("not valid json {") + """Test that invalid JSON string returns empty CSV gracefully.""" + # Invalid JSON should return empty string instead of raising + result = json_to_csv("not valid json {") + assert result == "" def test_csv_output_format(self): """Test that output is valid CSV with proper headers.""" From bf52bd02cabe2447ddd4152e508dcc9105c3036a Mon Sep 17 00:00:00 2001 From: Alex Novotny Date: Tue, 18 Nov 2025 11:45:12 -0600 Subject: [PATCH 5/6] update dotenv vs --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e9230e8..e23d72e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.10" dependencies = [ "mcp[cli]>=1.15.0", "massive>=2.0.1", - "python-dotenv>=1.0.0", + "python-dotenv>=1.2.0", ] [[project.authors]] name = "Massive" diff --git a/uv.lock b/uv.lock index 2463ee2..0915212 100644 --- a/uv.lock +++ b/uv.lock @@ -243,7 +243,7 @@ dev = [ requires-dist = [ { name = "massive", specifier = ">=2.0.1" }, { name = "mcp", extras = ["cli"], specifier = ">=1.15.0" }, - { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "python-dotenv", specifier = ">=1.2.0" }, ] [package.metadata.requires-dev] @@ -424,11 +424,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] From 3ba904c91b22f9da4cb8cfbab3f85a79477f7f21 Mon Sep 17 00:00:00 2001 From: Alex Novotny Date: Tue, 18 Nov 2025 11:50:21 -0600 Subject: [PATCH 6/6] update version to 0.7.0 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e23d72e..a043d74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp_massive" -version = "0.6.0" +version = "0.7.0" description = "A MCP server project" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index 0915212..f83188d 100644 --- a/uv.lock +++ b/uv.lock @@ -224,7 +224,7 @@ cli = [ [[package]] name = "mcp-massive" -version = "0.6.0" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "massive" },