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 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.2.4] - 2026-07-30
9
-
10
- ### Fixed
11
-
12
- - Fresh installs no longer crash on startup. `requirements.txt` had no upper
13
- bound on `mcp`, so after the MCP Python SDK 2.0.0 release (2026-07-28)
14
- `pip install -r requirements.txt` pulled 2.x, which removed
15
- `mcp.server.fastmcp` and made the server die with
16
- `ModuleNotFoundError: No module named 'mcp.server.fastmcp'`. The requirement
17
- is now `mcp>=1.2.0,<2`. Existing installs were unaffected (the launcher
18
- caches its virtualenv per requirements hash).
19
-
20
- ## [1.2.3] - 2026-06-06
21
-
22
- ### Changed
23
-
24
- - Enriched every tool description with purpose, when-to-use/disambiguation
25
- guidance, and return/error behavior. This raises self-describing quality for
26
- AI agents (and Glama's Tool Definition Quality score) while staying lean
27
- (~140 tokens per tool); parameters remain self-documented via their field
28
- descriptions.
29
-
30
- ### Added
31
-
32
- - `Dockerfile`, `.dockerignore`, and `glama.json` to support a containerized
33
- Glama release (builds `python:3.12-slim`, runs the server over stdio as a
34
- non-root user). Verified locally: the image builds and `tools/list` returns
35
- all 13 tools.
36
-
37
- ## [1.2.2] - 2026-06-04
38
-
39
- ### Fixed
40
-
41
- - Oversized responses no longer overflow the response size cap. Previously a
42
- too-large JSON response could be returned larger than the limit (and dumped a
43
- giant truncated preview into context); it now returns a small message asking
44
- to narrow the request.
45
-
46
- ### Changed
47
-
48
- - Trimmed verbose tool descriptions to cut the always-loaded schema size
49
- (~1,800 fewer tokens on connect). Tool behavior unchanged.
50
-
51
- ## [1.2.1] - 2026-06-02
52
-
53
- Packaging only. No runtime code changes.
54
-
55
- ### Added
56
-
57
- - `server.json` and an `mcpName` field for listing on the official MCP Registry.
58
-
59
- ## [1.2.0] - 2026-06-01
60
-
61
- Major feature release: **seven new data tools (6 → 13)** plus rate-limit
62
- hardening. Every tool was verified end to end by driving the real MCP server
63
- over stdio against live Yahoo Finance data.
64
-
65
- ### Added
66
-
67
- - **`get_market_news`** — latest news headlines for a ticker, with source,
68
- date, summary, and link. Handles the modern nested yfinance news format.
69
- - **`get_options_chain`** — list available expiration dates, or fetch the
70
- calls/puts chain (strike, bid/ask, volume, open interest, implied
71
- volatility) for a given expiration.
72
- - **`get_holders`** — institutional holders, mutual-fund holders, the
73
- major-holders breakdown, or recent insider transactions.
74
- - **`get_dividends_splits`** — full dividend payment history (with a trailing
75
- summary) and stock-split history.
76
- - **`get_analyst_estimates`** — forward analyst price targets, EPS and revenue
77
- estimates by period, and growth estimates (complements the existing
78
- recommendations tool).
79
- - **`search_symbols`** — find ticker symbols by company name or keyword, with
80
- exchange, type, sector, and industry.
81
- - **`get_market_status`** — whether a market (by region) is open or closed,
82
- with timing and a summary of its major indices.
83
- - **Shared `curl_cffi` browser-impersonating HTTP session** reused across all
84
- tools. This cuts down on Yahoo's HTTP 429 rate-limiting and speeds up
85
- repeated calls; it degrades gracefully to the default session if
86
- `curl_cffi` is unavailable.
87
- - Friendly, specific handling of `YFRateLimitError` ("wait and retry")
88
- across all tools.
89
-
90
- ### Changed
91
-
92
- - `requirements.txt`: add `curl_cffi`; raise the `yfinance` floor to
93
- `>=0.2.61` (which carries upstream rate-limit-handling fixes and the
94
- `Search`/`Market` APIs used by the new tools).
95
-
96
- ## [1.1.1] - 2026-05-31
97
-
98
- Documentation and packaging only. No runtime code changes; behavior is
99
- identical to 1.1.0.
100
-
101
- ### Changed
102
-
103
- - Reworked the README: clearer structure, a tools table with parameters and
104
- supported `period`/`interval` values, example prompts, and a note clarifying
105
- that the repository name (`yahoo-finance-mcp`) differs from the npm package
106
- name (`yahoo-finance-mcp-server`).
107
- - Added shields.io badges, including a monthly-downloads pill and a Python
108
- version badge.
109
- - Normalized `repository.url` to the `git+https://` form npm expects.
110
-
111
- ### Added
112
-
113
- - This `CHANGELOG.md`, now shipped in the npm tarball.
114
-
115
- ## [1.1.0] - 2026-05-30
116
-
117
- A full QA pass that fixed several bugs which broke core functionality, and made
118
- the documented `npx` installation actually work. Every fix was verified end to
119
- end by driving the real MCP server over stdio against live Yahoo Finance data.
120
-
121
- ### Fixed
122
-
123
- - **Tool inputs failed validation.** Each tool took a single Pydantic model
124
- argument, so the MCP schema nested every field under a required `params`
125
- object while the docs told the model to send fields directly. The result was
126
- that calls were rejected. Tools now expose their parameters directly.
127
- - **`get_analyst_recommendations` crashed** with `'RangeIndex' object has no
128
- attribute 'strftime'` due to a change in the `yfinance` recommendations
129
- format. It now reads the current format and also reports recent
130
- upgrades/downgrades.
131
- - **Dividend yield was shown 100x too high** (for example `35.00%` for AAPL).
132
- Modern `yfinance` already returns this value as a percentage, so it is no
133
- longer multiplied again.
134
- - **Markdown tables broke** when the optional `tabulate` package was missing.
135
- It is now a declared dependency, with a graceful plain-text fallback.
136
- - **Truncated JSON responses are now valid JSON** instead of being cut off
137
- mid-structure.
138
-
139
- ### Added
140
-
141
- - **`bin/cli.js` Node launcher** so `npx -y yahoo-finance-mcp-server` works. It
142
- locates Python 3.10+, creates an isolated virtual environment, installs the
143
- Python dependencies on first run, and starts the server with the protocol
144
- stream kept clean on stdout.
145
- - `tabulate` added to the dependencies.
146
-
147
- ### Changed
148
-
149
- - `package.json` `bin` now points at the Node launcher, the binary name matches
150
- the package name, and a Node engine requirement was added.
151
- - Tightened dependency floors (`yfinance>=0.2.40`, `mcp>=1.2.0`).
152
- - Replaced the deprecated Pydantic `min_items`/`max_items` with
153
- `min_length`/`max_length`.
154
- - Rewrote the README and corrected the installation and troubleshooting docs.
155
-
156
- ## [1.0.0] - 2025-11-01
157
-
158
- ### Added
159
-
160
- - Initial release of the Yahoo Finance MCP Server, distributed on npm.
161
- - Six tools: `get_stock_quote`, `get_historical_prices`, `get_company_info`,
162
- `get_financial_statements`, `compare_stocks`, and
163
- `get_analyst_recommendations`.
164
- - Markdown and JSON output formats.
165
-
166
- [1.2.0]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.2.0
167
- [1.1.1]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.1.1
168
- [1.1.0]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.1.0
169
- [1.0.0]: https://github.com/danishashko/yahoo-finance-mcp/releases/tag/v1.0.0
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.2.4",
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.2.4",
11
- "packages": [
12
- {
13
- "registryType": "npm",
14
- "registryBaseUrl": "https://registry.npmjs.org",
15
- "identifier": "yahoo-finance-mcp-server",
16
- "version": "1.2.4",
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
+ }
@@ -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 {len(df)} total*"
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. Request more data if needed or use JSON format for complete data.*"
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
- result = {
500
- "ticker": ticker,
501
- "period": period.value,
502
- "interval": interval.value,
503
- "totalRecords": len(hist),
504
- "data": hist_dict,
505
- }
506
- return truncate_json_response(
507
- json.dumps(result, indent=2, default=str),
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
- result = {
712
- "ticker": ticker,
713
- "incomeStatement": income_stmt.to_dict()
714
- if income_stmt is not None and not income_stmt.empty
715
- else {},
716
- "balanceSheet": balance_sheet.to_dict()
717
- if balance_sheet is not None and not balance_sheet.empty
718
- else {},
719
- "cashFlow": cash_flow.to_dict()
720
- if cash_flow is not None and not cash_flow.empty
721
- else {},
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
- "upgradesDowngrades": upgrades.reset_index().to_dict(orient="records")
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 with stdio transport (default for Claude Desktop)."""
1662
- mcp.run()
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__":