#
tokens: 8148/50000 15/15 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ![PyPI - Version](https://img.shields.io/pypi/v/mcp-yahoo-finance)
  4 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mcp-yahoo-finance)
  5 | ![PyPI - License](https://img.shields.io/pypi/l/mcp-yahoo-finance)
  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 | 
```