yahoo-finance-mcp-server 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -169
- package/README.md +72 -22
- package/bin/cli.js +6 -1
- package/package.json +51 -51
- package/server.json +23 -23
- package/yahoo_finance_mcp.py +173 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,169 +1,223 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project are documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [1.
|
|
9
|
-
|
|
10
|
-
###
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
[1.
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.3.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `--http` serves the same 13 tools over Streamable HTTP at `/mcp`, alongside
|
|
13
|
+
`--host`, `--port`, `--allowed-host` and `--stateful`. stdio is still the
|
|
14
|
+
default and is unchanged. This exists because a client that only accepts a
|
|
15
|
+
URL - n8n's MCP Client Tool node offers HTTP Streamable and SSE and no way to
|
|
16
|
+
spawn a command - could not reach a stdio-only server at all.
|
|
17
|
+
- DNS-rebinding protection now follows the host actually bound. FastMCP decides
|
|
18
|
+
its allow-list inside the constructor from the host it was given, so binding
|
|
19
|
+
wider at run time previously left the built-in localhost list rejecting the
|
|
20
|
+
very clients the wider bind was for, with a bare `421 Misdirected Request`.
|
|
21
|
+
`localhost`, `127.0.0.1` and `host.docker.internal` on the bound port are
|
|
22
|
+
accepted by default; anything else needs `--allowed-host`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`get_financial_statements` returned an error in JSON mode for every
|
|
27
|
+
ticker.** The statement frames are keyed by pandas Timestamp columns, and
|
|
28
|
+
`json.dumps` coerces values through `default=` but never keys, so the tool
|
|
29
|
+
answered `keys must be str, int, float, bool or None, not Timestamp`. Markdown
|
|
30
|
+
was unaffected, which is why it went unnoticed.
|
|
31
|
+
- **`get_analyst_recommendations` returned nothing in JSON mode for every
|
|
32
|
+
widely covered stock.** The markdown branch caps the upgrades/downgrades frame
|
|
33
|
+
at ten rows; the JSON branch serialised the entire rating history, reaching
|
|
34
|
+
~263,000 characters on NVDA. Over the response limit, JSON is refused outright
|
|
35
|
+
rather than truncated, so the price targets and consensus were lost too.
|
|
36
|
+
Measured before the fix on NVDA, AAPL, MSFT, PLTR and F: all five refused.
|
|
37
|
+
- **`get_historical_prices` refused a year of daily bars in JSON mode** for the
|
|
38
|
+
same reason. It now returns as many of the newest records as fit, with
|
|
39
|
+
`totalRecords`, `returnedRecords` and `truncated` so the caller can see what
|
|
40
|
+
was left out.
|
|
41
|
+
- Three statements at once exceeded the response limit, so JSON now applies the
|
|
42
|
+
same per-statement 30-row cap the markdown branch already used, and reports
|
|
43
|
+
the true line-item count in `lineItemCounts`.
|
|
44
|
+
- The markdown truncation note read the row count after the frame had already
|
|
45
|
+
been replaced by its own head, so it always claimed the cap as the total
|
|
46
|
+
("Showing first 30 rows of 30 total").
|
|
47
|
+
|
|
48
|
+
## [1.2.5] - 2026-08-06
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- Troubleshooting entry for running behind a non-Anthropic model or proxy
|
|
53
|
+
(LiteLLM, OpenRouter, NVIDIA NIM, local models). The server never talks to a
|
|
54
|
+
model, so the usual causes are a model without function calling, or a proxy
|
|
55
|
+
dropping unsupported parameters and silently stripping tool definitions.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- README section order now matches the other servers in this family:
|
|
60
|
+
Troubleshooting comes before Manual Installation.
|
|
61
|
+
|
|
62
|
+
## [1.2.4] - 2026-07-30
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- Fresh installs no longer crash on startup. `requirements.txt` had no upper
|
|
67
|
+
bound on `mcp`, so after the MCP Python SDK 2.0.0 release (2026-07-28)
|
|
68
|
+
`pip install -r requirements.txt` pulled 2.x, which removed
|
|
69
|
+
`mcp.server.fastmcp` and made the server die with
|
|
70
|
+
`ModuleNotFoundError: No module named 'mcp.server.fastmcp'`. The requirement
|
|
71
|
+
is now `mcp>=1.2.0,<2`. Existing installs were unaffected (the launcher
|
|
72
|
+
caches its virtualenv per requirements hash).
|
|
73
|
+
|
|
74
|
+
## [1.2.3] - 2026-06-06
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- Enriched every tool description with purpose, when-to-use/disambiguation
|
|
79
|
+
guidance, and return/error behavior. This raises self-describing quality for
|
|
80
|
+
AI agents (and Glama's Tool Definition Quality score) while staying lean
|
|
81
|
+
(~140 tokens per tool); parameters remain self-documented via their field
|
|
82
|
+
descriptions.
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- `Dockerfile`, `.dockerignore`, and `glama.json` to support a containerized
|
|
87
|
+
Glama release (builds `python:3.12-slim`, runs the server over stdio as a
|
|
88
|
+
non-root user). Verified locally: the image builds and `tools/list` returns
|
|
89
|
+
all 13 tools.
|
|
90
|
+
|
|
91
|
+
## [1.2.2] - 2026-06-04
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- Oversized responses no longer overflow the response size cap. Previously a
|
|
96
|
+
too-large JSON response could be returned larger than the limit (and dumped a
|
|
97
|
+
giant truncated preview into context); it now returns a small message asking
|
|
98
|
+
to narrow the request.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- Trimmed verbose tool descriptions to cut the always-loaded schema size
|
|
103
|
+
(~1,800 fewer tokens on connect). Tool behavior unchanged.
|
|
104
|
+
|
|
105
|
+
## [1.2.1] - 2026-06-02
|
|
106
|
+
|
|
107
|
+
Packaging only. No runtime code changes.
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
|
|
111
|
+
- `server.json` and an `mcpName` field for listing on the official MCP Registry.
|
|
112
|
+
|
|
113
|
+
## [1.2.0] - 2026-06-01
|
|
114
|
+
|
|
115
|
+
Major feature release: **seven new data tools (6 → 13)** plus rate-limit
|
|
116
|
+
hardening. Every tool was verified end to end by driving the real MCP server
|
|
117
|
+
over stdio against live Yahoo Finance data.
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- **`get_market_news`** — latest news headlines for a ticker, with source,
|
|
122
|
+
date, summary, and link. Handles the modern nested yfinance news format.
|
|
123
|
+
- **`get_options_chain`** — list available expiration dates, or fetch the
|
|
124
|
+
calls/puts chain (strike, bid/ask, volume, open interest, implied
|
|
125
|
+
volatility) for a given expiration.
|
|
126
|
+
- **`get_holders`** — institutional holders, mutual-fund holders, the
|
|
127
|
+
major-holders breakdown, or recent insider transactions.
|
|
128
|
+
- **`get_dividends_splits`** — full dividend payment history (with a trailing
|
|
129
|
+
summary) and stock-split history.
|
|
130
|
+
- **`get_analyst_estimates`** — forward analyst price targets, EPS and revenue
|
|
131
|
+
estimates by period, and growth estimates (complements the existing
|
|
132
|
+
recommendations tool).
|
|
133
|
+
- **`search_symbols`** — find ticker symbols by company name or keyword, with
|
|
134
|
+
exchange, type, sector, and industry.
|
|
135
|
+
- **`get_market_status`** — whether a market (by region) is open or closed,
|
|
136
|
+
with timing and a summary of its major indices.
|
|
137
|
+
- **Shared `curl_cffi` browser-impersonating HTTP session** reused across all
|
|
138
|
+
tools. This cuts down on Yahoo's HTTP 429 rate-limiting and speeds up
|
|
139
|
+
repeated calls; it degrades gracefully to the default session if
|
|
140
|
+
`curl_cffi` is unavailable.
|
|
141
|
+
- Friendly, specific handling of `YFRateLimitError` ("wait and retry")
|
|
142
|
+
across all tools.
|
|
143
|
+
|
|
144
|
+
### Changed
|
|
145
|
+
|
|
146
|
+
- `requirements.txt`: add `curl_cffi`; raise the `yfinance` floor to
|
|
147
|
+
`>=0.2.61` (which carries upstream rate-limit-handling fixes and the
|
|
148
|
+
`Search`/`Market` APIs used by the new tools).
|
|
149
|
+
|
|
150
|
+
## [1.1.1] - 2026-05-31
|
|
151
|
+
|
|
152
|
+
Documentation and packaging only. No runtime code changes; behavior is
|
|
153
|
+
identical to 1.1.0.
|
|
154
|
+
|
|
155
|
+
### Changed
|
|
156
|
+
|
|
157
|
+
- Reworked the README: clearer structure, a tools table with parameters and
|
|
158
|
+
supported `period`/`interval` values, example prompts, and a note clarifying
|
|
159
|
+
that the repository name (`yahoo-finance-mcp`) differs from the npm package
|
|
160
|
+
name (`yahoo-finance-mcp-server`).
|
|
161
|
+
- Added shields.io badges, including a monthly-downloads pill and a Python
|
|
162
|
+
version badge.
|
|
163
|
+
- Normalized `repository.url` to the `git+https://` form npm expects.
|
|
164
|
+
|
|
165
|
+
### Added
|
|
166
|
+
|
|
167
|
+
- This `CHANGELOG.md`, now shipped in the npm tarball.
|
|
168
|
+
|
|
169
|
+
## [1.1.0] - 2026-05-30
|
|
170
|
+
|
|
171
|
+
A full QA pass that fixed several bugs which broke core functionality, and made
|
|
172
|
+
the documented `npx` installation actually work. Every fix was verified end to
|
|
173
|
+
end by driving the real MCP server over stdio against live Yahoo Finance data.
|
|
174
|
+
|
|
175
|
+
### Fixed
|
|
176
|
+
|
|
177
|
+
- **Tool inputs failed validation.** Each tool took a single Pydantic model
|
|
178
|
+
argument, so the MCP schema nested every field under a required `params`
|
|
179
|
+
object while the docs told the model to send fields directly. The result was
|
|
180
|
+
that calls were rejected. Tools now expose their parameters directly.
|
|
181
|
+
- **`get_analyst_recommendations` crashed** with `'RangeIndex' object has no
|
|
182
|
+
attribute 'strftime'` due to a change in the `yfinance` recommendations
|
|
183
|
+
format. It now reads the current format and also reports recent
|
|
184
|
+
upgrades/downgrades.
|
|
185
|
+
- **Dividend yield was shown 100x too high** (for example `35.00%` for AAPL).
|
|
186
|
+
Modern `yfinance` already returns this value as a percentage, so it is no
|
|
187
|
+
longer multiplied again.
|
|
188
|
+
- **Markdown tables broke** when the optional `tabulate` package was missing.
|
|
189
|
+
It is now a declared dependency, with a graceful plain-text fallback.
|
|
190
|
+
- **Truncated JSON responses are now valid JSON** instead of being cut off
|
|
191
|
+
mid-structure.
|
|
192
|
+
|
|
193
|
+
### Added
|
|
194
|
+
|
|
195
|
+
- **`bin/cli.js` Node launcher** so `npx -y yahoo-finance-mcp-server` works. It
|
|
196
|
+
locates Python 3.10+, creates an isolated virtual environment, installs the
|
|
197
|
+
Python dependencies on first run, and starts the server with the protocol
|
|
198
|
+
stream kept clean on stdout.
|
|
199
|
+
- `tabulate` added to the dependencies.
|
|
200
|
+
|
|
201
|
+
### Changed
|
|
202
|
+
|
|
203
|
+
- `package.json` `bin` now points at the Node launcher, the binary name matches
|
|
204
|
+
the package name, and a Node engine requirement was added.
|
|
205
|
+
- Tightened dependency floors (`yfinance>=0.2.40`, `mcp>=1.2.0`).
|
|
206
|
+
- Replaced the deprecated Pydantic `min_items`/`max_items` with
|
|
207
|
+
`min_length`/`max_length`.
|
|
208
|
+
- Rewrote the README and corrected the installation and troubleshooting docs.
|
|
209
|
+
|
|
210
|
+
## [1.0.0] - 2025-11-01
|
|
211
|
+
|
|
212
|
+
### Added
|
|
213
|
+
|
|
214
|
+
- Initial release of the Yahoo Finance MCP Server, distributed on npm.
|
|
215
|
+
- Six tools: `get_stock_quote`, `get_historical_prices`, `get_company_info`,
|
|
216
|
+
`get_financial_statements`, `compare_stocks`, and
|
|
217
|
+
`get_analyst_recommendations`.
|
|
218
|
+
- Markdown and JSON output formats.
|
|
219
|
+
|
|
220
|
+
[1.2.0]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.2.0
|
|
221
|
+
[1.1.1]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.1.1
|
|
222
|
+
[1.1.0]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.1.0
|
|
223
|
+
[1.0.0]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -63,6 +63,47 @@ npm install -g yahoo-finance-mcp-server
|
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## 🌐 HTTP mode (n8n, remote agents, anything that connects to a URL)
|
|
67
|
+
|
|
68
|
+
Some clients spawn an MCP server as a child process and talk to it over stdio;
|
|
69
|
+
others only take a URL. n8n's **MCP Client Tool** node is the second kind - it
|
|
70
|
+
offers HTTP Streamable and SSE and no way to launch a command - so stdio alone
|
|
71
|
+
puts the server out of reach.
|
|
72
|
+
|
|
73
|
+
`--http` serves the same 13 tools over Streamable HTTP at `/mcp`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx -y yahoo-finance-mcp-server --http
|
|
77
|
+
# -> http://127.0.0.1:8000/mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Flag | Default | What it does |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `--http` | off | Serve Streamable HTTP at `/mcp` instead of stdio |
|
|
83
|
+
| `--host` | `127.0.0.1` | Interface to bind |
|
|
84
|
+
| `--port` | `8000` | Port to bind |
|
|
85
|
+
| `--allowed-host` | see below | Host header to accept, repeatable |
|
|
86
|
+
| `--stateful` | off | Keep a session per client instead of stateless |
|
|
87
|
+
|
|
88
|
+
No authentication is added, so keep it on an interface you trust.
|
|
89
|
+
|
|
90
|
+
### Reaching it from a container
|
|
91
|
+
|
|
92
|
+
n8n in Docker is not on your loopback interface, so bind wider and use the
|
|
93
|
+
host alias:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx -y yahoo-finance-mcp-server --http --host 0.0.0.0 --port 8931
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then point the MCP Client Tool node at `http://host.docker.internal:8931/mcp`
|
|
100
|
+
with authentication set to **None**.
|
|
101
|
+
|
|
102
|
+
DNS-rebinding protection stays on when you bind wider. `localhost`,
|
|
103
|
+
`127.0.0.1` and `host.docker.internal` on the bound port are accepted by
|
|
104
|
+
default; any other name needs `--allowed-host name:port`, and a request
|
|
105
|
+
arriving under an unlisted name gets `421 Misdirected Request`.
|
|
106
|
+
|
|
66
107
|
## 🔧 Available Tools
|
|
67
108
|
|
|
68
109
|
| Tool | What it returns | Parameters |
|
|
@@ -107,6 +148,37 @@ Once the server is connected, just ask Claude:
|
|
|
107
148
|
- "Who are the biggest institutional holders of Apple?"
|
|
108
149
|
- "What's Coca-Cola's dividend history?"
|
|
109
150
|
|
|
151
|
+
## 🐛 Troubleshooting
|
|
152
|
+
|
|
153
|
+
**"Command not found" / "Python not found"**
|
|
154
|
+
Make sure Python and Node.js are installed and on your PATH. On macOS/Linux, try `python3` instead of `python` in the config.
|
|
155
|
+
|
|
156
|
+
**"Module not found: yfinance" (manual install only)**
|
|
157
|
+
Install the dependencies:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install yfinance curl_cffi pandas tabulate mcp pydantic httpx
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Tools not showing up in Claude**
|
|
164
|
+
1. Confirm the config file is valid JSON (no trailing commas).
|
|
165
|
+
2. Fully quit and reopen Claude Desktop.
|
|
166
|
+
3. Check the path in your config actually exists.
|
|
167
|
+
|
|
168
|
+
**"Error fetching data"**
|
|
169
|
+
- Check your internet connection.
|
|
170
|
+
- Verify the ticker symbol (for example `AAPL`, not `Apple`).
|
|
171
|
+
- Some smaller companies have limited data, and Yahoo Finance can be briefly unavailable.
|
|
172
|
+
|
|
173
|
+
**Using a different model or provider (LiteLLM, OpenRouter, NVIDIA NIM, a local model)**
|
|
174
|
+
This server never talks to a model. Your client starts it as a local process and
|
|
175
|
+
speaks JSON-RPC over stdin/stdout, so changing `ANTHROPIC_BASE_URL` or swapping the
|
|
176
|
+
model behind your client has no effect on it. If tools stop firing after a switch
|
|
177
|
+
like that, check two things: the model has to support function calling, and a proxy
|
|
178
|
+
configured to drop unsupported parameters can silently strip your tool definitions,
|
|
179
|
+
which produces no error at all. Run `/mcp` in your client (or `claude mcp list`) to
|
|
180
|
+
confirm the server is connected before suspecting the server.
|
|
181
|
+
|
|
110
182
|
## 🛠️ Manual Installation (Alternative)
|
|
111
183
|
|
|
112
184
|
If you would rather run the Python file directly instead of via npx:
|
|
@@ -138,28 +210,6 @@ On Windows use `"command": "python"` and a path like `"C:\\path\\to\\yahoo_finan
|
|
|
138
210
|
|
|
139
211
|
**3. Restart Claude Desktop.**
|
|
140
212
|
|
|
141
|
-
## 🐛 Troubleshooting
|
|
142
|
-
|
|
143
|
-
**"Command not found" / "Python not found"**
|
|
144
|
-
Make sure Python and Node.js are installed and on your PATH. On macOS/Linux, try `python3` instead of `python` in the config.
|
|
145
|
-
|
|
146
|
-
**"Module not found: yfinance" (manual install only)**
|
|
147
|
-
Install the dependencies:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
pip install yfinance curl_cffi pandas tabulate mcp pydantic httpx
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Tools not showing up in Claude**
|
|
154
|
-
1. Confirm the config file is valid JSON (no trailing commas).
|
|
155
|
-
2. Fully quit and reopen Claude Desktop.
|
|
156
|
-
3. Check the path in your config actually exists.
|
|
157
|
-
|
|
158
|
-
**"Error fetching data"**
|
|
159
|
-
- Check your internet connection.
|
|
160
|
-
- Verify the ticker symbol (for example `AAPL`, not `Apple`).
|
|
161
|
-
- Some smaller companies have limited data, and Yahoo Finance can be briefly unavailable.
|
|
162
|
-
|
|
163
213
|
## 🔒 Privacy & Rate Limits
|
|
164
214
|
|
|
165
215
|
- Uses the free Yahoo Finance API via the `yfinance` library.
|
package/bin/cli.js
CHANGED
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
* passes straight through.
|
|
15
15
|
*
|
|
16
16
|
* IMPORTANT: stdout is the MCP protocol channel. This wrapper must never write
|
|
17
|
-
* anything to stdout. All diagnostics go to stderr.
|
|
17
|
+
* anything to stdout. All diagnostics go to stderr. (In `--http` mode the
|
|
18
|
+
* protocol moves to the socket and stdout is free, but the rule stays: the
|
|
19
|
+
* wrapper cannot know which mode the arguments select without parsing them.)
|
|
20
|
+
*
|
|
21
|
+
* Arguments are forwarded to the Python server untouched, so `--http` and its
|
|
22
|
+
* companions work through `npx` exactly as they do on a direct invocation.
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
const { spawn, spawnSync } = require("child_process");
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "yahoo-finance-mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"mcpName": "io.github.danishashko/yahoo-finance-mcp",
|
|
5
|
-
"description": "Yahoo Finance MCP Server - Real-time stock data, company info, financial statements, and market analysis via Model Context Protocol",
|
|
6
|
-
"main": "bin/cli.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"yahoo-finance-mcp-server": "bin/cli.js",
|
|
9
|
-
"yahoo-finance-mcp": "bin/cli.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "python test_installation.py"
|
|
13
|
-
},
|
|
14
|
-
"keywords": [
|
|
15
|
-
"mcp",
|
|
16
|
-
"model-context-protocol",
|
|
17
|
-
"yahoo-finance",
|
|
18
|
-
"stocks",
|
|
19
|
-
"finance",
|
|
20
|
-
"market-data",
|
|
21
|
-
"stock-prices",
|
|
22
|
-
"financial-data",
|
|
23
|
-
"yfinance",
|
|
24
|
-
"claude",
|
|
25
|
-
"ai",
|
|
26
|
-
"llm"
|
|
27
|
-
],
|
|
28
|
-
"author": "Daniel Shashko",
|
|
29
|
-
"license": "MIT",
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "git+https://github.com/danishashko/yahoo-finance-mcp.git"
|
|
33
|
-
},
|
|
34
|
-
"homepage": "https://github.com/danishashko/yahoo-finance-mcp#readme",
|
|
35
|
-
"bugs": {
|
|
36
|
-
"url": "https://github.com/danishashko/yahoo-finance-mcp/issues"
|
|
37
|
-
},
|
|
38
|
-
"engines": {
|
|
39
|
-
"node": ">=16",
|
|
40
|
-
"python": ">=3.10"
|
|
41
|
-
},
|
|
42
|
-
"files": [
|
|
43
|
-
"bin/",
|
|
44
|
-
"yahoo_finance_mcp.py",
|
|
45
|
-
"requirements.txt",
|
|
46
|
-
"README.md",
|
|
47
|
-
"CHANGELOG.md",
|
|
48
|
-
"LICENSE",
|
|
49
|
-
"server.json"
|
|
50
|
-
]
|
|
51
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "yahoo-finance-mcp-server",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"mcpName": "io.github.danishashko/yahoo-finance-mcp",
|
|
5
|
+
"description": "Yahoo Finance MCP Server - Real-time stock data, company info, financial statements, and market analysis via Model Context Protocol",
|
|
6
|
+
"main": "bin/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yahoo-finance-mcp-server": "bin/cli.js",
|
|
9
|
+
"yahoo-finance-mcp": "bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "python test_installation.py"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"yahoo-finance",
|
|
18
|
+
"stocks",
|
|
19
|
+
"finance",
|
|
20
|
+
"market-data",
|
|
21
|
+
"stock-prices",
|
|
22
|
+
"financial-data",
|
|
23
|
+
"yfinance",
|
|
24
|
+
"claude",
|
|
25
|
+
"ai",
|
|
26
|
+
"llm"
|
|
27
|
+
],
|
|
28
|
+
"author": "Daniel Shashko",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/danishashko/yahoo-finance-mcp.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/danishashko/yahoo-finance-mcp#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/danishashko/yahoo-finance-mcp/issues"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16",
|
|
40
|
+
"python": ">=3.10"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"bin/",
|
|
44
|
+
"yahoo_finance_mcp.py",
|
|
45
|
+
"requirements.txt",
|
|
46
|
+
"README.md",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"server.json"
|
|
50
|
+
]
|
|
51
|
+
}
|
package/server.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.danishashko/yahoo-finance-mcp",
|
|
4
|
-
"description": "Real-time Yahoo Finance data: quotes, history, financials, analyst ratings, options, and news.",
|
|
5
|
-
"title": "Yahoo Finance",
|
|
6
|
-
"repository": {
|
|
7
|
-
"url": "https://github.com/danishashko/yahoo-finance-mcp",
|
|
8
|
-
"source": "github"
|
|
9
|
-
},
|
|
10
|
-
"version": "1.
|
|
11
|
-
"packages": [
|
|
12
|
-
{
|
|
13
|
-
"registryType": "npm",
|
|
14
|
-
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
|
-
"identifier": "yahoo-finance-mcp-server",
|
|
16
|
-
"version": "1.
|
|
17
|
-
"runtimeHint": "npx",
|
|
18
|
-
"transport": {
|
|
19
|
-
"type": "stdio"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.danishashko/yahoo-finance-mcp",
|
|
4
|
+
"description": "Real-time Yahoo Finance data: quotes, history, financials, analyst ratings, options, and news.",
|
|
5
|
+
"title": "Yahoo Finance",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/danishashko/yahoo-finance-mcp",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "1.3.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
|
+
"identifier": "yahoo-finance-mcp-server",
|
|
16
|
+
"version": "1.3.0",
|
|
17
|
+
"runtimeHint": "npx",
|
|
18
|
+
"transport": {
|
|
19
|
+
"type": "stdio"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/yahoo_finance_mcp.py
CHANGED
|
@@ -8,6 +8,7 @@ including stock prices, company information, financial statements, and market an
|
|
|
8
8
|
Built with FastMCP and yfinance.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import argparse
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
13
14
|
from enum import Enum
|
|
@@ -18,6 +19,7 @@ import pandas as pd
|
|
|
18
19
|
from pydantic import Field
|
|
19
20
|
|
|
20
21
|
from mcp.server.fastmcp import FastMCP
|
|
22
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
21
23
|
|
|
22
24
|
# Keep yfinance from emitting warnings/progress that could clutter the
|
|
23
25
|
# stderr stream of an MCP stdio server. The JSON-RPC channel is stdout only.
|
|
@@ -232,8 +234,9 @@ def dataframe_to_markdown(df: pd.DataFrame, max_rows: int = 50) -> str:
|
|
|
232
234
|
|
|
233
235
|
truncated_msg = ""
|
|
234
236
|
if len(df) > max_rows:
|
|
237
|
+
total = len(df)
|
|
235
238
|
df = df.head(max_rows)
|
|
236
|
-
truncated_msg = f"\n\n*Showing first {max_rows} rows of {
|
|
239
|
+
truncated_msg = f"\n\n*Showing first {max_rows} rows of {total} total*"
|
|
237
240
|
|
|
238
241
|
try:
|
|
239
242
|
return df.to_markdown() + truncated_msg
|
|
@@ -242,6 +245,57 @@ def dataframe_to_markdown(df: pd.DataFrame, max_rows: int = 50) -> str:
|
|
|
242
245
|
return "```\n" + df.to_string() + "\n```" + truncated_msg
|
|
243
246
|
|
|
244
247
|
|
|
248
|
+
def _statement_to_dict(df: Any, max_rows: int = 30) -> Dict[str, Any]:
|
|
249
|
+
"""A financial statement keyed by period, with the periods as strings.
|
|
250
|
+
|
|
251
|
+
Every column label on these frames is a pandas Timestamp, and `to_dict()`
|
|
252
|
+
makes those labels the keys of the returned mapping. `json.dumps` only
|
|
253
|
+
coerces *values* through `default=`, never keys, so serialising the frame
|
|
254
|
+
raised "keys must be str, int, float, bool or None, not Timestamp" and the
|
|
255
|
+
whole tool answered with an error string - in JSON mode only, while the
|
|
256
|
+
markdown branch rendered the same frame fine.
|
|
257
|
+
"""
|
|
258
|
+
if df is None or getattr(df, "empty", True):
|
|
259
|
+
return {}
|
|
260
|
+
out = df.copy()
|
|
261
|
+
out.columns = [
|
|
262
|
+
c.strftime("%Y-%m-%d") if hasattr(c, "strftime") else str(c)
|
|
263
|
+
for c in out.columns
|
|
264
|
+
]
|
|
265
|
+
# Same 30-row cap the markdown branch applies per statement. All three
|
|
266
|
+
# uncapped come to ~38,800 characters for NVDA against a 25,000 limit, and
|
|
267
|
+
# an oversized JSON payload is refused rather than trimmed, so leaving them
|
|
268
|
+
# whole cost the caller all three statements instead of the tail of each.
|
|
269
|
+
if len(out) > max_rows:
|
|
270
|
+
out = out.head(max_rows)
|
|
271
|
+
return out.to_dict()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _fit_records(records: List[Any], envelope: Dict[str, Any], key: str) -> str:
|
|
275
|
+
"""Serialise as many of the newest records as the response limit allows.
|
|
276
|
+
|
|
277
|
+
The JSON branch refuses an oversized payload outright rather than trimming
|
|
278
|
+
it, so an uncapped frame does not come back shortened - it does not come
|
|
279
|
+
back at all. A year of daily bars is about 65,000 characters, so asking for
|
|
280
|
+
a year silently returned nothing. Keep the tail, and say what was dropped.
|
|
281
|
+
"""
|
|
282
|
+
kept = len(records)
|
|
283
|
+
while True:
|
|
284
|
+
body = dict(envelope)
|
|
285
|
+
body[key] = records[len(records) - kept :] if kept else []
|
|
286
|
+
body["returnedRecords"] = kept
|
|
287
|
+
body["truncated"] = kept < len(records)
|
|
288
|
+
if body["truncated"]:
|
|
289
|
+
body["note"] = (
|
|
290
|
+
f"Newest {kept} of {len(records)} records. Narrow the period or "
|
|
291
|
+
"widen the interval for full coverage."
|
|
292
|
+
)
|
|
293
|
+
payload = json.dumps(body, indent=2, default=str)
|
|
294
|
+
if len(payload) <= CHARACTER_LIMIT or kept == 0:
|
|
295
|
+
return payload
|
|
296
|
+
kept = kept * 3 // 4 if kept > 4 else kept - 1
|
|
297
|
+
|
|
298
|
+
|
|
245
299
|
def truncate_response(response: str, message: str = "") -> str:
|
|
246
300
|
"""Truncate a (markdown/text) response if it exceeds CHARACTER_LIMIT."""
|
|
247
301
|
if len(response) <= CHARACTER_LIMIT:
|
|
@@ -482,7 +536,7 @@ async def get_historical_prices(
|
|
|
482
536
|
result += dataframe_to_markdown(recent_data)
|
|
483
537
|
|
|
484
538
|
if len(hist) > 10:
|
|
485
|
-
result += f"\n\n*Showing last 10 of {len(hist)} records.
|
|
539
|
+
result += f"\n\n*Showing last 10 of {len(hist)} records. JSON format returns as many of the newest records as fit in one response.*"
|
|
486
540
|
|
|
487
541
|
return truncate_response(
|
|
488
542
|
result,
|
|
@@ -496,16 +550,15 @@ async def get_historical_prices(
|
|
|
496
550
|
elif "Datetime" in record:
|
|
497
551
|
record["Datetime"] = record["Datetime"].isoformat()
|
|
498
552
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"Consider using a shorter period.",
|
|
553
|
+
return _fit_records(
|
|
554
|
+
hist_dict,
|
|
555
|
+
{
|
|
556
|
+
"ticker": ticker,
|
|
557
|
+
"period": period.value,
|
|
558
|
+
"interval": interval.value,
|
|
559
|
+
"totalRecords": len(hist),
|
|
560
|
+
},
|
|
561
|
+
"data",
|
|
509
562
|
)
|
|
510
563
|
|
|
511
564
|
except Exception as e:
|
|
@@ -708,17 +761,17 @@ async def get_financial_statements(
|
|
|
708
761
|
result, "Request specific statement types separately if needed."
|
|
709
762
|
)
|
|
710
763
|
else:
|
|
711
|
-
|
|
712
|
-
"
|
|
713
|
-
"
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
764
|
+
statements = {
|
|
765
|
+
"incomeStatement": income_stmt,
|
|
766
|
+
"balanceSheet": balance_sheet,
|
|
767
|
+
"cashFlow": cash_flow,
|
|
768
|
+
}
|
|
769
|
+
result = {"ticker": ticker}
|
|
770
|
+
result.update({k: _statement_to_dict(v) for k, v in statements.items()})
|
|
771
|
+
result["lineItemCounts"] = {
|
|
772
|
+
k: int(len(v))
|
|
773
|
+
for k, v in statements.items()
|
|
774
|
+
if v is not None and not v.empty
|
|
722
775
|
}
|
|
723
776
|
return truncate_json_response(
|
|
724
777
|
json.dumps(result, indent=2, default=str),
|
|
@@ -967,7 +1020,15 @@ async def get_analyst_recommendations(
|
|
|
967
1020
|
"recommendationTrend": recommendations.to_dict(orient="records")
|
|
968
1021
|
if recommendations is not None and not recommendations.empty
|
|
969
1022
|
else [],
|
|
970
|
-
|
|
1023
|
+
# Same last-10 cap the markdown branch applies. Without it this
|
|
1024
|
+
# frame is the full rating history, and on a widely covered
|
|
1025
|
+
# stock that is ~260,000 characters - over the response limit,
|
|
1026
|
+
# so the whole tool answered `response_too_large` and returned
|
|
1027
|
+
# no targets and no consensus either. Measured on NVDA, AAPL,
|
|
1028
|
+
# MSFT, PLTR and F: every one of them, markdown fine, JSON dead.
|
|
1029
|
+
"upgradesDowngrades": upgrades.head(10)
|
|
1030
|
+
.reset_index()
|
|
1031
|
+
.to_dict(orient="records")
|
|
971
1032
|
if upgrades is not None and not upgrades.empty
|
|
972
1033
|
else [],
|
|
973
1034
|
}
|
|
@@ -1657,9 +1718,95 @@ async def get_market_status(
|
|
|
1657
1718
|
# ============================================================================
|
|
1658
1719
|
|
|
1659
1720
|
|
|
1721
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
1722
|
+
parser = argparse.ArgumentParser(
|
|
1723
|
+
prog="yahoo-finance-mcp",
|
|
1724
|
+
description="Yahoo Finance MCP server (stdio by default).",
|
|
1725
|
+
)
|
|
1726
|
+
parser.add_argument(
|
|
1727
|
+
"--http",
|
|
1728
|
+
action="store_true",
|
|
1729
|
+
help=(
|
|
1730
|
+
"Serve over Streamable HTTP at http://HOST:PORT/mcp instead of "
|
|
1731
|
+
"stdio. Use this for clients that connect to a URL rather than "
|
|
1732
|
+
"spawning a process (n8n, remote agents)."
|
|
1733
|
+
),
|
|
1734
|
+
)
|
|
1735
|
+
parser.add_argument(
|
|
1736
|
+
"--host",
|
|
1737
|
+
default="127.0.0.1",
|
|
1738
|
+
help=(
|
|
1739
|
+
"Interface to bind in --http mode (default: 127.0.0.1). Use "
|
|
1740
|
+
"0.0.0.0 to accept connections from other containers on the host."
|
|
1741
|
+
),
|
|
1742
|
+
)
|
|
1743
|
+
parser.add_argument(
|
|
1744
|
+
"--port",
|
|
1745
|
+
type=int,
|
|
1746
|
+
default=8000,
|
|
1747
|
+
help="Port to bind in --http mode (default: 8000).",
|
|
1748
|
+
)
|
|
1749
|
+
parser.add_argument(
|
|
1750
|
+
"--allowed-host",
|
|
1751
|
+
action="append",
|
|
1752
|
+
metavar="HOST[:PORT]",
|
|
1753
|
+
help=(
|
|
1754
|
+
"Host header to accept in --http mode, repeatable. Only consulted "
|
|
1755
|
+
"when --host is not loopback. Defaults to localhost, 127.0.0.1 and "
|
|
1756
|
+
"host.docker.internal on the bound port."
|
|
1757
|
+
),
|
|
1758
|
+
)
|
|
1759
|
+
parser.add_argument(
|
|
1760
|
+
"--stateful",
|
|
1761
|
+
action="store_true",
|
|
1762
|
+
help=(
|
|
1763
|
+
"Keep a session per client in --http mode. The default is "
|
|
1764
|
+
"stateless, which every tool call in this server is anyway and "
|
|
1765
|
+
"which survives clients that drop the Mcp-Session-Id header."
|
|
1766
|
+
),
|
|
1767
|
+
)
|
|
1768
|
+
return parser
|
|
1769
|
+
|
|
1770
|
+
|
|
1660
1771
|
def main() -> None:
|
|
1661
|
-
"""Run the MCP server
|
|
1662
|
-
|
|
1772
|
+
"""Run the MCP server over stdio, or over Streamable HTTP with --http."""
|
|
1773
|
+
args = _build_arg_parser().parse_args()
|
|
1774
|
+
|
|
1775
|
+
if not args.http:
|
|
1776
|
+
mcp.run()
|
|
1777
|
+
return
|
|
1778
|
+
|
|
1779
|
+
# These are read at run time, so setting them on the module-level server
|
|
1780
|
+
# after argument parsing is enough - no second FastMCP instance.
|
|
1781
|
+
mcp.settings.host = args.host
|
|
1782
|
+
mcp.settings.port = args.port
|
|
1783
|
+
# Every tool here is a single request/response against Yahoo Finance with
|
|
1784
|
+
# nothing carried between calls, so a session buys nothing and costs
|
|
1785
|
+
# compatibility: a client that forgets Mcp-Session-Id gets a 400 mid-run.
|
|
1786
|
+
mcp.settings.stateless_http = not args.stateful
|
|
1787
|
+
|
|
1788
|
+
if args.host not in ("127.0.0.1", "localhost", "::1"):
|
|
1789
|
+
# FastMCP picks its DNS-rebinding allow-list inside its constructor,
|
|
1790
|
+
# from the host it was constructed with - and this server is built at
|
|
1791
|
+
# import time on the loopback default. So binding wider here would
|
|
1792
|
+
# leave that allow-list rejecting the very clients the wider bind was
|
|
1793
|
+
# for, with a bare "421 Misdirected Request" and no hint why. Rebuild
|
|
1794
|
+
# the list around the host actually bound instead of switching the
|
|
1795
|
+
# protection off.
|
|
1796
|
+
hosts = args.allowed_host or [
|
|
1797
|
+
f"127.0.0.1:{args.port}",
|
|
1798
|
+
f"localhost:{args.port}",
|
|
1799
|
+
# the alias a container uses to reach a server on its host, which
|
|
1800
|
+
# is how n8n and other dockerised clients arrive
|
|
1801
|
+
f"host.docker.internal:{args.port}",
|
|
1802
|
+
]
|
|
1803
|
+
mcp.settings.transport_security = TransportSecuritySettings(
|
|
1804
|
+
enable_dns_rebinding_protection=True,
|
|
1805
|
+
allowed_hosts=hosts,
|
|
1806
|
+
allowed_origins=[f"http://{h}" for h in hosts],
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1809
|
+
mcp.run(transport="streamable-http")
|
|
1663
1810
|
|
|
1664
1811
|
|
|
1665
1812
|
if __name__ == "__main__":
|