# Directory Structure ``` ├── .dockerignore ├── .gitattributes ├── .github │ ├── dependabot.yaml │ └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── Dockerfile ├── LICENSE ├── pyproject.toml ├── README.md ├── src │ └── mcp_yahoo_finance │ ├── __init__.py │ ├── __main__.py │ ├── py.typed │ ├── server.py │ └── utils.py ├── tests │ └── test_server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.10 2 | ``` -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` 1 | Dockerfile linguist-generated=true 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | 12 | .env 13 | Makefile 14 | ``` -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | rev: v0.11.5 13 | hooks: 14 | - id: ruff 15 | args: [ --fix, --select, I ] 16 | - id: ruff-format 17 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | # Git 2 | .git 3 | .gitignore 4 | .gitattributes 5 | 6 | 7 | # CI 8 | .codeclimate.yml 9 | .travis.yml 10 | .taskcluster.yml 11 | .pre-commit-config.yaml 12 | 13 | # Docker 14 | docker-compose.yml 15 | Dockerfile 16 | .docker 17 | .dockerignore 18 | 19 | # Byte-compiled / optimized / DLL files 20 | **/__pycache__/ 21 | **/*.py[cod] 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | env/ 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .coverage 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | .pytest_cache/ 61 | tests/ 62 | 63 | # Linting 64 | .ruff_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Virtual environment 80 | .env 81 | .venv/ 82 | venv/ 83 | 84 | # PyCharm 85 | .idea 86 | 87 | # Python mode for VIM 88 | .ropeproject 89 | **/.ropeproject 90 | 91 | # Vim swap files 92 | **/*.swp 93 | 94 | # VS Code 95 | .vscode/ 96 | 97 | # Other 98 | Makefile 99 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Yahoo Finance 2 | 3 |  4 |  5 |  6 | 7 | 8 | A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for Yahoo Finance interaction. This server provides tools to get pricing, company information and more. 9 | 10 | > Please note that `mcp-yahoo-finance` is currently in early development. The functionality and available tools are subject to change and expansion as I continue to develop and improve the server. 11 | 12 | ## Installation 13 | 14 | You don't need to manually install `mcp-yahoo-finance` if you use [`uv`](https://docs.astral.sh/uv/). We'll use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run `mcp-yahoo-finance`. 15 | 16 | I would recommend using this method if you simply want to use the MCP server. 17 | 18 | ### Using pip 19 | 20 | Using `pip`. 21 | 22 | ```sh 23 | pip install mcp-yahoo-finance 24 | ``` 25 | 26 | ### Using Git 27 | 28 | You can also install the package after cloning the repository to your machine. 29 | 30 | ```sh 31 | git clone [email protected]:maxscheijen/mcp-yahoo-finance.git 32 | cd mcp-yahoo-finance 33 | uv sync 34 | ``` 35 | 36 | ## Configuration 37 | 38 | ### Claude Desktop 39 | 40 | Add this to your `claude_desktop_config.json`: 41 | 42 | ```json 43 | { 44 | "mcpServers": { 45 | "yahoo-finance": { 46 | "command": "uvx", 47 | "args": ["mcp-yahoo-finance"] 48 | } 49 | } 50 | } 51 | ``` 52 | You can also use docker: 53 | 54 | ```json 55 | { 56 | "mcpServers": { 57 | "yahoo-finance": { 58 | "command": "docker", 59 | "args": ["run", "-i", "--rm", "IMAGE"] 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | ### VSCode 66 | 67 | Add this to your `.vscode/mcp.json`: 68 | 69 | ```json 70 | { 71 | "servers": { 72 | "yahoo-finance": { 73 | "command": "uvx", 74 | "args": ["mcp-yahoo-finance"] 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | ## Examples of Questions 81 | 82 | 1. "What is the stock price of Apple?" 83 | 2. "What is the difference in stock price between Apple and Google?" 84 | 3. "How much did the stock price of Apple change between 2024-01-01 and 2025-01-01?" 85 | 4. "What are the available options expiration dates for AAPL?" 86 | 5. "Show me the options chain for AAPL expiring on 2024-01-19" 87 | 6. "What are the call and put options for Tesla?" 88 | 89 | ## Build 90 | 91 | Docker: 92 | 93 | ```sh 94 | docker build -t [IMAGE] . 95 | ``` 96 | 97 | ## Test with MCP Inspector 98 | 99 | ```sh 100 | npx @modelcontextprotocol/inspector uv run mcp-yahoo-finance 101 | ``` 102 | ``` -------------------------------------------------------------------------------- /src/mcp_yahoo_finance/__main__.py: -------------------------------------------------------------------------------- ```python 1 | from mcp_yahoo_finance import main 2 | 3 | main() 4 | ``` -------------------------------------------------------------------------------- /src/mcp_yahoo_finance/__init__.py: -------------------------------------------------------------------------------- ```python 1 | from mcp_yahoo_finance import server 2 | 3 | 4 | def main(): 5 | import asyncio 6 | 7 | asyncio.run(server.serve()) 8 | 9 | 10 | if __name__ == "__main__": 11 | main() 12 | ``` -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- ```yaml 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | versioning-strategy: "increase" 9 | ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - "3.10" 18 | - "3.11" 19 | - "3.12" 20 | - "3.13" 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Install uv and set the python version 26 | uses: astral-sh/setup-uv@v6 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install Dependencies 31 | run: | 32 | uv sync --locked --all-extras 33 | 34 | - name: Lint & Format with ruff 35 | run: | 36 | uv run ruff check . 37 | uv run ruff format --check 38 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Use a Python image with uv pre-installed 2 | FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder 3 | 4 | # Install the project into `/app` 5 | WORKDIR /app 6 | 7 | # Enable bytecode compilation 8 | ENV UV_COMPILE_BYTECODE=1 9 | 10 | # Copy from the cache instead of linking since it's a mounted volume 11 | ENV UV_LINK_MODE=copy 12 | 13 | # Install the project's dependencies using the lockfile and settings 14 | RUN --mount=type=cache,target=/root/.cache/uv \ 15 | --mount=type=bind,source=uv.lock,target=uv.lock \ 16 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 17 | uv sync --frozen --no-install-project --no-dev --no-editable 18 | 19 | # Then, add the rest of the project source code and install it 20 | # Installing separately from its dependencies allows optimal layer caching 21 | ADD . /app 22 | RUN --mount=type=cache,target=/root/.cache/uv \ 23 | uv sync --frozen --no-dev --no-editable 24 | 25 | # Place executables in the environment at the front of the path 26 | ENV PATH="/app/.venv/bin:$PATH" 27 | 28 | # Set entry point 29 | ENTRYPOINT ["mcp-yahoo-finance"] 30 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-yahoo-finance" 3 | version = "0.1.3" 4 | description = "A Model Context Protol (MCP) server providing tools to interact with Yahoo Finance for LLMs" 5 | readme = {file = "README.md", content-type = "text/markdown"} 6 | authors = [ 7 | { name = "Max Scheijen", email = "[email protected]" } 8 | ] 9 | maintainers = [ 10 | {name = "Max Scheijen", email = "[email protected]"} 11 | ] 12 | requires-python = ">=3.10" 13 | keywords = ["finance", "yahoo finance", "mcp", "llm", "automation"] 14 | license = { text = "MIT" } 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: MIT License", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | ] 25 | dependencies = [ 26 | "mcp>=1.6.0", 27 | "yfinance>=0.2.55", 28 | ] 29 | 30 | [project.scripts] 31 | mcp-yahoo-finance = "mcp_yahoo_finance:main" 32 | 33 | [project.urls] 34 | Homepage = "https://github.com/maxscheijen/mcp-yahoo-finance" 35 | Documentation = "https://github.com/maxscheijen/mcp-yahoo-finance" 36 | Repository = "https://github.com/maxscheijen/mcp-yahoo-finance.git" 37 | Issues = "https://github.com/maxscheijen/mcp-yahoo-finance/issues" 38 | 39 | [build-system] 40 | requires = ["hatchling"] 41 | build-backend = "hatchling.build" 42 | 43 | [dependency-groups] 44 | dev = [ 45 | "pre-commit>=4.2.0", 46 | "pytest-asyncio>=0.26.0", 47 | "pytest>=8.3.5", 48 | "ruff>=0.11.5", 49 | ] 50 | ``` -------------------------------------------------------------------------------- /src/mcp_yahoo_finance/utils.py: -------------------------------------------------------------------------------- ```python 1 | import inspect 2 | from typing import Any 3 | 4 | from mcp.types import Tool 5 | 6 | 7 | def parse_docstring(docstring: str) -> dict[str, str]: 8 | """Parses a Google-style docstring to extract parameter descriptions.""" 9 | descriptions = {} 10 | if not docstring: 11 | return descriptions 12 | 13 | lines = docstring.split("\n") 14 | current_param = None 15 | 16 | for line in lines: 17 | line = line.strip() 18 | if line.startswith("Args:"): 19 | continue 20 | elif line and "(" in line and ")" in line and ":" in line: 21 | param = line.split("(")[0].strip() 22 | desc = line.split("):")[1].strip() 23 | descriptions[param] = desc 24 | current_param = param 25 | elif current_param and line: 26 | descriptions[current_param] += " " + line.strip() 27 | 28 | return descriptions 29 | 30 | 31 | def generate_tool(func: Any) -> Tool: 32 | """Generates a tool schema from a Python function.""" 33 | signature = inspect.signature(func) 34 | docstring = inspect.getdoc(func) or "" 35 | param_descriptions = parse_docstring(docstring) 36 | 37 | schema = { 38 | "name": func.__name__, 39 | "description": docstring.split("Args:")[0].strip(), 40 | "inputSchema": { 41 | "type": "object", 42 | "properties": {}, 43 | }, 44 | } 45 | 46 | for param_name, param in signature.parameters.items(): 47 | param_type = ( 48 | "number" 49 | if param.annotation is float 50 | else "string" 51 | if param.annotation is str 52 | else "string" 53 | ) 54 | schema["inputSchema"]["properties"][param_name] = { 55 | "type": param_type, 56 | "description": param_descriptions.get(param_name, ""), 57 | } 58 | 59 | if "required" not in schema["inputSchema"]: 60 | schema["inputSchema"]["required"] = [param_name] 61 | else: 62 | if "=" not in str(param): 63 | schema["inputSchema"]["required"].append(param_name) 64 | 65 | return Tool(**schema) 66 | ``` -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- ```python 1 | import asyncio 2 | import json 3 | 4 | import pytest 5 | from mcp import ClientSession 6 | from mcp.client.stdio import StdioServerParameters, stdio_client 7 | from mcp.types import TextContent, Tool 8 | 9 | 10 | @pytest.fixture 11 | def server_params(): 12 | return StdioServerParameters(command="mcp-yahoo-finance") 13 | 14 | 15 | @pytest.fixture 16 | def client_tools() -> list[Tool]: 17 | server_params = StdioServerParameters(command="mcp-yahoo-finance") 18 | 19 | async def _get_tools(): 20 | async with ( 21 | stdio_client(server_params) as (read, write), 22 | ClientSession(read, write) as session, 23 | ): 24 | await session.initialize() 25 | tool_list_result = await session.list_tools() 26 | return tool_list_result.tools 27 | 28 | return asyncio.run(_get_tools()) 29 | 30 | 31 | @pytest.mark.asyncio 32 | @pytest.mark.parametrize( 33 | "tool_name", 34 | [ 35 | "get_current_stock_price", 36 | "get_stock_price_by_date", 37 | "get_stock_price_date_range", 38 | "get_historical_stock_prices", 39 | "get_dividends", 40 | "get_income_statement", 41 | "get_cashflow", 42 | "get_earning_dates", 43 | "get_news", 44 | "get_recommendations", 45 | "get_option_expiration_dates", 46 | "get_option_chain", 47 | ], 48 | ) 49 | async def test_list_tools(client_tools: list[Tool], tool_name) -> None: 50 | tool_names = [tool.name for tool in client_tools] 51 | assert tool_name in tool_names 52 | 53 | 54 | @pytest.mark.asyncio 55 | @pytest.mark.parametrize( 56 | "symbol, date, expected_price", 57 | [ 58 | ("AAPL", "2025-01-01", 243.5822), 59 | ("GOOG", "2025-02-01", 202.4094), 60 | ("META", "2025-02-01", 696.8401), 61 | ], 62 | ) 63 | async def test_get_stock_price_by_date(server_params, symbol, date, expected_price): 64 | async with ( 65 | stdio_client(server_params) as (read, write), 66 | ClientSession(read, write) as session, 67 | ): 68 | await session.initialize() 69 | tool_result = await session.call_tool( 70 | "get_stock_price_by_date", {"symbol": symbol, "date": date} 71 | ) 72 | 73 | assert len(tool_result.content) == 1 74 | assert isinstance(tool_result.content[0], TextContent) 75 | 76 | data = json.loads(tool_result.content[0].text) 77 | 78 | assert isinstance(data, float) 79 | assert data == expected_price 80 | ``` -------------------------------------------------------------------------------- /src/mcp_yahoo_finance/server.py: -------------------------------------------------------------------------------- ```python 1 | import json 2 | from typing import Any, Literal 3 | 4 | import pandas as pd 5 | from mcp.server import Server 6 | from mcp.server.stdio import stdio_server 7 | from mcp.types import TextContent, Tool 8 | from requests import Session 9 | from yfinance import Ticker 10 | 11 | from mcp_yahoo_finance.utils import generate_tool 12 | 13 | 14 | class YahooFinance: 15 | def __init__(self, session: Session | None = None, verify: bool = True) -> None: 16 | self.session = session 17 | 18 | if self.session: 19 | self.session.verify = verify 20 | 21 | def get_current_stock_price(self, symbol: str) -> str: 22 | """Get the current stock price based on stock symbol. 23 | 24 | Args: 25 | symbol (str): Stock symbol in Yahoo Finance format. 26 | """ 27 | stock = Ticker(ticker=symbol, session=self.session).info 28 | current_price = stock.get( 29 | "regularMarketPrice", stock.get("currentPrice", "N/A") 30 | ) 31 | return ( 32 | f"{current_price:.4f}" 33 | if current_price 34 | else f"Couldn't fetch {symbol} current price" 35 | ) 36 | 37 | def get_stock_price_by_date(self, symbol: str, date: str) -> str: 38 | """Get the stock price for a given stock symbol on a specific date. 39 | 40 | Args: 41 | symbol (str): Stock symbol in Yahoo Finance format. 42 | date (str): The date in YYYY-MM-DD format. 43 | """ 44 | stock = Ticker(ticker=symbol, session=self.session) 45 | price = stock.history(start=date, period="1d") 46 | return f"{price.iloc[0]['Close']:.4f}" 47 | 48 | def get_stock_price_date_range( 49 | self, symbol: str, start_date: str, end_date: str 50 | ) -> str: 51 | """Get the stock prices for a given date range for a given stock symbol. 52 | 53 | Args: 54 | symbol (str): Stock symbol in Yahoo Finance format. 55 | start_date (str): The start date in YYYY-MM-DD format. 56 | end_date (str): The end date in YYYY-MM-DD format. 57 | """ 58 | stock = Ticker(ticker=symbol, session=self.session) 59 | prices = stock.history(start=start_date, end=end_date) 60 | prices.index = prices.index.astype(str) 61 | return f"{prices['Close'].to_json(orient='index')}" 62 | 63 | def get_historical_stock_prices( 64 | self, 65 | symbol: str, 66 | period: Literal[ 67 | "1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max" 68 | ] = "1mo", 69 | interval: Literal["1d", "5d", "1wk", "1mo", "3mo"] = "1d", 70 | ) -> str: 71 | """Get historical stock prices for a given stock symbol. 72 | 73 | Args: 74 | symbol (str): Stock symbol in Yahoo Finance format. 75 | period (str): The period for historical data. Defaults to "1mo". 76 | Valid periods: "1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max" 77 | interval (str): The interval beween data points. Defaults to "1d". 78 | Valid intervals: "1d", "5d", "1wk", "1mo", "3mo" 79 | """ 80 | stock = Ticker(ticker=symbol, session=self.session) 81 | prices = stock.history(period=period, interval=interval) 82 | 83 | if hasattr(prices.index, "date"): 84 | prices.index = prices.index.date.astype(str) # type: ignore 85 | return f"{prices['Close'].to_json(orient='index')}" 86 | 87 | def get_dividends(self, symbol: str) -> str: 88 | """Get dividends for a given stock symbol. 89 | 90 | Args: 91 | symbol (str): Stock symbol in Yahoo Finance format. 92 | """ 93 | stock = Ticker(ticker=symbol, session=self.session) 94 | dividends = stock.dividends 95 | 96 | if hasattr(dividends.index, "date"): 97 | dividends.index = dividends.index.date.astype(str) # type: ignore 98 | return f"{dividends.to_json(orient='index')}" 99 | 100 | def get_income_statement( 101 | self, symbol: str, freq: Literal["yearly", "quarterly", "trainling"] = "yearly" 102 | ) -> str: 103 | """Get income statement for a given stock symbol. 104 | 105 | Args: 106 | symbol (str): Stock symbol in Yahoo Finance format. 107 | freq (str): At what frequency to get cashflow statements. Defaults to "yearly". 108 | Valid freqencies: "yearly", "quarterly", "trainling" 109 | """ 110 | stock = Ticker(ticker=symbol, session=self.session) 111 | income_statement = stock.get_income_stmt(freq=freq, pretty=True) 112 | 113 | if isinstance(income_statement, pd.DataFrame): 114 | income_statement.columns = [ 115 | str(col.date()) for col in income_statement.columns 116 | ] 117 | return f"{income_statement.to_json()}" 118 | return f"{income_statement}" 119 | 120 | def get_cashflow( 121 | self, symbol: str, freq: Literal["yearly", "quarterly", "trainling"] = "yearly" 122 | ): 123 | """Get cashflow for a given stock symbol. 124 | 125 | Args: 126 | symbol (str): Stock symbol in Yahoo Finance format. 127 | freq (str): At what frequency to get cashflow statements. Defaults to "yearly". 128 | Valid freqencies: "yearly", "quarterly", "trainling" 129 | """ 130 | stock = Ticker(ticker=symbol, session=self.session) 131 | cashflow = stock.get_cashflow(freq=freq, pretty=True) 132 | 133 | if isinstance(cashflow, pd.DataFrame): 134 | cashflow.columns = [str(col.date()) for col in cashflow.columns] 135 | return f"{cashflow.to_json(indent=2)}" 136 | return f"{cashflow}" 137 | 138 | def get_earning_dates(self, symbol: str, limit: int = 12) -> str: 139 | """Get earning dates. 140 | 141 | 142 | Args: 143 | symbol (str): Stock symbol in Yahoo Finance format. 144 | limit (int): max amount of upcoming and recent earnings dates to return. Default value 12 should return next 4 quarters and last 8 quarters. Increase if more history is needed. 145 | """ 146 | 147 | stock = Ticker(ticker=symbol, session=self.session) 148 | earning_dates = stock.get_earnings_dates(limit=limit) 149 | 150 | if isinstance(earning_dates, pd.DataFrame): 151 | earning_dates.index = earning_dates.index.date.astype(str) # type: ignore 152 | return f"{earning_dates.to_json(indent=2)}" 153 | return f"{earning_dates}" 154 | 155 | def get_news(self, symbol: str) -> str: 156 | """Get news for a given stock symbol. 157 | 158 | Args: 159 | symbol (str): Stock symbol in Yahoo Finance format. 160 | """ 161 | stock = Ticker(ticker=symbol, session=self.session) 162 | return json.dumps(stock.news, indent=2) 163 | 164 | def get_recommendations(self, symbol: str) -> str: 165 | """Get analyst recommendations for a given symbol. 166 | 167 | Args: 168 | symbol (str): Stock symbol in Yahoo Finance format. 169 | """ 170 | stock = Ticker(ticker=symbol, session=self.session) 171 | recommendations = stock.get_recommendations() 172 | print(recommendations) 173 | if isinstance(recommendations, pd.DataFrame): 174 | return f"{recommendations.to_json(orient='records', indent=2)}" 175 | return f"{recommendations}" 176 | 177 | def get_option_expiration_dates(self, symbol: str) -> str: 178 | """Get available options expiration dates for a given stock symbol. 179 | 180 | Args: 181 | symbol (str): Stock symbol in Yahoo Finance format. 182 | """ 183 | stock = Ticker(ticker=symbol, session=self.session) 184 | expiration_dates = stock.options 185 | return json.dumps(list(expiration_dates), indent=2) 186 | 187 | def get_option_chain(self, symbol: str, expiration_date: str) -> str: 188 | """Get options chain for a specific expiration date. 189 | 190 | Args: 191 | symbol (str): Stock symbol in Yahoo Finance format. 192 | expiration_date (str): Options expiration date in YYYY-MM-DD format. 193 | """ 194 | stock = Ticker(ticker=symbol, session=self.session) 195 | option_chain = stock.option_chain(expiration_date) 196 | 197 | result = {"calls": None, "puts": None, "underlying": option_chain.underlying} 198 | 199 | if option_chain.calls is not None: 200 | # Convert dates to strings for JSON serialization 201 | calls_df = option_chain.calls.copy() 202 | if "lastTradeDate" in calls_df.columns: 203 | calls_df["lastTradeDate"] = calls_df["lastTradeDate"].astype(str) 204 | result["calls"] = calls_df.to_dict(orient="records") 205 | 206 | if option_chain.puts is not None: 207 | # Convert dates to strings for JSON serialization 208 | puts_df = option_chain.puts.copy() 209 | if "lastTradeDate" in puts_df.columns: 210 | puts_df["lastTradeDate"] = puts_df["lastTradeDate"].astype(str) 211 | result["puts"] = puts_df.to_dict(orient="records") 212 | 213 | return json.dumps(result, indent=2) 214 | 215 | 216 | async def serve() -> None: 217 | server = Server("mcp-yahoo-finance") 218 | yf = YahooFinance() 219 | 220 | @server.list_tools() 221 | async def list_tools() -> list[Tool]: 222 | return [ 223 | generate_tool(yf.get_current_stock_price), 224 | generate_tool(yf.get_stock_price_by_date), 225 | generate_tool(yf.get_stock_price_date_range), 226 | generate_tool(yf.get_historical_stock_prices), 227 | generate_tool(yf.get_dividends), 228 | generate_tool(yf.get_income_statement), 229 | generate_tool(yf.get_cashflow), 230 | generate_tool(yf.get_earning_dates), 231 | generate_tool(yf.get_news), 232 | generate_tool(yf.get_recommendations), 233 | generate_tool(yf.get_option_expiration_dates), 234 | generate_tool(yf.get_option_chain), 235 | ] 236 | 237 | @server.call_tool() 238 | async def call_tool(name: str, args: dict[str, Any]) -> list[TextContent]: 239 | match name: 240 | case "get_current_stock_price": 241 | price = yf.get_current_stock_price(**args) 242 | return [TextContent(type="text", text=price)] 243 | case "get_stock_price_by_date": 244 | price = yf.get_stock_price_by_date(**args) 245 | return [TextContent(type="text", text=price)] 246 | case "get_stock_price_date_range": 247 | price = yf.get_stock_price_date_range(**args) 248 | return [TextContent(type="text", text=price)] 249 | case "get_historical_stock_prices": 250 | price = yf.get_historical_stock_prices(**args) 251 | return [TextContent(type="text", text=price)] 252 | case "get_dividends": 253 | price = yf.get_dividends(**args) 254 | return [TextContent(type="text", text=price)] 255 | case "get_income_statement": 256 | price = yf.get_income_statement(**args) 257 | return [TextContent(type="text", text=price)] 258 | case "get_cashflow": 259 | price = yf.get_cashflow(**args) 260 | return [TextContent(type="text", text=price)] 261 | case "get_earning_dates": 262 | price = yf.get_earning_dates(**args) 263 | return [TextContent(type="text", text=price)] 264 | case "get_news": 265 | price = yf.get_news(**args) 266 | return [TextContent(type="text", text=price)] 267 | case "get_recommendations": 268 | recommendations = yf.get_recommendations(**args) 269 | return [TextContent(type="text", text=recommendations)] 270 | case "get_option_expiration_dates": 271 | dates = yf.get_option_expiration_dates(**args) 272 | return [TextContent(type="text", text=dates)] 273 | case "get_option_chain": 274 | chain = yf.get_option_chain(**args) 275 | return [TextContent(type="text", text=chain)] 276 | case _: 277 | raise ValueError(f"Unknown tool: {name}") 278 | 279 | options = server.create_initialization_options() 280 | async with stdio_server() as (read_stream, write_stream): 281 | await server.run(read_stream, write_stream, options, raise_exceptions=True) 282 | ```