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 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
+ [![npm version](https://img.shields.io/npm/v/yahoo-finance-mcp-server.svg)](https://www.npmjs.com/package/yahoo-finance-mcp-server)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ }
@@ -0,0 +1,8 @@
1
+ # Yahoo Finance MCP Server Requirements
2
+ # Install all dependencies with: pip install -r requirements.txt
3
+
4
+ yfinance>=0.2.32
5
+ pandas>=1.5.0
6
+ mcp>=0.1.0
7
+ pydantic>=2.0.0
8
+ httpx>=0.24.0
@@ -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()