yahoo-finance-mcp-server 1.0.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/LICENSE +21 -0
- package/README.md +284 -0
- package/package.json +45 -0
- package/requirements.txt +8 -0
- package/yahoo_finance_mcp.py +918 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daniel Shashko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Yahoo Finance MCP Server 📈
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/yahoo-finance-mcp-server)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A Model Context Protocol (MCP) server that provides real-time stock market data through Yahoo Finance. Access stock quotes, historical prices, company information, financial statements, and analyst recommendations directly in Claude Desktop or any MCP-compatible client.
|
|
7
|
+
|
|
8
|
+
## 🎯 What Does This Do?
|
|
9
|
+
|
|
10
|
+
This MCP server gives your AI assistant real-time access to:
|
|
11
|
+
- 📊 **Real-time stock quotes** with market data
|
|
12
|
+
- 📈 **Historical price data** and performance metrics
|
|
13
|
+
- 🏢 **Company information** and business details
|
|
14
|
+
- 💰 **Financial statements** (income, balance sheet, cash flow)
|
|
15
|
+
- 🎯 **Analyst recommendations** and price targets
|
|
16
|
+
- ⚖️ **Multi-stock comparisons** side-by-side
|
|
17
|
+
|
|
18
|
+
## 🚀 Quick Start (npm)
|
|
19
|
+
|
|
20
|
+
### Install via npx (No installation required)
|
|
21
|
+
|
|
22
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on Mac or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"yahoo-finance": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "yahoo-finance-mcp-server"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That's it! The server will auto-install Python dependencies on first run.
|
|
36
|
+
|
|
37
|
+
### Or install globally
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g yahoo-finance-mcp-server
|
|
41
|
+
|
|
42
|
+
# Then use in Claude Desktop config:
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"yahoo-finance": {
|
|
46
|
+
"command": "yahoo-finance-mcp-server"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 📋 Prerequisites
|
|
53
|
+
|
|
54
|
+
- **Python 3.10+** - [Download here](https://www.python.org/downloads/)
|
|
55
|
+
- **Node.js** (for npm installation)
|
|
56
|
+
|
|
57
|
+
## 🛠️ Manual Installation (Alternative)
|
|
58
|
+
|
|
59
|
+
If you prefer to install manually:
|
|
60
|
+
|
|
61
|
+
### Step 2: Download the Server File
|
|
62
|
+
|
|
63
|
+
1. Save the `yahoo_finance_mcp.py` file to a folder on your computer
|
|
64
|
+
2. Remember where you saved it (you'll need this path)
|
|
65
|
+
|
|
66
|
+
### Step 3: Configure Claude Desktop
|
|
67
|
+
|
|
68
|
+
To use this server with Claude Desktop, you need to add it to your configuration file.
|
|
69
|
+
|
|
70
|
+
#### On Mac:
|
|
71
|
+
|
|
72
|
+
1. Open Terminal
|
|
73
|
+
2. Type: `nano ~/Library/Application\ Support/Claude/claude_desktop_config.json`
|
|
74
|
+
3. Add the following configuration (adjust the path to where you saved the file):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"yahoo-finance": {
|
|
80
|
+
"command": "python3",
|
|
81
|
+
"args": ["/path/to/yahoo_finance_mcp.py"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
4. Press `Ctrl + X`, then `Y`, then `Enter` to save
|
|
88
|
+
|
|
89
|
+
#### On Windows:
|
|
90
|
+
|
|
91
|
+
1. Open Notepad as Administrator
|
|
92
|
+
2. Open: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
93
|
+
3. Add the following configuration (adjust the path):
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"yahoo-finance": {
|
|
99
|
+
"command": "python",
|
|
100
|
+
"args": ["C:\\path\\to\\yahoo_finance_mcp.py"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
4. Save the file
|
|
107
|
+
|
|
108
|
+
**Important:** Replace `/path/to/yahoo_finance_mcp.py` with the actual path where you saved the file!
|
|
109
|
+
|
|
110
|
+
### Step 4: Restart Claude Desktop
|
|
111
|
+
|
|
112
|
+
Close Claude Desktop completely and open it again. The Yahoo Finance tools should now be available!
|
|
113
|
+
|
|
114
|
+
## ✅ Testing the Installation
|
|
115
|
+
|
|
116
|
+
Once Claude Desktop restarts, try asking:
|
|
117
|
+
|
|
118
|
+
- "What's the current price of Apple stock?"
|
|
119
|
+
- "Show me Tesla's stock performance over the last year"
|
|
120
|
+
- "Compare Apple, Microsoft, and Google stocks"
|
|
121
|
+
- "What do analysts think about Amazon?"
|
|
122
|
+
|
|
123
|
+
If everything is working, Claude will use the Yahoo Finance tools to answer these questions!
|
|
124
|
+
|
|
125
|
+
## 🔧 Available Tools
|
|
126
|
+
|
|
127
|
+
The server provides these tools:
|
|
128
|
+
|
|
129
|
+
### 1. **get_stock_quote**
|
|
130
|
+
Get current price, volume, market cap, and basic info for a stock.
|
|
131
|
+
|
|
132
|
+
Example: "What's the price of AAPL?"
|
|
133
|
+
|
|
134
|
+
### 2. **get_historical_prices**
|
|
135
|
+
Get price history over different time periods (1 day to 10 years).
|
|
136
|
+
|
|
137
|
+
Example: "Show me Microsoft's stock price over the last 6 months"
|
|
138
|
+
|
|
139
|
+
### 3. **get_company_info**
|
|
140
|
+
Get detailed company information, officers, and comprehensive statistics.
|
|
141
|
+
|
|
142
|
+
Example: "Tell me about Tesla as a company"
|
|
143
|
+
|
|
144
|
+
### 4. **get_financial_statements**
|
|
145
|
+
Get income statement, balance sheet, and cash flow data.
|
|
146
|
+
|
|
147
|
+
Example: "Show me Apple's financial statements"
|
|
148
|
+
|
|
149
|
+
### 5. **compare_stocks**
|
|
150
|
+
Compare multiple stocks side-by-side.
|
|
151
|
+
|
|
152
|
+
Example: "Compare AAPL, MSFT, and GOOGL"
|
|
153
|
+
|
|
154
|
+
### 6. **get_analyst_recommendations**
|
|
155
|
+
Get Wall Street analyst ratings and price targets.
|
|
156
|
+
|
|
157
|
+
Example: "What do analysts think about Tesla?"
|
|
158
|
+
|
|
159
|
+
## 📊 Example Questions to Ask Claude
|
|
160
|
+
|
|
161
|
+
Here are some example questions you can ask once the server is running:
|
|
162
|
+
|
|
163
|
+
**Basic Quotes:**
|
|
164
|
+
- "What's the current stock price of Apple?"
|
|
165
|
+
- "How is Tesla stock doing today?"
|
|
166
|
+
- "Show me the quote for Microsoft"
|
|
167
|
+
|
|
168
|
+
**Historical Data:**
|
|
169
|
+
- "Show me Amazon's stock performance over the last year"
|
|
170
|
+
- "What was Google's stock price history in the last 3 months?"
|
|
171
|
+
- "Get me daily prices for Netflix over the past month"
|
|
172
|
+
|
|
173
|
+
**Company Information:**
|
|
174
|
+
- "Tell me about Apple's business"
|
|
175
|
+
- "Who are the executives at Microsoft?"
|
|
176
|
+
- "What sector is Tesla in?"
|
|
177
|
+
|
|
178
|
+
**Financial Analysis:**
|
|
179
|
+
- "Show me Apple's income statement"
|
|
180
|
+
- "What's Microsoft's revenue?"
|
|
181
|
+
- "Get Tesla's balance sheet"
|
|
182
|
+
|
|
183
|
+
**Comparisons:**
|
|
184
|
+
- "Compare Apple, Microsoft, and Google stocks"
|
|
185
|
+
- "Which is better: Tesla or Ford?"
|
|
186
|
+
- "Compare the tech giants"
|
|
187
|
+
|
|
188
|
+
**Analyst Insights:**
|
|
189
|
+
- "What do analysts think about Amazon?"
|
|
190
|
+
- "What's the price target for Tesla?"
|
|
191
|
+
- "Show me recent analyst recommendations for Apple"
|
|
192
|
+
|
|
193
|
+
## 🐛 Troubleshooting
|
|
194
|
+
|
|
195
|
+
### "Command not found" or "Python not found"
|
|
196
|
+
|
|
197
|
+
**Solution:** Make sure Python is installed and in your PATH. Try using `python3` instead of `python` in the config file (especially on Mac/Linux).
|
|
198
|
+
|
|
199
|
+
### "Module not found: yfinance" or "Module not found: mcp"
|
|
200
|
+
|
|
201
|
+
**Solution:** Install the required libraries:
|
|
202
|
+
```bash
|
|
203
|
+
pip install yfinance pandas mcp
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Or on Mac/Linux:
|
|
207
|
+
```bash
|
|
208
|
+
pip3 install yfinance pandas mcp
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### "No such file or directory"
|
|
212
|
+
|
|
213
|
+
**Solution:** Double-check the path in your `claude_desktop_config.json`. Make sure:
|
|
214
|
+
- The path is correct and complete
|
|
215
|
+
- On Windows, use double backslashes (`\\`) or forward slashes (`/`)
|
|
216
|
+
- The file actually exists at that location
|
|
217
|
+
|
|
218
|
+
### Tools not showing up in Claude
|
|
219
|
+
|
|
220
|
+
**Solution:**
|
|
221
|
+
1. Make sure you saved the config file correctly
|
|
222
|
+
2. Restart Claude Desktop completely (quit and reopen)
|
|
223
|
+
3. Check that the JSON syntax is correct (no missing commas or brackets)
|
|
224
|
+
|
|
225
|
+
### "Error fetching data" messages
|
|
226
|
+
|
|
227
|
+
**Solution:**
|
|
228
|
+
- Check your internet connection
|
|
229
|
+
- Verify the ticker symbol is correct (e.g., "AAPL" not "Apple")
|
|
230
|
+
- Some stocks may have limited data available
|
|
231
|
+
- Yahoo Finance API may be temporarily down
|
|
232
|
+
|
|
233
|
+
## 🔒 Privacy & Rate Limits
|
|
234
|
+
|
|
235
|
+
- This server uses the **free** Yahoo Finance API through the yfinance library
|
|
236
|
+
- All data requests go directly to Yahoo Finance - nothing is stored
|
|
237
|
+
- Yahoo Finance has rate limits (~2,000 requests/hour per IP)
|
|
238
|
+
- This is intended for **personal use only**, not commercial applications
|
|
239
|
+
|
|
240
|
+
## 📝 Notes
|
|
241
|
+
|
|
242
|
+
- Stock tickers should be in UPPERCASE (AAPL, MSFT, TSLA, etc.)
|
|
243
|
+
- Market data may have a 15-20 minute delay for some stocks
|
|
244
|
+
- Not all data is available for every stock (especially smaller companies)
|
|
245
|
+
- Financial statements are typically available for larger public companies
|
|
246
|
+
|
|
247
|
+
## 🆘 Getting Help
|
|
248
|
+
|
|
249
|
+
If you're having trouble:
|
|
250
|
+
|
|
251
|
+
1. Double-check you followed all installation steps
|
|
252
|
+
2. Make sure Python and all libraries are installed correctly
|
|
253
|
+
3. Verify your `claude_desktop_config.json` syntax is correct
|
|
254
|
+
4. Try the troubleshooting steps above
|
|
255
|
+
|
|
256
|
+
## 📚 Additional Resources
|
|
257
|
+
|
|
258
|
+
- [Python Download](https://www.python.org/downloads/)
|
|
259
|
+
- [MCP Documentation](https://modelcontextprotocol.io/)
|
|
260
|
+
- [yfinance Documentation](https://ranaroussi.github.io/yfinance/)
|
|
261
|
+
- [Claude Desktop](https://claude.ai/download)
|
|
262
|
+
|
|
263
|
+
## ⚖️ Legal Disclaimer
|
|
264
|
+
|
|
265
|
+
This tool uses Yahoo Finance's publicly available data through the yfinance library. Yahoo!, Y!Finance, and Yahoo! Finance are registered trademarks of Yahoo, Inc. This tool is not affiliated with, endorsed by, or vetted by Yahoo, Inc.
|
|
266
|
+
|
|
267
|
+
Please refer to Yahoo!'s terms of use for details on your rights to use the data. This API is intended for personal, educational, and research purposes only.
|
|
268
|
+
|
|
269
|
+
## 🎉 You're All Set!
|
|
270
|
+
|
|
271
|
+
Once everything is configured, you can start asking Claude about stocks and financial data. Have fun exploring the markets! 📈
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 👤 Author
|
|
276
|
+
|
|
277
|
+
**Daniel Shashko**
|
|
278
|
+
- GitHub: [@danishashko](https://github.com/danishashko)
|
|
279
|
+
- LinkedIn: [daniel-shashko](https://linkedin.com/in/daniel-shashko)
|
|
280
|
+
- npm: [@danishashko](https://www.npmjs.com/~danishashko)
|
|
281
|
+
|
|
282
|
+
## 📄 License
|
|
283
|
+
|
|
284
|
+
MIT © Daniel Shashko
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yahoo-finance-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Yahoo Finance MCP Server - Real-time stock data, company info, financial statements, and market analysis via Model Context Protocol",
|
|
5
|
+
"main": "yahoo_finance_mcp.py",
|
|
6
|
+
"bin": {
|
|
7
|
+
"yahoo-finance-mcp": "./yahoo_finance_mcp.py"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "python test_installation.py"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"model-context-protocol",
|
|
15
|
+
"yahoo-finance",
|
|
16
|
+
"stocks",
|
|
17
|
+
"finance",
|
|
18
|
+
"market-data",
|
|
19
|
+
"stock-prices",
|
|
20
|
+
"financial-data",
|
|
21
|
+
"yfinance",
|
|
22
|
+
"claude",
|
|
23
|
+
"ai",
|
|
24
|
+
"llm"
|
|
25
|
+
],
|
|
26
|
+
"author": "Daniel Shashko",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/danishashko/yahoo-finance-mcp.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/danishashko/yahoo-finance-mcp#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/danishashko/yahoo-finance-mcp/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"python": ">=3.10"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"yahoo_finance_mcp.py",
|
|
41
|
+
"requirements.txt",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
]
|
|
45
|
+
}
|
package/requirements.txt
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yahoo Finance MCP Server
|
|
3
|
+
|
|
4
|
+
This MCP server provides tools to access financial market data from Yahoo Finance,
|
|
5
|
+
including stock prices, company information, financial statements, and market analysis.
|
|
6
|
+
|
|
7
|
+
Built with FastMCP and yfinance library.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
12
|
+
from typing import Optional, List, Dict, Any, Literal
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
import json
|
|
16
|
+
import yfinance as yf
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
# Initialize the MCP server
|
|
20
|
+
mcp = FastMCP("yahoo_finance_mcp")
|
|
21
|
+
|
|
22
|
+
# Constants
|
|
23
|
+
CHARACTER_LIMIT = 25000 # Maximum response size in characters
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# ENUMS AND INPUT MODELS
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
class ResponseFormat(str, Enum):
|
|
30
|
+
"""Output format for tool responses."""
|
|
31
|
+
MARKDOWN = "markdown"
|
|
32
|
+
JSON = "json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Period(str, Enum):
|
|
36
|
+
"""Time periods for historical data."""
|
|
37
|
+
ONE_DAY = "1d"
|
|
38
|
+
FIVE_DAYS = "5d"
|
|
39
|
+
ONE_MONTH = "1mo"
|
|
40
|
+
THREE_MONTHS = "3mo"
|
|
41
|
+
SIX_MONTHS = "6mo"
|
|
42
|
+
ONE_YEAR = "1y"
|
|
43
|
+
TWO_YEARS = "2y"
|
|
44
|
+
FIVE_YEARS = "5y"
|
|
45
|
+
TEN_YEARS = "10y"
|
|
46
|
+
YTD = "ytd"
|
|
47
|
+
MAX = "max"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Interval(str, Enum):
|
|
51
|
+
"""Data intervals for historical prices."""
|
|
52
|
+
ONE_MINUTE = "1m"
|
|
53
|
+
TWO_MINUTES = "2m"
|
|
54
|
+
FIVE_MINUTES = "5m"
|
|
55
|
+
FIFTEEN_MINUTES = "15m"
|
|
56
|
+
THIRTY_MINUTES = "30m"
|
|
57
|
+
SIXTY_MINUTES = "60m"
|
|
58
|
+
NINETY_MINUTES = "90m"
|
|
59
|
+
ONE_HOUR = "1h"
|
|
60
|
+
ONE_DAY = "1d"
|
|
61
|
+
FIVE_DAYS = "5d"
|
|
62
|
+
ONE_WEEK = "1wk"
|
|
63
|
+
ONE_MONTH = "1mo"
|
|
64
|
+
THREE_MONTHS = "3mo"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# UTILITY FUNCTIONS
|
|
69
|
+
# ============================================================================
|
|
70
|
+
|
|
71
|
+
def safe_get(data: Dict[str, Any], key: str, default: Any = "N/A") -> Any:
|
|
72
|
+
"""Safely get a value from a dictionary."""
|
|
73
|
+
return data.get(key, default)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_currency(value: Any) -> str:
|
|
77
|
+
"""Format a value as currency."""
|
|
78
|
+
if value is None or value == "N/A":
|
|
79
|
+
return "N/A"
|
|
80
|
+
try:
|
|
81
|
+
return f"${value:,.2f}"
|
|
82
|
+
except (ValueError, TypeError):
|
|
83
|
+
return str(value)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def format_large_number(value: Any) -> str:
|
|
87
|
+
"""Format large numbers with K, M, B suffixes."""
|
|
88
|
+
if value is None or value == "N/A":
|
|
89
|
+
return "N/A"
|
|
90
|
+
try:
|
|
91
|
+
num = float(value)
|
|
92
|
+
if abs(num) >= 1e9:
|
|
93
|
+
return f"${num/1e9:.2f}B"
|
|
94
|
+
elif abs(num) >= 1e6:
|
|
95
|
+
return f"${num/1e6:.2f}M"
|
|
96
|
+
elif abs(num) >= 1e3:
|
|
97
|
+
return f"${num/1e3:.2f}K"
|
|
98
|
+
else:
|
|
99
|
+
return f"${num:.2f}"
|
|
100
|
+
except (ValueError, TypeError):
|
|
101
|
+
return str(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def format_percentage(value: Any) -> str:
|
|
105
|
+
"""Format a value as a percentage."""
|
|
106
|
+
if value is None or value == "N/A":
|
|
107
|
+
return "N/A"
|
|
108
|
+
try:
|
|
109
|
+
return f"{float(value)*100:.2f}%"
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
return str(value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def dataframe_to_markdown(df: pd.DataFrame, max_rows: int = 50) -> str:
|
|
115
|
+
"""Convert a pandas DataFrame to markdown format."""
|
|
116
|
+
if df.empty:
|
|
117
|
+
return "No data available"
|
|
118
|
+
|
|
119
|
+
# Limit rows
|
|
120
|
+
if len(df) > max_rows:
|
|
121
|
+
df = df.head(max_rows)
|
|
122
|
+
truncated_msg = f"\n\n*Showing first {max_rows} rows of {len(df)} total*"
|
|
123
|
+
else:
|
|
124
|
+
truncated_msg = ""
|
|
125
|
+
|
|
126
|
+
return df.to_markdown() + truncated_msg
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def truncate_response(response: str, message: str = "") -> str:
|
|
130
|
+
"""Truncate response if it exceeds CHARACTER_LIMIT."""
|
|
131
|
+
if len(response) <= CHARACTER_LIMIT:
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
truncated = response[:CHARACTER_LIMIT]
|
|
135
|
+
truncation_msg = f"\n\n⚠️ Response truncated at {CHARACTER_LIMIT} characters. {message}"
|
|
136
|
+
return truncated + truncation_msg
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ============================================================================
|
|
140
|
+
# TOOL INPUT MODELS
|
|
141
|
+
# ============================================================================
|
|
142
|
+
|
|
143
|
+
class TickerInput(BaseModel):
|
|
144
|
+
"""Input model for single ticker operations."""
|
|
145
|
+
model_config = ConfigDict(
|
|
146
|
+
str_strip_whitespace=True,
|
|
147
|
+
validate_assignment=True,
|
|
148
|
+
extra='forbid'
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
ticker: str = Field(
|
|
152
|
+
...,
|
|
153
|
+
description="Stock ticker symbol (e.g., 'AAPL' for Apple, 'MSFT' for Microsoft, 'TSLA' for Tesla)",
|
|
154
|
+
min_length=1,
|
|
155
|
+
max_length=10
|
|
156
|
+
)
|
|
157
|
+
response_format: ResponseFormat = Field(
|
|
158
|
+
default=ResponseFormat.MARKDOWN,
|
|
159
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@field_validator('ticker')
|
|
163
|
+
@classmethod
|
|
164
|
+
def uppercase_ticker(cls, v: str) -> str:
|
|
165
|
+
"""Convert ticker to uppercase."""
|
|
166
|
+
return v.upper().strip()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class HistoricalPriceInput(BaseModel):
|
|
170
|
+
"""Input model for historical price data."""
|
|
171
|
+
model_config = ConfigDict(
|
|
172
|
+
str_strip_whitespace=True,
|
|
173
|
+
validate_assignment=True,
|
|
174
|
+
extra='forbid'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
ticker: str = Field(
|
|
178
|
+
...,
|
|
179
|
+
description="Stock ticker symbol (e.g., 'AAPL', 'GOOGL', 'MSFT')",
|
|
180
|
+
min_length=1,
|
|
181
|
+
max_length=10
|
|
182
|
+
)
|
|
183
|
+
period: Period = Field(
|
|
184
|
+
default=Period.ONE_MONTH,
|
|
185
|
+
description="Time period for historical data (e.g., '1mo' for 1 month, '1y' for 1 year)"
|
|
186
|
+
)
|
|
187
|
+
interval: Interval = Field(
|
|
188
|
+
default=Interval.ONE_DAY,
|
|
189
|
+
description="Data interval (e.g., '1d' for daily, '1h' for hourly)"
|
|
190
|
+
)
|
|
191
|
+
response_format: ResponseFormat = Field(
|
|
192
|
+
default=ResponseFormat.MARKDOWN,
|
|
193
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@field_validator('ticker')
|
|
197
|
+
@classmethod
|
|
198
|
+
def uppercase_ticker(cls, v: str) -> str:
|
|
199
|
+
return v.upper().strip()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class MultiTickerInput(BaseModel):
|
|
203
|
+
"""Input model for multiple ticker operations."""
|
|
204
|
+
model_config = ConfigDict(
|
|
205
|
+
str_strip_whitespace=True,
|
|
206
|
+
validate_assignment=True,
|
|
207
|
+
extra='forbid'
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
tickers: List[str] = Field(
|
|
211
|
+
...,
|
|
212
|
+
description="List of stock ticker symbols (e.g., ['AAPL', 'MSFT', 'GOOGL'])",
|
|
213
|
+
min_items=1,
|
|
214
|
+
max_items=20
|
|
215
|
+
)
|
|
216
|
+
response_format: ResponseFormat = Field(
|
|
217
|
+
default=ResponseFormat.MARKDOWN,
|
|
218
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@field_validator('tickers')
|
|
222
|
+
@classmethod
|
|
223
|
+
def uppercase_tickers(cls, v: List[str]) -> List[str]:
|
|
224
|
+
return [ticker.upper().strip() for ticker in v]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class QuoteComparisonInput(BaseModel):
|
|
228
|
+
"""Input model for comparing multiple stock quotes."""
|
|
229
|
+
model_config = ConfigDict(
|
|
230
|
+
str_strip_whitespace=True,
|
|
231
|
+
validate_assignment=True,
|
|
232
|
+
extra='forbid'
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
tickers: List[str] = Field(
|
|
236
|
+
...,
|
|
237
|
+
description="List of stock ticker symbols to compare (e.g., ['AAPL', 'MSFT', 'GOOGL'])",
|
|
238
|
+
min_items=2,
|
|
239
|
+
max_items=10
|
|
240
|
+
)
|
|
241
|
+
response_format: ResponseFormat = Field(
|
|
242
|
+
default=ResponseFormat.MARKDOWN,
|
|
243
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@field_validator('tickers')
|
|
247
|
+
@classmethod
|
|
248
|
+
def uppercase_tickers(cls, v: List[str]) -> List[str]:
|
|
249
|
+
return [ticker.upper().strip() for ticker in v]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ============================================================================
|
|
253
|
+
# MCP TOOLS
|
|
254
|
+
# ============================================================================
|
|
255
|
+
|
|
256
|
+
@mcp.tool(
|
|
257
|
+
name="get_stock_quote",
|
|
258
|
+
annotations={
|
|
259
|
+
"title": "Get Current Stock Quote",
|
|
260
|
+
"readOnlyHint": True,
|
|
261
|
+
"destructiveHint": False,
|
|
262
|
+
"idempotentHint": True,
|
|
263
|
+
"openWorldHint": True
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
async def get_stock_quote(params: TickerInput) -> str:
|
|
267
|
+
"""Get current stock quote with real-time price, volume, and market data.
|
|
268
|
+
|
|
269
|
+
This tool retrieves the latest stock quote including current price, day's range,
|
|
270
|
+
trading volume, market cap, and other key metrics for a given ticker symbol.
|
|
271
|
+
|
|
272
|
+
Use this tool when:
|
|
273
|
+
- User wants current/latest stock price
|
|
274
|
+
- User asks "what's the price of [stock]"
|
|
275
|
+
- User wants to know if market is open
|
|
276
|
+
- User wants basic stock information
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
params (TickerInput): Contains:
|
|
280
|
+
- ticker (str): Stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA')
|
|
281
|
+
- response_format (ResponseFormat): 'markdown' or 'json'
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
str: Current stock quote in requested format (markdown or JSON)
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
Input: {"ticker": "AAPL", "response_format": "markdown"}
|
|
288
|
+
Output: Formatted markdown with current price, volume, market cap, etc.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
ticker_obj = yf.Ticker(params.ticker)
|
|
292
|
+
info = ticker_obj.info
|
|
293
|
+
|
|
294
|
+
# Get fast info for real-time data
|
|
295
|
+
try:
|
|
296
|
+
fast_info = ticker_obj.fast_info
|
|
297
|
+
current_price = fast_info.get('lastPrice', safe_get(info, 'currentPrice'))
|
|
298
|
+
previous_close = fast_info.get('previousClose', safe_get(info, 'previousClose'))
|
|
299
|
+
except Exception:
|
|
300
|
+
current_price = safe_get(info, 'currentPrice')
|
|
301
|
+
previous_close = safe_get(info, 'previousClose')
|
|
302
|
+
|
|
303
|
+
# Calculate change
|
|
304
|
+
if current_price != "N/A" and previous_close != "N/A":
|
|
305
|
+
try:
|
|
306
|
+
change = current_price - previous_close
|
|
307
|
+
change_pct = (change / previous_close) * 100
|
|
308
|
+
except (TypeError, ZeroDivisionError):
|
|
309
|
+
change = "N/A"
|
|
310
|
+
change_pct = "N/A"
|
|
311
|
+
else:
|
|
312
|
+
change = "N/A"
|
|
313
|
+
change_pct = "N/A"
|
|
314
|
+
|
|
315
|
+
if params.response_format == ResponseFormat.MARKDOWN:
|
|
316
|
+
# Format as markdown
|
|
317
|
+
result = f"# {safe_get(info, 'longName', params.ticker)} ({params.ticker})\n\n"
|
|
318
|
+
result += f"**Current Price:** {format_currency(current_price)}\n"
|
|
319
|
+
|
|
320
|
+
if change != "N/A":
|
|
321
|
+
change_symbol = "🔺" if change >= 0 else "🔻"
|
|
322
|
+
result += f"**Change:** {change_symbol} {format_currency(change)} ({change_pct:.2f}%)\n"
|
|
323
|
+
|
|
324
|
+
result += f"\n## Market Data\n"
|
|
325
|
+
result += f"- **Previous Close:** {format_currency(previous_close)}\n"
|
|
326
|
+
result += f"- **Open:** {format_currency(safe_get(info, 'open'))}\n"
|
|
327
|
+
result += f"- **Day's Range:** {format_currency(safe_get(info, 'dayLow'))} - {format_currency(safe_get(info, 'dayHigh'))}\n"
|
|
328
|
+
result += f"- **52 Week Range:** {format_currency(safe_get(info, 'fiftyTwoWeekLow'))} - {format_currency(safe_get(info, 'fiftyTwoWeekHigh'))}\n"
|
|
329
|
+
result += f"- **Volume:** {safe_get(info, 'volume'):,}\n" if safe_get(info, 'volume') != "N/A" else f"- **Volume:** N/A\n"
|
|
330
|
+
result += f"- **Avg Volume:** {safe_get(info, 'averageVolume'):,}\n" if safe_get(info, 'averageVolume') != "N/A" else f"- **Avg Volume:** N/A\n"
|
|
331
|
+
result += f"- **Market Cap:** {format_large_number(safe_get(info, 'marketCap'))}\n"
|
|
332
|
+
result += f"- **Beta:** {safe_get(info, 'beta')}\n"
|
|
333
|
+
result += f"- **PE Ratio:** {safe_get(info, 'trailingPE')}\n"
|
|
334
|
+
result += f"- **EPS:** {format_currency(safe_get(info, 'trailingEps'))}\n"
|
|
335
|
+
result += f"- **Dividend Yield:** {format_percentage(safe_get(info, 'dividendYield'))}\n"
|
|
336
|
+
|
|
337
|
+
result += f"\n## Company Info\n"
|
|
338
|
+
result += f"- **Sector:** {safe_get(info, 'sector')}\n"
|
|
339
|
+
result += f"- **Industry:** {safe_get(info, 'industry')}\n"
|
|
340
|
+
result += f"- **Website:** {safe_get(info, 'website')}\n"
|
|
341
|
+
|
|
342
|
+
return truncate_response(result, "Use get_company_info for more detailed information.")
|
|
343
|
+
else:
|
|
344
|
+
# JSON format
|
|
345
|
+
result = {
|
|
346
|
+
"ticker": params.ticker,
|
|
347
|
+
"longName": safe_get(info, 'longName'),
|
|
348
|
+
"currentPrice": current_price,
|
|
349
|
+
"previousClose": previous_close,
|
|
350
|
+
"change": change,
|
|
351
|
+
"changePercent": change_pct,
|
|
352
|
+
"open": safe_get(info, 'open'),
|
|
353
|
+
"dayLow": safe_get(info, 'dayLow'),
|
|
354
|
+
"dayHigh": safe_get(info, 'dayHigh'),
|
|
355
|
+
"fiftyTwoWeekLow": safe_get(info, 'fiftyTwoWeekLow'),
|
|
356
|
+
"fiftyTwoWeekHigh": safe_get(info, 'fiftyTwoWeekHigh'),
|
|
357
|
+
"volume": safe_get(info, 'volume'),
|
|
358
|
+
"averageVolume": safe_get(info, 'averageVolume'),
|
|
359
|
+
"marketCap": safe_get(info, 'marketCap'),
|
|
360
|
+
"beta": safe_get(info, 'beta'),
|
|
361
|
+
"trailingPE": safe_get(info, 'trailingPE'),
|
|
362
|
+
"trailingEps": safe_get(info, 'trailingEps'),
|
|
363
|
+
"dividendYield": safe_get(info, 'dividendYield'),
|
|
364
|
+
"sector": safe_get(info, 'sector'),
|
|
365
|
+
"industry": safe_get(info, 'industry'),
|
|
366
|
+
"website": safe_get(info, 'website')
|
|
367
|
+
}
|
|
368
|
+
return json.dumps(result, indent=2)
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
error_msg = f"Error fetching quote for {params.ticker}: {str(e)}\n\n"
|
|
372
|
+
error_msg += "**Troubleshooting:**\n"
|
|
373
|
+
error_msg += "- Verify the ticker symbol is correct\n"
|
|
374
|
+
error_msg += "- Check if the market is open (some data may be delayed)\n"
|
|
375
|
+
error_msg += "- Try again in a moment if it's a temporary issue"
|
|
376
|
+
return error_msg
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@mcp.tool(
|
|
380
|
+
name="get_historical_prices",
|
|
381
|
+
annotations={
|
|
382
|
+
"title": "Get Historical Stock Prices",
|
|
383
|
+
"readOnlyHint": True,
|
|
384
|
+
"destructiveHint": False,
|
|
385
|
+
"idempotentHint": True,
|
|
386
|
+
"openWorldHint": True
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
async def get_historical_prices(params: HistoricalPriceInput) -> str:
|
|
390
|
+
"""Get historical stock price data with OHLCV (Open, High, Low, Close, Volume).
|
|
391
|
+
|
|
392
|
+
This tool retrieves historical price data for technical analysis, charting,
|
|
393
|
+
and trend analysis. Data includes Open, High, Low, Close prices and Volume.
|
|
394
|
+
|
|
395
|
+
Use this tool when:
|
|
396
|
+
- User wants to see price history/trends
|
|
397
|
+
- User asks "how has [stock] performed over [time period]"
|
|
398
|
+
- User wants data for charting or analysis
|
|
399
|
+
- User wants to compare historical prices
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
params (HistoricalPriceInput): Contains:
|
|
403
|
+
- ticker (str): Stock ticker symbol
|
|
404
|
+
- period (Period): Time period ('1mo', '1y', '5y', etc.)
|
|
405
|
+
- interval (Interval): Data interval ('1d', '1h', etc.)
|
|
406
|
+
- response_format (ResponseFormat): 'markdown' or 'json'
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
str: Historical price data in requested format
|
|
410
|
+
|
|
411
|
+
Example:
|
|
412
|
+
Input: {"ticker": "AAPL", "period": "1mo", "interval": "1d"}
|
|
413
|
+
Output: Daily OHLCV data for the past month
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
ticker_obj = yf.Ticker(params.ticker)
|
|
417
|
+
hist = ticker_obj.history(period=params.period.value, interval=params.interval.value)
|
|
418
|
+
|
|
419
|
+
if hist.empty:
|
|
420
|
+
return f"No historical data available for {params.ticker} with period={params.period.value} and interval={params.interval.value}"
|
|
421
|
+
|
|
422
|
+
if params.response_format == ResponseFormat.MARKDOWN:
|
|
423
|
+
result = f"# Historical Prices: {params.ticker}\n\n"
|
|
424
|
+
result += f"**Period:** {params.period.value} | **Interval:** {params.interval.value}\n\n"
|
|
425
|
+
result += f"**Date Range:** {hist.index[0].strftime('%Y-%m-%d')} to {hist.index[-1].strftime('%Y-%m-%d')}\n"
|
|
426
|
+
result += f"**Total Records:** {len(hist)}\n\n"
|
|
427
|
+
|
|
428
|
+
# Summary statistics
|
|
429
|
+
result += "## Summary Statistics\n\n"
|
|
430
|
+
result += f"- **Highest Close:** {format_currency(hist['Close'].max())} on {hist['Close'].idxmax().strftime('%Y-%m-%d')}\n"
|
|
431
|
+
result += f"- **Lowest Close:** {format_currency(hist['Close'].min())} on {hist['Close'].idxmin().strftime('%Y-%m-%d')}\n"
|
|
432
|
+
result += f"- **Average Close:** {format_currency(hist['Close'].mean())}\n"
|
|
433
|
+
result += f"- **Average Volume:** {hist['Volume'].mean():,.0f}\n"
|
|
434
|
+
|
|
435
|
+
# Calculate return
|
|
436
|
+
if len(hist) > 1:
|
|
437
|
+
start_price = hist['Close'].iloc[0]
|
|
438
|
+
end_price = hist['Close'].iloc[-1]
|
|
439
|
+
total_return = ((end_price - start_price) / start_price) * 100
|
|
440
|
+
result += f"- **Total Return:** {total_return:.2f}%\n"
|
|
441
|
+
|
|
442
|
+
result += "\n## Recent Data\n\n"
|
|
443
|
+
# Show last 10 records
|
|
444
|
+
recent_data = hist.tail(10).copy()
|
|
445
|
+
recent_data.index = recent_data.index.strftime('%Y-%m-%d %H:%M')
|
|
446
|
+
result += dataframe_to_markdown(recent_data)
|
|
447
|
+
|
|
448
|
+
if len(hist) > 10:
|
|
449
|
+
result += f"\n\n*Showing last 10 of {len(hist)} records. Request more data if needed or use JSON format for complete data.*"
|
|
450
|
+
|
|
451
|
+
return truncate_response(result, "Request smaller time period or use JSON format for complete data.")
|
|
452
|
+
else:
|
|
453
|
+
# JSON format - return all data
|
|
454
|
+
hist_dict = hist.reset_index().to_dict(orient='records')
|
|
455
|
+
# Convert timestamps to strings
|
|
456
|
+
for record in hist_dict:
|
|
457
|
+
if 'Date' in record:
|
|
458
|
+
record['Date'] = record['Date'].isoformat()
|
|
459
|
+
elif 'Datetime' in record:
|
|
460
|
+
record['Datetime'] = record['Datetime'].isoformat()
|
|
461
|
+
|
|
462
|
+
result = {
|
|
463
|
+
"ticker": params.ticker,
|
|
464
|
+
"period": params.period.value,
|
|
465
|
+
"interval": params.interval.value,
|
|
466
|
+
"totalRecords": len(hist),
|
|
467
|
+
"data": hist_dict
|
|
468
|
+
}
|
|
469
|
+
return truncate_response(json.dumps(result, indent=2), "Consider using a shorter period.")
|
|
470
|
+
|
|
471
|
+
except Exception as e:
|
|
472
|
+
error_msg = f"Error fetching historical prices for {params.ticker}: {str(e)}\n\n"
|
|
473
|
+
error_msg += "**Troubleshooting:**\n"
|
|
474
|
+
error_msg += "- Verify ticker symbol is correct\n"
|
|
475
|
+
error_msg += "- Some intervals may not be available for all periods\n"
|
|
476
|
+
error_msg += "- Try a different period/interval combination"
|
|
477
|
+
return error_msg
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@mcp.tool(
|
|
481
|
+
name="get_company_info",
|
|
482
|
+
annotations={
|
|
483
|
+
"title": "Get Detailed Company Information",
|
|
484
|
+
"readOnlyHint": True,
|
|
485
|
+
"destructiveHint": False,
|
|
486
|
+
"idempotentHint": True,
|
|
487
|
+
"openWorldHint": True
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
async def get_company_info(params: TickerInput) -> str:
|
|
491
|
+
"""Get comprehensive company information including business description, officers, and key statistics.
|
|
492
|
+
|
|
493
|
+
This tool retrieves detailed company information including business summary,
|
|
494
|
+
company officers, address, employee count, and comprehensive financial metrics.
|
|
495
|
+
|
|
496
|
+
Use this tool when:
|
|
497
|
+
- User wants to know "what does [company] do"
|
|
498
|
+
- User asks about company leadership/executives
|
|
499
|
+
- User wants detailed company background
|
|
500
|
+
- User needs comprehensive financial statistics
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
params (TickerInput): Contains:
|
|
504
|
+
- ticker (str): Stock ticker symbol
|
|
505
|
+
- response_format (ResponseFormat): 'markdown' or 'json'
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
str: Detailed company information in requested format
|
|
509
|
+
|
|
510
|
+
Example:
|
|
511
|
+
Input: {"ticker": "AAPL", "response_format": "markdown"}
|
|
512
|
+
Output: Full company profile with description, officers, statistics
|
|
513
|
+
"""
|
|
514
|
+
try:
|
|
515
|
+
ticker_obj = yf.Ticker(params.ticker)
|
|
516
|
+
info = ticker_obj.info
|
|
517
|
+
|
|
518
|
+
if params.response_format == ResponseFormat.MARKDOWN:
|
|
519
|
+
result = f"# {safe_get(info, 'longName', params.ticker)} ({params.ticker})\n\n"
|
|
520
|
+
|
|
521
|
+
# Business Summary
|
|
522
|
+
result += "## Business Summary\n\n"
|
|
523
|
+
summary = safe_get(info, 'longBusinessSummary', 'No description available')
|
|
524
|
+
result += f"{summary}\n\n"
|
|
525
|
+
|
|
526
|
+
# Company Details
|
|
527
|
+
result += "## Company Details\n\n"
|
|
528
|
+
result += f"- **Sector:** {safe_get(info, 'sector')}\n"
|
|
529
|
+
result += f"- **Industry:** {safe_get(info, 'industry')}\n"
|
|
530
|
+
result += f"- **Full Time Employees:** {safe_get(info, 'fullTimeEmployees'):,}\n" if safe_get(info, 'fullTimeEmployees') != "N/A" else f"- **Full Time Employees:** N/A\n"
|
|
531
|
+
result += f"- **Website:** {safe_get(info, 'website')}\n"
|
|
532
|
+
result += f"- **Address:** {safe_get(info, 'address1')}, {safe_get(info, 'city')}, {safe_get(info, 'state')} {safe_get(info, 'zip')}\n"
|
|
533
|
+
result += f"- **Country:** {safe_get(info, 'country')}\n"
|
|
534
|
+
result += f"- **Phone:** {safe_get(info, 'phone')}\n\n"
|
|
535
|
+
|
|
536
|
+
# Company Officers
|
|
537
|
+
officers = safe_get(info, 'companyOfficers', [])
|
|
538
|
+
if officers and isinstance(officers, list):
|
|
539
|
+
result += "## Key Executives\n\n"
|
|
540
|
+
for officer in officers[:5]: # Show top 5
|
|
541
|
+
name = officer.get('name', 'N/A')
|
|
542
|
+
title = officer.get('title', 'N/A')
|
|
543
|
+
pay = officer.get('totalPay')
|
|
544
|
+
result += f"- **{name}** - {title}"
|
|
545
|
+
if pay:
|
|
546
|
+
result += f" (Compensation: {format_large_number(pay)})"
|
|
547
|
+
result += "\n"
|
|
548
|
+
result += "\n"
|
|
549
|
+
|
|
550
|
+
# Key Statistics
|
|
551
|
+
result += "## Key Statistics\n\n"
|
|
552
|
+
result += f"- **Market Cap:** {format_large_number(safe_get(info, 'marketCap'))}\n"
|
|
553
|
+
result += f"- **Enterprise Value:** {format_large_number(safe_get(info, 'enterpriseValue'))}\n"
|
|
554
|
+
result += f"- **PE Ratio (Trailing):** {safe_get(info, 'trailingPE')}\n"
|
|
555
|
+
result += f"- **PE Ratio (Forward):** {safe_get(info, 'forwardPE')}\n"
|
|
556
|
+
result += f"- **PEG Ratio:** {safe_get(info, 'pegRatio')}\n"
|
|
557
|
+
result += f"- **Price to Book:** {safe_get(info, 'priceToBook')}\n"
|
|
558
|
+
result += f"- **Price to Sales:** {safe_get(info, 'priceToSalesTrailing12Months')}\n"
|
|
559
|
+
result += f"- **EPS (Trailing):** {format_currency(safe_get(info, 'trailingEps'))}\n"
|
|
560
|
+
result += f"- **EPS (Forward):** {format_currency(safe_get(info, 'forwardEps'))}\n"
|
|
561
|
+
result += f"- **Dividend Rate:** {format_currency(safe_get(info, 'dividendRate'))}\n"
|
|
562
|
+
result += f"- **Dividend Yield:** {format_percentage(safe_get(info, 'dividendYield'))}\n"
|
|
563
|
+
result += f"- **Ex-Dividend Date:** {safe_get(info, 'exDividendDate')}\n"
|
|
564
|
+
result += f"- **Beta:** {safe_get(info, 'beta')}\n"
|
|
565
|
+
result += f"- **52 Week High:** {format_currency(safe_get(info, 'fiftyTwoWeekHigh'))}\n"
|
|
566
|
+
result += f"- **52 Week Low:** {format_currency(safe_get(info, 'fiftyTwoWeekLow'))}\n"
|
|
567
|
+
result += f"- **50 Day Avg:** {format_currency(safe_get(info, 'fiftyDayAverage'))}\n"
|
|
568
|
+
result += f"- **200 Day Avg:** {format_currency(safe_get(info, 'twoHundredDayAverage'))}\n"
|
|
569
|
+
result += f"- **Shares Outstanding:** {safe_get(info, 'sharesOutstanding'):,}\n" if safe_get(info, 'sharesOutstanding') != "N/A" else f"- **Shares Outstanding:** N/A\n"
|
|
570
|
+
result += f"- **Float Shares:** {safe_get(info, 'floatShares'):,}\n" if safe_get(info, 'floatShares') != "N/A" else f"- **Float Shares:** N/A\n"
|
|
571
|
+
|
|
572
|
+
# Financial Highlights
|
|
573
|
+
result += "\n## Financial Highlights\n\n"
|
|
574
|
+
result += f"- **Revenue:** {format_large_number(safe_get(info, 'totalRevenue'))}\n"
|
|
575
|
+
result += f"- **Revenue Per Share:** {format_currency(safe_get(info, 'revenuePerShare'))}\n"
|
|
576
|
+
result += f"- **Profit Margin:** {format_percentage(safe_get(info, 'profitMargins'))}\n"
|
|
577
|
+
result += f"- **Operating Margin:** {format_percentage(safe_get(info, 'operatingMargins'))}\n"
|
|
578
|
+
result += f"- **ROA (Return on Assets):** {format_percentage(safe_get(info, 'returnOnAssets'))}\n"
|
|
579
|
+
result += f"- **ROE (Return on Equity):** {format_percentage(safe_get(info, 'returnOnEquity'))}\n"
|
|
580
|
+
result += f"- **Total Cash:** {format_large_number(safe_get(info, 'totalCash'))}\n"
|
|
581
|
+
result += f"- **Total Debt:** {format_large_number(safe_get(info, 'totalDebt'))}\n"
|
|
582
|
+
result += f"- **Debt to Equity:** {safe_get(info, 'debtToEquity')}\n"
|
|
583
|
+
result += f"- **Current Ratio:** {safe_get(info, 'currentRatio')}\n"
|
|
584
|
+
result += f"- **Free Cash Flow:** {format_large_number(safe_get(info, 'freeCashflow'))}\n"
|
|
585
|
+
|
|
586
|
+
return truncate_response(result, "")
|
|
587
|
+
else:
|
|
588
|
+
# JSON format - return full info dict
|
|
589
|
+
return truncate_response(json.dumps(info, indent=2, default=str), "")
|
|
590
|
+
|
|
591
|
+
except Exception as e:
|
|
592
|
+
error_msg = f"Error fetching company info for {params.ticker}: {str(e)}\n\n"
|
|
593
|
+
error_msg += "**Troubleshooting:**\n"
|
|
594
|
+
error_msg += "- Verify ticker symbol is correct\n"
|
|
595
|
+
error_msg += "- Some data may not be available for all companies"
|
|
596
|
+
return error_msg
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@mcp.tool(
|
|
600
|
+
name="get_financial_statements",
|
|
601
|
+
annotations={
|
|
602
|
+
"title": "Get Company Financial Statements",
|
|
603
|
+
"readOnlyHint": True,
|
|
604
|
+
"destructiveHint": False,
|
|
605
|
+
"idempotentHint": True,
|
|
606
|
+
"openWorldHint": True
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
async def get_financial_statements(params: TickerInput) -> str:
|
|
610
|
+
"""Get comprehensive financial statements including income statement, balance sheet, and cash flow.
|
|
611
|
+
|
|
612
|
+
This tool retrieves all three major financial statements with quarterly and annual data.
|
|
613
|
+
Essential for fundamental analysis and understanding company financials.
|
|
614
|
+
|
|
615
|
+
Use this tool when:
|
|
616
|
+
- User wants to see revenue, earnings, expenses
|
|
617
|
+
- User asks about balance sheet items (assets, liabilities)
|
|
618
|
+
- User wants cash flow information
|
|
619
|
+
- User needs data for financial analysis
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
params (TickerInput): Contains:
|
|
623
|
+
- ticker (str): Stock ticker symbol
|
|
624
|
+
- response_format (ResponseFormat): 'markdown' or 'json'
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
str: Financial statements in requested format
|
|
628
|
+
|
|
629
|
+
Example:
|
|
630
|
+
Input: {"ticker": "AAPL", "response_format": "markdown"}
|
|
631
|
+
Output: Income statement, balance sheet, and cash flow data
|
|
632
|
+
"""
|
|
633
|
+
try:
|
|
634
|
+
ticker_obj = yf.Ticker(params.ticker)
|
|
635
|
+
|
|
636
|
+
# Get financial statements
|
|
637
|
+
income_stmt = ticker_obj.income_stmt
|
|
638
|
+
balance_sheet = ticker_obj.balance_sheet
|
|
639
|
+
cash_flow = ticker_obj.cashflow
|
|
640
|
+
|
|
641
|
+
if params.response_format == ResponseFormat.MARKDOWN:
|
|
642
|
+
result = f"# Financial Statements: {params.ticker}\n\n"
|
|
643
|
+
|
|
644
|
+
# Income Statement
|
|
645
|
+
if not income_stmt.empty:
|
|
646
|
+
result += "## Income Statement (Annual)\n\n"
|
|
647
|
+
result += dataframe_to_markdown(income_stmt, max_rows=30)
|
|
648
|
+
result += "\n\n"
|
|
649
|
+
|
|
650
|
+
# Balance Sheet
|
|
651
|
+
if not balance_sheet.empty:
|
|
652
|
+
result += "## Balance Sheet (Annual)\n\n"
|
|
653
|
+
result += dataframe_to_markdown(balance_sheet, max_rows=30)
|
|
654
|
+
result += "\n\n"
|
|
655
|
+
|
|
656
|
+
# Cash Flow Statement
|
|
657
|
+
if not cash_flow.empty:
|
|
658
|
+
result += "## Cash Flow Statement (Annual)\n\n"
|
|
659
|
+
result += dataframe_to_markdown(cash_flow, max_rows=30)
|
|
660
|
+
result += "\n\n"
|
|
661
|
+
|
|
662
|
+
result += "*Note: Use JSON format for quarterly statements or complete data export.*"
|
|
663
|
+
|
|
664
|
+
return truncate_response(result, "Request specific statement types separately if needed.")
|
|
665
|
+
else:
|
|
666
|
+
# JSON format
|
|
667
|
+
result = {
|
|
668
|
+
"ticker": params.ticker,
|
|
669
|
+
"incomeStatement": income_stmt.to_dict() if not income_stmt.empty else {},
|
|
670
|
+
"balanceSheet": balance_sheet.to_dict() if not balance_sheet.empty else {},
|
|
671
|
+
"cashFlow": cash_flow.to_dict() if not cash_flow.empty else {}
|
|
672
|
+
}
|
|
673
|
+
return truncate_response(json.dumps(result, indent=2, default=str), "")
|
|
674
|
+
|
|
675
|
+
except Exception as e:
|
|
676
|
+
error_msg = f"Error fetching financial statements for {params.ticker}: {str(e)}\n\n"
|
|
677
|
+
error_msg += "**Troubleshooting:**\n"
|
|
678
|
+
error_msg += "- Verify ticker symbol is correct\n"
|
|
679
|
+
error_msg += "- Financial statements may not be available for all companies\n"
|
|
680
|
+
error_msg += "- Try get_company_info for basic financial metrics"
|
|
681
|
+
return error_msg
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@mcp.tool(
|
|
685
|
+
name="compare_stocks",
|
|
686
|
+
annotations={
|
|
687
|
+
"title": "Compare Multiple Stocks",
|
|
688
|
+
"readOnlyHint": True,
|
|
689
|
+
"destructiveHint": False,
|
|
690
|
+
"idempotentHint": True,
|
|
691
|
+
"openWorldHint": True
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
async def compare_stocks(params: QuoteComparisonInput) -> str:
|
|
695
|
+
"""Compare key metrics across multiple stocks side-by-side.
|
|
696
|
+
|
|
697
|
+
This tool enables easy comparison of multiple stocks by showing key metrics
|
|
698
|
+
in a side-by-side format for quick analysis and decision making.
|
|
699
|
+
|
|
700
|
+
Use this tool when:
|
|
701
|
+
- User wants to compare multiple stocks
|
|
702
|
+
- User asks "which is better, [stock1] or [stock2]"
|
|
703
|
+
- User wants to see relative performance
|
|
704
|
+
- User needs comparison for investment decisions
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
params (QuoteComparisonInput): Contains:
|
|
708
|
+
- tickers (List[str]): 2-10 ticker symbols to compare
|
|
709
|
+
- response_format (ResponseFormat): 'markdown' or 'json'
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
str: Comparison table in requested format
|
|
713
|
+
|
|
714
|
+
Example:
|
|
715
|
+
Input: {"tickers": ["AAPL", "MSFT", "GOOGL"], "response_format": "markdown"}
|
|
716
|
+
Output: Side-by-side comparison table of key metrics
|
|
717
|
+
"""
|
|
718
|
+
try:
|
|
719
|
+
comparison_data = []
|
|
720
|
+
|
|
721
|
+
for ticker in params.tickers:
|
|
722
|
+
try:
|
|
723
|
+
ticker_obj = yf.Ticker(ticker)
|
|
724
|
+
info = ticker_obj.info
|
|
725
|
+
|
|
726
|
+
data = {
|
|
727
|
+
"Ticker": ticker,
|
|
728
|
+
"Name": safe_get(info, 'longName', ticker),
|
|
729
|
+
"Price": safe_get(info, 'currentPrice'),
|
|
730
|
+
"Change%": safe_get(info, 'regularMarketChangePercent'),
|
|
731
|
+
"MarketCap": safe_get(info, 'marketCap'),
|
|
732
|
+
"PE": safe_get(info, 'trailingPE'),
|
|
733
|
+
"EPS": safe_get(info, 'trailingEps'),
|
|
734
|
+
"DivYield%": safe_get(info, 'dividendYield'),
|
|
735
|
+
"Beta": safe_get(info, 'beta'),
|
|
736
|
+
"52WkHigh": safe_get(info, 'fiftyTwoWeekHigh'),
|
|
737
|
+
"52WkLow": safe_get(info, 'fiftyTwoWeekLow'),
|
|
738
|
+
"Volume": safe_get(info, 'volume'),
|
|
739
|
+
"AvgVolume": safe_get(info, 'averageVolume'),
|
|
740
|
+
"Sector": safe_get(info, 'sector'),
|
|
741
|
+
"Industry": safe_get(info, 'industry')
|
|
742
|
+
}
|
|
743
|
+
comparison_data.append(data)
|
|
744
|
+
except Exception as e:
|
|
745
|
+
comparison_data.append({
|
|
746
|
+
"Ticker": ticker,
|
|
747
|
+
"Error": str(e)
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
if params.response_format == ResponseFormat.MARKDOWN:
|
|
751
|
+
result = f"# Stock Comparison: {', '.join(params.tickers)}\n\n"
|
|
752
|
+
|
|
753
|
+
# Create comparison DataFrame
|
|
754
|
+
df = pd.DataFrame(comparison_data)
|
|
755
|
+
result += dataframe_to_markdown(df)
|
|
756
|
+
|
|
757
|
+
result += "\n\n## Key Insights\n\n"
|
|
758
|
+
|
|
759
|
+
# Find best/worst performers
|
|
760
|
+
valid_data = [d for d in comparison_data if "Error" not in d]
|
|
761
|
+
if valid_data:
|
|
762
|
+
# Highest price
|
|
763
|
+
prices = [(d["Ticker"], d["Price"]) for d in valid_data if d["Price"] != "N/A"]
|
|
764
|
+
if prices:
|
|
765
|
+
highest = max(prices, key=lambda x: x[1])
|
|
766
|
+
result += f"- **Highest Price:** {highest[0]} at {format_currency(highest[1])}\n"
|
|
767
|
+
|
|
768
|
+
# Largest market cap
|
|
769
|
+
market_caps = [(d["Ticker"], d["MarketCap"]) for d in valid_data if d["MarketCap"] != "N/A"]
|
|
770
|
+
if market_caps:
|
|
771
|
+
largest = max(market_caps, key=lambda x: x[1])
|
|
772
|
+
result += f"- **Largest Market Cap:** {largest[0]} at {format_large_number(largest[1])}\n"
|
|
773
|
+
|
|
774
|
+
# Best dividend yield
|
|
775
|
+
div_yields = [(d["Ticker"], d["DivYield%"]) for d in valid_data if d["DivYield%"] != "N/A"]
|
|
776
|
+
if div_yields:
|
|
777
|
+
best_div = max(div_yields, key=lambda x: x[1])
|
|
778
|
+
result += f"- **Highest Dividend Yield:** {best_div[0]} at {format_percentage(best_div[1])}\n"
|
|
779
|
+
|
|
780
|
+
return truncate_response(result, "")
|
|
781
|
+
else:
|
|
782
|
+
# JSON format
|
|
783
|
+
result = {
|
|
784
|
+
"tickers": params.tickers,
|
|
785
|
+
"comparison": comparison_data
|
|
786
|
+
}
|
|
787
|
+
return json.dumps(result, indent=2, default=str)
|
|
788
|
+
|
|
789
|
+
except Exception as e:
|
|
790
|
+
error_msg = f"Error comparing stocks: {str(e)}\n\n"
|
|
791
|
+
error_msg += "**Troubleshooting:**\n"
|
|
792
|
+
error_msg += "- Verify all ticker symbols are correct\n"
|
|
793
|
+
error_msg += "- Some data may be missing for certain stocks"
|
|
794
|
+
return error_msg
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
@mcp.tool(
|
|
798
|
+
name="get_analyst_recommendations",
|
|
799
|
+
annotations={
|
|
800
|
+
"title": "Get Analyst Recommendations and Price Targets",
|
|
801
|
+
"readOnlyHint": True,
|
|
802
|
+
"destructiveHint": False,
|
|
803
|
+
"idempotentHint": True,
|
|
804
|
+
"openWorldHint": True
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
async def get_analyst_recommendations(params: TickerInput) -> str:
|
|
808
|
+
"""Get analyst recommendations, price targets, and upgrades/downgrades history.
|
|
809
|
+
|
|
810
|
+
This tool provides Wall Street analyst consensus, price targets, and
|
|
811
|
+
recommendation changes to help understand professional sentiment.
|
|
812
|
+
|
|
813
|
+
Use this tool when:
|
|
814
|
+
- User wants to know what analysts think
|
|
815
|
+
- User asks about price targets or recommendations
|
|
816
|
+
- User wants to see recent upgrades/downgrades
|
|
817
|
+
- User needs professional analysis summary
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
params (TickerInput): Contains:
|
|
821
|
+
- ticker (str): Stock ticker symbol
|
|
822
|
+
- response_format (ResponseFormat): 'markdown' or 'json'
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
str: Analyst recommendations and price targets
|
|
826
|
+
|
|
827
|
+
Example:
|
|
828
|
+
Input: {"ticker": "AAPL", "response_format": "markdown"}
|
|
829
|
+
Output: Analyst consensus, price targets, recent recommendations
|
|
830
|
+
"""
|
|
831
|
+
try:
|
|
832
|
+
ticker_obj = yf.Ticker(params.ticker)
|
|
833
|
+
recommendations = ticker_obj.recommendations
|
|
834
|
+
info = ticker_obj.info
|
|
835
|
+
|
|
836
|
+
if params.response_format == ResponseFormat.MARKDOWN:
|
|
837
|
+
result = f"# Analyst Recommendations: {params.ticker}\n\n"
|
|
838
|
+
|
|
839
|
+
# Price targets
|
|
840
|
+
result += "## Price Targets\n\n"
|
|
841
|
+
result += f"- **Target High:** {format_currency(safe_get(info, 'targetHighPrice'))}\n"
|
|
842
|
+
result += f"- **Target Mean:** {format_currency(safe_get(info, 'targetMeanPrice'))}\n"
|
|
843
|
+
result += f"- **Target Low:** {format_currency(safe_get(info, 'targetLowPrice'))}\n"
|
|
844
|
+
result += f"- **Target Median:** {format_currency(safe_get(info, 'targetMedianPrice'))}\n"
|
|
845
|
+
result += f"- **Current Price:** {format_currency(safe_get(info, 'currentPrice'))}\n\n"
|
|
846
|
+
|
|
847
|
+
# Calculate upside/downside
|
|
848
|
+
current = safe_get(info, 'currentPrice')
|
|
849
|
+
target = safe_get(info, 'targetMeanPrice')
|
|
850
|
+
if current != "N/A" and target != "N/A":
|
|
851
|
+
upside = ((target - current) / current) * 100
|
|
852
|
+
result += f"**Potential from Mean Target:** {upside:+.2f}%\n\n"
|
|
853
|
+
|
|
854
|
+
# Analyst consensus
|
|
855
|
+
result += "## Analyst Consensus\n\n"
|
|
856
|
+
result += f"- **Number of Analysts:** {safe_get(info, 'numberOfAnalystOpinions')}\n"
|
|
857
|
+
result += f"- **Recommendation Mean:** {safe_get(info, 'recommendationMean')} "
|
|
858
|
+
|
|
859
|
+
rec_mean = safe_get(info, 'recommendationMean')
|
|
860
|
+
if rec_mean != "N/A":
|
|
861
|
+
if rec_mean <= 2.0:
|
|
862
|
+
result += "(Strong Buy/Buy)\n"
|
|
863
|
+
elif rec_mean <= 3.0:
|
|
864
|
+
result += "(Hold)\n"
|
|
865
|
+
else:
|
|
866
|
+
result += "(Sell/Underperform)\n"
|
|
867
|
+
else:
|
|
868
|
+
result += "\n"
|
|
869
|
+
|
|
870
|
+
result += f"- **Recommendation Key:** {safe_get(info, 'recommendationKey')}\n\n"
|
|
871
|
+
|
|
872
|
+
# Recent recommendations
|
|
873
|
+
if recommendations is not None and not recommendations.empty:
|
|
874
|
+
result += "## Recent Recommendations (Last 10)\n\n"
|
|
875
|
+
recent = recommendations.tail(10).copy()
|
|
876
|
+
recent.index = recent.index.strftime('%Y-%m-%d')
|
|
877
|
+
result += dataframe_to_markdown(recent)
|
|
878
|
+
if len(recommendations) > 10:
|
|
879
|
+
result += f"\n\n*Showing last 10 of {len(recommendations)} recommendations. Request more if needed.*"
|
|
880
|
+
else:
|
|
881
|
+
result += "No recent recommendation data available.\n"
|
|
882
|
+
|
|
883
|
+
return truncate_response(result, "")
|
|
884
|
+
else:
|
|
885
|
+
# JSON format
|
|
886
|
+
result = {
|
|
887
|
+
"ticker": params.ticker,
|
|
888
|
+
"priceTargets": {
|
|
889
|
+
"high": safe_get(info, 'targetHighPrice'),
|
|
890
|
+
"mean": safe_get(info, 'targetMeanPrice'),
|
|
891
|
+
"low": safe_get(info, 'targetLowPrice'),
|
|
892
|
+
"median": safe_get(info, 'targetMedianPrice'),
|
|
893
|
+
"currentPrice": safe_get(info, 'currentPrice')
|
|
894
|
+
},
|
|
895
|
+
"consensus": {
|
|
896
|
+
"numberOfAnalysts": safe_get(info, 'numberOfAnalystOpinions'),
|
|
897
|
+
"recommendationMean": safe_get(info, 'recommendationMean'),
|
|
898
|
+
"recommendationKey": safe_get(info, 'recommendationKey')
|
|
899
|
+
},
|
|
900
|
+
"recentRecommendations": recommendations.to_dict() if recommendations is not None and not recommendations.empty else {}
|
|
901
|
+
}
|
|
902
|
+
return json.dumps(result, indent=2, default=str)
|
|
903
|
+
|
|
904
|
+
except Exception as e:
|
|
905
|
+
error_msg = f"Error fetching analyst recommendations for {params.ticker}: {str(e)}\n\n"
|
|
906
|
+
error_msg += "**Troubleshooting:**\n"
|
|
907
|
+
error_msg += "- Verify ticker symbol is correct\n"
|
|
908
|
+
error_msg += "- Analyst data may not be available for all stocks"
|
|
909
|
+
return error_msg
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
# ============================================================================
|
|
913
|
+
# RUN SERVER
|
|
914
|
+
# ============================================================================
|
|
915
|
+
|
|
916
|
+
if __name__ == "__main__":
|
|
917
|
+
# Run the MCP server with stdio transport (default for Claude Desktop)
|
|
918
|
+
mcp.run()
|