#
tokens: 28611/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .gitignore
├── .python-version
├── CLAUDE.md
├── Dockerfile
├── LICENSE
├── pyproject.toml
├── README.md
├── smithery.yaml
├── src
│   └── alpha_vantage_mcp
│       ├── __init__.py
│       ├── server.py
│       └── tools.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.12
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | # Version control
 2 | .git/
 3 | .gitignore
 4 | .github/
 5 | 
 6 | # Environment
 7 | .env
 8 | .venv/
 9 | venv/
10 | env/
11 | .python-version
12 | 
13 | # IDE and editor files
14 | .vscode/
15 | .idea/
16 | *.swp
17 | *.swo
18 | *~
19 | .DS_Store
20 | 
21 | # Project configuration
22 | smithery.yaml
23 | .dockerignore
24 | 
25 | # Documentation
26 | README.md
27 | LICENSE
28 | docs/
29 | *.md
30 | 
31 | # Testing
32 | tests/
33 | **/__pycache__/
34 | **/*.pyc
35 | **/*.pyo
36 | **/*.pyd
37 | **/.pytest_cache/
38 | **/.coverage
39 | htmlcov/
40 | 
41 | # Build artifacts
42 | *.egg-info/
43 | dist/
44 | build/
45 | 
46 | # Temporary files
47 | *.log
48 | *.tmp
49 | .cache/
50 | temp/
51 | 
52 | # Keep only what's needed
53 | # (Don't ignore these files)
54 | !pyproject.toml
55 | !uv.lock
56 | !src/
57 | !entrypoint.sh
58 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Alpha Vantage MCP Server
  2 | [![smithery badge](https://smithery.ai/badge/@berlinbra/alpha-vantage-mcp)](https://smithery.ai/server/@berlinbra/alpha-vantage-mcp)
  3 | 
  4 | A Model Context Protocol (MCP) server that provides real-time access to financial market data through the free [Alpha Vantage API](https://www.alphavantage.co/documentation/). This server implements a standardized interface for retrieving stock quotes and company information.
  5 | 
  6 | <a href="https://glama.ai/mcp/servers/0wues5td08"><img width="380" height="200" src="https://glama.ai/mcp/servers/0wues5td08/badge" alt="AlphaVantage-MCP MCP server" /></a>
  7 | 
  8 | # Features
  9 | 
 10 | - Real-time stock quotes with price, volume, and change data
 11 | - Detailed company information including sector, industry, and market cap
 12 | - Real-time cryptocurrency exchange rates with bid/ask prices
 13 | - Daily, weekly, and monthly cryptocurrency time series data
 14 | - Real-time options chain data with Greeks and implied volatility
 15 | - Historical options chain data with advanced filtering and sorting
 16 | - Comprehensive ETF profile data with holdings, sector allocation, and key metrics
 17 | - Upcoming earnings calendar with customizable time horizons
 18 | - Historical earnings data with annual and quarterly reports
 19 | - Built-in error handling and rate limit management
 20 | 
 21 | ## Installation
 22 | 
 23 | ### Using Claude Desktop
 24 | 
 25 | #### Installing via Docker
 26 | 
 27 | - Clone the repository and build a local image to be utilized by your Claude desktop client
 28 | 
 29 | ```sh
 30 | cd alpha-vantage-mcp
 31 | docker build -t mcp/alpha-vantage .
 32 | ```
 33 | 
 34 | - Change your `claude_desktop_config.json` to match the following, replacing `REPLACE_API_KEY` with your actual key:
 35 | 
 36 |  > `claude_desktop_config.json` path
 37 |  >
 38 |  > - On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
 39 |  > - On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
 40 | 
 41 | ```json
 42 | {
 43 |   "mcpServers": {
 44 |     "alphavantage": {
 45 |       "command": "docker",
 46 |       "args": [
 47 |         "run",
 48 |         "-i",
 49 |         "-e",
 50 |         "ALPHA_VANTAGE_API_KEY",
 51 |         "mcp/alpha-vantage"
 52 |       ],
 53 |       "env": {
 54 |         "ALPHA_VANTAGE_API_KEY": "REPLACE_API_KEY"
 55 |       }
 56 |     }
 57 |   }
 58 | }
 59 | ```
 60 | 
 61 | #### Installing via Smithery
 62 | 
 63 | To install Alpha Vantage MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@berlinbra/alpha-vantage-mcp):
 64 | 
 65 | ```bash
 66 | npx -y @smithery/cli install @berlinbra/alpha-vantage-mcp --client claude
 67 | ```
 68 | 
 69 | <summary> <h3> Development/Unpublished Servers Configuration <h3> </summary>
 70 | 
 71 | <details>
 72 | 
 73 | ```json
 74 | {
 75 |  "mcpServers": {
 76 |   "alpha-vantage-mcp": {
 77 |    "args": [
 78 |     "--directory",
 79 |     "/Users/{INSERT_USER}/YOUR/PATH/TO/alpha-vantage-mcp",
 80 |     "run",
 81 |     "alpha-vantage-mcp"
 82 |    ],
 83 |    "command": "uv",
 84 |    "env": {
 85 |     "ALPHA_VANTAGE_API_KEY": "<insert api key>"
 86 |    }
 87 |   }
 88 |  }
 89 | }
 90 | ```
 91 |         
 92 | </details>
 93 | 
 94 | #### Install packages
 95 | 
 96 | ```
 97 | uv install -e .
 98 | ```
 99 | 
100 | #### Running
101 | 
102 | After connecting Claude client with the MCP tool via json file and installing the packages, Claude should see the server's mcp tools:
103 | 
104 | You can run the sever yourself via:
105 | In alpha-vantage-mcp repo: 
106 | ```
107 | uv run src/alpha_vantage_mcp/server.py
108 | ```
109 | 
110 | with inspector
111 | ```
112 | * npx @modelcontextprotocol/inspector uv --directory /Users/{INSERT_USER}/YOUR/PATH/TO/alpha-vantage-mcp run src/alpha_vantage_mcp/server.py `
113 | ```
114 | 
115 | ## Available Tools
116 | 
117 | The server implements twelve tools:
118 | - `get-stock-quote`: Get the latest stock quote for a specific company
119 | - `get-company-info`: Get stock-related information for a specific company
120 | - `get-crypto-exchange-rate`: Get current cryptocurrency exchange rates
121 | - `get-time-series`: Get historical daily price data for a stock
122 | - `get-realtime-options`: Get real-time options chain data with Greeks and implied volatility
123 | - `get-historical-options`: Get historical options chain data with advanced filtering and sorting capabilities
124 | - `get-etf-profile`: Get comprehensive ETF profile information including holdings and sector allocation
125 | - `get-crypto-daily`: Get daily time series data for a cryptocurrency
126 | - `get-crypto-weekly`: Get weekly time series data for a cryptocurrency
127 | - `get-crypto-monthly`: Get monthly time series data for a cryptocurrency
128 | - `get-earnings-calendar`: Get upcoming earnings calendar data for companies
129 | - `get-historical-earnings`: Get historical earnings data for a specific company
130 | 
131 | ### get-stock-quote
132 | 
133 | **Input Schema:**
134 | ```json
135 | {
136 |     "symbol": {
137 |         "type": "string",
138 |         "description": "Stock symbol (e.g., AAPL, MSFT)"
139 |     }
140 | }
141 | ```
142 | 
143 | **Example Response:**
144 | ```
145 | Stock quote for AAPL:
146 | 
147 | Price: $198.50
148 | Change: $2.50 (+1.25%)
149 | Volume: 58942301
150 | High: $199.62
151 | Low: $197.20
152 | ```
153 | 
154 | ### get-company-info
155 | 
156 | Retrieves detailed company information for a given symbol.
157 | 
158 | **Input Schema:**
159 | ```json
160 | {
161 |     "symbol": {
162 |         "type": "string",
163 |         "description": "Stock symbol (e.g., AAPL, MSFT)"
164 |     }
165 | }
166 | ```
167 | 
168 | **Example Response:**
169 | ```
170 | Company information for AAPL:
171 | 
172 | Name: Apple Inc
173 | Sector: Technology
174 | Industry: Consumer Electronics
175 | Market Cap: $3000000000000
176 | Description: Apple Inc. designs, manufactures, and markets smartphones...
177 | Exchange: NASDAQ
178 | Currency: USD
179 | ```
180 | 
181 | ### get-crypto-exchange-rate
182 | 
183 | Retrieves real-time cryptocurrency exchange rates with additional market data.
184 | 
185 | **Input Schema:**
186 | ```json
187 | {
188 |     "crypto_symbol": {
189 |         "type": "string",
190 |         "description": "Cryptocurrency symbol (e.g., BTC, ETH)"
191 |     },
192 |     "market": {
193 |         "type": "string",
194 |         "description": "Market currency (e.g., USD, EUR)",
195 |         "default": "USD"
196 |     }
197 | }
198 | ```
199 | 
200 | **Example Response:**
201 | ```
202 | Cryptocurrency exchange rate for BTC/USD:
203 | 
204 | From: Bitcoin (BTC)
205 | To: United States Dollar (USD)
206 | Exchange Rate: 43521.45000
207 | Last Updated: 2024-12-17 19:45:00 UTC
208 | Bid Price: 43521.00000
209 | Ask Price: 43522.00000
210 | ```
211 | 
212 | ### get-time-series
213 | 
214 | Retrieves daily time series (OHLCV) data with optional date filtering.
215 | 
216 | **Input Schema:**
217 | ```json
218 | {
219 |     "symbol": {
220 |         "type": "string",
221 |         "description": "Stock symbol (e.g., AAPL, MSFT)"
222 |     },
223 |     "outputsize": {
224 |         "type": "string",
225 |         "description": "compact (latest 100 data points) or full (up to 20 years of data). When start_date or end_date is specified, defaults to 'full'",
226 |         "default": "compact"
227 |     },
228 |     "start_date": {
229 |         "type": "string",
230 |         "description": "Optional: Start date in YYYY-MM-DD format for filtering results",
231 |         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
232 |     },
233 |     "end_date": {
234 |         "type": "string",
235 |         "description": "Optional: End date in YYYY-MM-DD format for filtering results",
236 |         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
237 |     },
238 |     "limit": {
239 |         "type": "integer",
240 |         "description": "Optional: Number of data points to return when no date filtering is applied (default: 5)",
241 |         "default": 5,
242 |         "minimum": 1
243 |     }
244 | }
245 | ```
246 | **Example Response (Recent Data):**
247 | ```
248 | Time Series Data for AAPL (Last Refreshed: 2024-12-17 16:00:00):
249 | (Showing 5 most recent data points)
250 | 
251 | Date: 2024-12-16
252 | Open: $195.09
253 | High: $197.68
254 | Low: $194.83
255 | Close: $197.57
256 | Volume: 55,751,011
257 | ---
258 | Date: 2024-12-13
259 | Open: $194.50
260 | High: $196.25
261 | Low: $193.80
262 | Close: $195.12
263 | Volume: 48,320,567
264 | ---
265 | ```
266 | 
267 | **Example Response (Date Range Filtering):**
268 | ```
269 | Time Series Data for AAPL (Last Refreshed: 2024-12-17 16:00:00):
270 | Date Range: 2024-12-01 to 2024-12-07 (5 data points)
271 | 
272 | Date: 2024-12-06
273 | Open: $191.25
274 | High: $193.80
275 | Low: $190.55
276 | Close: $192.90
277 | Volume: 52,145,890
278 | ---
279 | Date: 2024-12-05
280 | Open: $189.75
281 | High: $192.40
282 | Low: $188.90
283 | Close: $191.30
284 | Volume: 47,892,345
285 | ---
286 | ```
287 | 
288 | ### get-realtime-options
289 | 
290 | Retrieves real-time options chain data for a stock with optional Greeks calculation and contract filtering.
291 | 
292 | **⚠️ PREMIUM SUBSCRIPTION REQUIRED**: This endpoint requires Alpha Vantage Premium with either the 600 requests/minute or 1200 requests/minute plan. The standard 75 requests/minute plan and free accounts will receive placeholder/demo data instead of real market data. For most use cases, consider using `get-historical-options` which works with all API key tiers.
293 | 
294 | **Input Schema:**
295 | ```json
296 | {
297 |     "symbol": {
298 |         "type": "string",
299 |         "description": "Stock symbol (e.g., AAPL, MSFT)"
300 |     },
301 |     "require_greeks": {
302 |         "type": "boolean",
303 |         "description": "Optional: Enable Greeks and implied volatility calculation (default: false)",
304 |         "default": false
305 |     },
306 |     "contract": {
307 |         "type": "string",
308 |         "description": "Optional: Specific options contract ID to retrieve"
309 |     },
310 |     "datatype": {
311 |         "type": "string",
312 |         "description": "Optional: Response format (json or csv, default: json)",
313 |         "enum": ["json", "csv"],
314 |         "default": "json"
315 |     }
316 | }
317 | ```
318 | 
319 | **Example Response:**
320 | ```
321 | Realtime Options Data for AAPL
322 | Last Updated: 2025-01-21 16:00:00
323 | 
324 | === Expiration: 2025-01-24 ===
325 | 
326 | Strike: $220.0 (CALL)
327 | Last: $5.25
328 | Bid: $5.10
329 | Ask: $5.30
330 | Volume: 1250
331 | Open Interest: 8420
332 | IV: 0.28
333 | Delta: 0.65
334 | Gamma: 0.02
335 | Theta: -0.15
336 | Vega: 0.45
337 | Rho: 0.12
338 | ---
339 | 
340 | Strike: $220.0 (PUT)
341 | Last: $1.85
342 | Bid: $1.80
343 | Ask: $1.90
344 | Volume: 820
345 | Open Interest: 5240
346 | IV: 0.25
347 | Delta: -0.35
348 | Gamma: 0.02
349 | Theta: -0.12
350 | Vega: 0.42
351 | Rho: -0.08
352 | ---
353 | ```
354 | 
355 | **Note**: The above example shows real market data which is only available with Alpha Vantage Premium 600+ requests/minute plans. Users with free accounts or 75 requests/minute plans will see placeholder data (symbols like "XXYYZZ", dates like "2099-99-99") and should use `get-historical-options` instead.
356 | 
357 | ### get-historical-options
358 | 
359 | Retrieves historical options chain data with advanced filtering and sorting capabilities to find specific contracts.
360 | 
361 | **Input Schema:**
362 | ```json
363 | {
364 |     "symbol": {
365 |         "type": "string",
366 |         "description": "Stock symbol (e.g., AAPL, MSFT)"
367 |     },
368 |     "date": {
369 |         "type": "string",
370 |         "description": "Optional: Trading date in YYYY-MM-DD format (defaults to previous trading day, must be after 2008-01-01)",
371 |         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
372 |     },
373 |     "expiry_date": {
374 |         "type": "string",
375 |         "description": "Optional: Filter by expiration date in YYYY-MM-DD format",
376 |         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
377 |     },
378 |     "min_strike": {
379 |         "type": "number",
380 |         "description": "Optional: Minimum strike price filter (e.g., 100.00)",
381 |         "minimum": 0
382 |     },
383 |     "max_strike": {
384 |         "type": "number",
385 |         "description": "Optional: Maximum strike price filter (e.g., 200.00)",
386 |         "minimum": 0
387 |     },
388 |     "contract_id": {
389 |         "type": "string",
390 |         "description": "Optional: Filter by specific contract ID (e.g., MSTR260116C00000500)"
391 |     },
392 |     "contract_type": {
393 |         "type": "string",
394 |         "description": "Optional: Filter by contract type (call or put)",
395 |         "enum": ["call", "put", "C", "P"]
396 |     },
397 |     "limit": {
398 |         "type": "integer",
399 |         "description": "Optional: Number of contracts to return after filtering (default: 10, use -1 for all contracts)",
400 |         "default": 10,
401 |         "minimum": -1
402 |     },
403 |     "sort_by": {
404 |         "type": "string",
405 |         "description": "Optional: Field to sort by",
406 |         "enum": ["strike", "expiration", "volume", "open_interest", "implied_volatility", "delta", "gamma", "theta", "vega", "rho", "last", "bid", "ask"],
407 |         "default": "strike"
408 |     },
409 |     "sort_order": {
410 |         "type": "string",
411 |         "description": "Optional: Sort order",
412 |         "enum": ["asc", "desc"],
413 |         "default": "asc"
414 |     }
415 | }
416 | ```
417 | 
418 | **Example Response (Basic):**
419 | ```
420 | Historical Options Data for AAPL (2024-02-20):
421 | Status: success
422 | Found 156 contracts, sorted by: strike (asc)
423 | 
424 | Contract Details:
425 | Contract ID: AAPL240315C00190000
426 | Expiration: 2024-03-15
427 | Strike: $190.00
428 | Type: call
429 | Last: $8.45
430 | Bid: $8.40
431 | Ask: $8.50
432 | Volume: 1245
433 | Open Interest: 4567
434 | Implied Volatility: 0.25
435 | Greeks:
436 |   Delta: 0.65
437 |   Gamma: 0.04
438 |   Theta: -0.15
439 |   Vega: 0.30
440 |   Rho: 0.25
441 | ---
442 | ```
443 | 
444 | **Example Response (Filtered):**
445 | ```
446 | Historical Options Data for MSTR (2024-02-20):
447 | Status: success
448 | Filters: Expiry: 2026-01-16, Strike: min $400 - max $600, Type: call
449 | Found 3 contracts, sorted by: strike (asc)
450 | 
451 | Contract Details:
452 | Contract ID: MSTR260116C00000500
453 | Expiration: 2026-01-16
454 | Strike: $500.00
455 | Type: call
456 | Last: $125.30
457 | Bid: $124.50
458 | Ask: $126.10
459 | Volume: 89
460 | Open Interest: 1234
461 | ---
462 | ```
463 | 
464 | ### get-etf-profile
465 | 
466 | Retrieves comprehensive ETF profile information including basic metrics, sector allocation, and top holdings.
467 | 
468 | **Input Schema:**
469 | ```json
470 | {
471 |     "symbol": {
472 |         "type": "string",
473 |         "description": "ETF symbol (e.g., QQQ, SPY, VTI)"
474 |     }
475 | }
476 | ```
477 | 
478 | **Example Response:**
479 | ```
480 | ETF profile for QQQ:
481 | 
482 | ETF Profile
483 | 
484 | Basic Information:
485 | Net Assets: $352,700,000,000
486 | Net Expense Ratio: 0.200%
487 | Portfolio Turnover: 8.0%
488 | Dividend Yield: 0.50%
489 | Inception Date: 1999-03-10
490 | Leveraged: NO
491 | 
492 | Sector Allocation:
493 | INFORMATION TECHNOLOGY: 51.9%
494 | COMMUNICATION SERVICES: 15.4%
495 | CONSUMER DISCRETIONARY: 12.2%
496 | CONSUMER STAPLES: 4.8%
497 | HEALTHCARE: 4.5%
498 | INDUSTRIALS: 4.4%
499 | UTILITIES: 1.4%
500 | MATERIALS: 1.3%
501 | ENERGY: 0.5%
502 | FINANCIALS: 0.4%
503 | 
504 | Top Holdings:
505 |  1. NVDA - NVIDIA CORP: 9.80%
506 |  2. MSFT - MICROSOFT CORP: 8.85%
507 |  3. AAPL - APPLE INC: 7.35%
508 |  4. AMZN - AMAZON.COM INC: 5.65%
509 |  5. AVGO - BROADCOM INC: 5.14%
510 |  6. META - META PLATFORMS INC CLASS A: 3.63%
511 |  7. NFLX - NETFLIX INC: 3.10%
512 |  8. TSLA - TESLA INC: 2.66%
513 |  9. GOOGL - ALPHABET INC CLASS A: 2.49%
514 | 10. COST - COSTCO WHOLESALE CORP: 2.49%
515 | 
516 | ... and 92 more holdings
517 | 
518 | Total Holdings: 102
519 | ```
520 | 
521 | ### get-crypto-daily
522 | 
523 | Retrieves daily time series data for a cryptocurrency.
524 | 
525 | **Input Schema:**
526 | ```json
527 | {
528 |     "symbol": {
529 |         "type": "string",
530 |         "description": "Cryptocurrency symbol (e.g., BTC, ETH)"
531 |     },
532 |     "market": {
533 |         "type": "string",
534 |         "description": "Market currency (e.g., USD, EUR)",
535 |         "default": "USD"
536 |     }
537 | }
538 | ```
539 | 
540 | **Example Response:**
541 | ```
542 | Daily cryptocurrency time series for SOL in USD:
543 | 
544 | Daily Time Series for Solana (SOL)
545 | Market: United States Dollar (USD)
546 | Last Refreshed: 2025-04-17 00:00:00 UTC
547 | 
548 | Date: 2025-04-17
549 | Open: 131.31000000 USD
550 | High: 131.67000000 USD
551 | Low: 130.74000000 USD
552 | Close: 131.15000000 USD
553 | Volume: 39652.22195178
554 | ---
555 | Date: 2025-04-16
556 | Open: 126.10000000 USD
557 | High: 133.91000000 USD
558 | Low: 123.46000000 USD
559 | Close: 131.32000000 USD
560 | Volume: 1764240.04195810
561 | ---
562 | ```
563 | 
564 | ### get-crypto-weekly
565 | 
566 | Retrieves weekly time series data for a cryptocurrency.
567 | 
568 | **Input Schema:**
569 | ```json
570 | {
571 |     "symbol": {
572 |         "type": "string",
573 |         "description": "Cryptocurrency symbol (e.g., BTC, ETH)"
574 |     },
575 |     "market": {
576 |         "type": "string",
577 |         "description": "Market currency (e.g., USD, EUR)",
578 |         "default": "USD"
579 |     }
580 | }
581 | ```
582 | 
583 | **Example Response:**
584 | ```
585 | Weekly cryptocurrency time series for SOL in USD:
586 | 
587 | Weekly Time Series for Solana (SOL)
588 | Market: United States Dollar (USD)
589 | Last Refreshed: 2025-04-17 00:00:00 UTC
590 | 
591 | Date: 2025-04-17
592 | Open: 128.32000000 USD
593 | High: 136.00000000 USD
594 | Low: 123.46000000 USD
595 | Close: 131.15000000 USD
596 | Volume: 4823091.05667581
597 | ---
598 | Date: 2025-04-13
599 | Open: 105.81000000 USD
600 | High: 134.11000000 USD
601 | Low: 95.16000000 USD
602 | Close: 128.32000000 USD
603 | Volume: 18015328.38860037
604 | ---
605 | ```
606 | 
607 | ### get-crypto-monthly
608 | 
609 | Retrieves monthly time series data for a cryptocurrency.
610 | 
611 | **Input Schema:**
612 | ```json
613 | {
614 |     "symbol": {
615 |         "type": "string",
616 |         "description": "Cryptocurrency symbol (e.g., BTC, ETH)"
617 |     },
618 |     "market": {
619 |         "type": "string",
620 |         "description": "Market currency (e.g., USD, EUR)",
621 |         "default": "USD"
622 |     }
623 | }
624 | ```
625 | 
626 | **Example Response:**
627 | ```
628 | Monthly cryptocurrency time series for SOL in USD:
629 | 
630 | Monthly Time Series for Solana (SOL)
631 | Market: United States Dollar (USD)
632 | Last Refreshed: 2025-04-17 00:00:00 UTC
633 | 
634 | Date: 2025-04-17
635 | Open: 124.51000000 USD
636 | High: 136.18000000 USD
637 | Low: 95.16000000 USD
638 | Close: 131.15000000 USD
639 | Volume: 34268628.85976021
640 | ---
641 | Date: 2025-03-31
642 | Open: 148.09000000 USD
643 | High: 180.00000000 USD
644 | Low: 112.00000000 USD
645 | Close: 124.54000000 USD
646 | Volume: 42360395.75443056
647 | ---
648 | ```
649 | 
650 | ### get-earnings-calendar
651 | 
652 | Retrieves upcoming earnings calendar data for companies with customizable time horizons and sorting capabilities.
653 | 
654 | **Input Schema:**
655 | ```json
656 | {
657 |     "symbol": {
658 |         "type": "string",
659 |         "description": "Optional: Stock symbol to filter earnings for a specific company (e.g., AAPL, MSFT, IBM)"
660 |     },
661 |     "horizon": {
662 |         "type": "string",
663 |         "description": "Optional: Time horizon for earnings data (3month, 6month, or 12month)",
664 |         "enum": ["3month", "6month", "12month"],
665 |         "default": "12month"
666 |     },
667 |     "limit": {
668 |         "type": "integer",
669 |         "description": "Optional: Number of earnings entries to return (default: 100)",
670 |         "default": 100,
671 |         "minimum": 1
672 |     },
673 |     "sort_by": {
674 |         "type": "string",
675 |         "description": "Optional: Field to sort by",
676 |         "enum": ["reportDate", "symbol", "name", "fiscalDateEnding", "estimate"],
677 |         "default": "reportDate"
678 |     },
679 |     "sort_order": {
680 |         "type": "string",
681 |         "description": "Optional: Sort order",
682 |         "enum": ["asc", "desc"],
683 |         "default": "desc"
684 |     }
685 | }
686 | ```
687 | 
688 | **Example Response (Default - Latest First):**
689 | ```
690 | Earnings calendar (12month):
691 | 
692 | Upcoming Earnings Calendar (Sorted by reportDate desc):
693 | 
694 | Company: NVDA - NVIDIA Corp
695 | Report Date: 2025-08-15
696 | Fiscal Date End: 2025-07-31
697 | Estimate: $4.25 USD
698 | ---
699 | Company: AAPL - Apple Inc
700 | Report Date: 2025-07-30
701 | Fiscal Date End: 2025-06-30
702 | Estimate: $1.85 USD
703 | ---
704 | Company: MSTR - MicroStrategy Inc
705 | Report Date: 2025-05-08
706 | Fiscal Date End: 2025-03-31
707 | Estimate: $1.30 USD
708 | ---
709 | Company: MSTR - MicroStrategy Inc
710 | Report Date: 2025-02-06
711 | Fiscal Date End: 2024-12-31
712 | Estimate: $1.25 USD
713 | ---
714 | ```
715 | 
716 | **Example Response (Sorted by Symbol):**
717 | ```
718 | Earnings calendar (12month):
719 | 
720 | Upcoming Earnings Calendar (Sorted by symbol asc):
721 | 
722 | Company: AAPL - Apple Inc
723 | Report Date: 2025-07-30
724 | Fiscal Date End: 2025-06-30
725 | Estimate: $1.85 USD
726 | ---
727 | Company: GOOGL - Alphabet Inc
728 | Report Date: 2025-04-25
729 | Fiscal Date End: 2025-03-31
730 | Estimate: $2.15 USD
731 | ---
732 | Company: MSTR - MicroStrategy Inc
733 | Report Date: 2025-02-06
734 | Fiscal Date End: 2024-12-31
735 | Estimate: $1.25 USD
736 | ---
737 | ```
738 | 
739 | ### get-historical-earnings
740 | 
741 | Retrieves historical earnings data for a specific company, including both annual and quarterly reports.
742 | 
743 | **Input Schema:**
744 | ```json
745 | {
746 |     "symbol": {
747 |         "type": "string",
748 |         "description": "Stock symbol for the company (e.g., AAPL, MSFT, IBM)"
749 |     },
750 |     "limit_annual": {
751 |         "type": "integer",
752 |         "description": "Optional: Number of annual earnings to return (default: 5)",
753 |         "default": 5,
754 |         "minimum": 1
755 |     },
756 |     "limit_quarterly": {
757 |         "type": "integer",
758 |         "description": "Optional: Number of quarterly earnings to return (default: 8)",
759 |         "default": 8,
760 |         "minimum": 1
761 |     }
762 | }
763 | ```
764 | 
765 | **Example Response:**
766 | ```
767 | Historical Earnings for MSTR:
768 | 
769 | === ANNUAL EARNINGS ===
770 | Fiscal Year End: 2023-12-31
771 | Reported EPS: $5.40
772 | ---
773 | Fiscal Year End: 2022-12-31
774 | Reported EPS: $-9.98
775 | ---
776 | 
777 | === QUARTERLY EARNINGS ===
778 | Fiscal Quarter End: 2024-09-30
779 | Reported Date: 2024-10-30
780 | Reported EPS: $1.10
781 | Estimated EPS: $0.98
782 | Surprise: +$0.12 (+12.24%)
783 | Report Time: post-market
784 | ---
785 | Fiscal Quarter End: 2024-06-30
786 | Reported Date: 2024-08-01
787 | Reported EPS: $1.05
788 | Estimated EPS: $0.92
789 | Surprise: +$0.13 (+14.13%)
790 | Report Time: post-market
791 | ---
792 | ```
793 | 
794 | ## Error Handling
795 | 
796 | The server includes comprehensive error handling for various scenarios:
797 | 
798 | - Rate limit exceeded
799 | - Invalid API key
800 | - Network connectivity issues
801 | - Timeout handling
802 | - Malformed responses
803 | 
804 | Error messages are returned in a clear, human-readable format.
805 | 
806 | ## Prerequisites
807 | 
808 | - Python 3.12 or higher
809 | - httpx
810 | - mcp
811 | 
812 | ## Contributors
813 | 
814 | - [berlinbra](https://github.com/berlinbra)
815 | - [zzulanas](https://github.com/zzulanas)
816 | 
817 | ## Contributing
818 | 
819 | Contributions are welcome! Please feel free to submit a Pull Request.
820 | 
821 | ## License
822 | This MCP server is licensed under the MIT License. 
823 | This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
824 | 
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Alpha Vantage MCP Server - Claude.md
  2 | 
  3 | ## Overview
  4 | 
  5 | The Alpha Vantage MCP Server is a Model Context Protocol (MCP) implementation that provides real-time access to financial market data through the Alpha Vantage API. This server enables Claude and other AI assistants to retrieve stock quotes, company information, and cryptocurrency data.
  6 | 
  7 | ## Architecture
  8 | 
  9 | ### Core Components
 10 | 
 11 | - **server.py** - Main MCP server implementation that handles tool registration and execution
 12 | - **tools.py** - Utility functions for API requests and data formatting
 13 | - **smithery.yaml** - Smithery MCP configuration for automated deployment
 14 | 
 15 | ### Key Features
 16 | 
 17 | 1. **Stock Market Data**
 18 |    - Real-time stock quotes with price, volume, and change data
 19 |    - Detailed company information (sector, industry, market cap)
 20 |    - Historical daily time series data (OHLCV)
 21 | 
 22 | 2. **Cryptocurrency Support** 
 23 |    - Real-time cryptocurrency exchange rates with bid/ask prices
 24 |    - Daily, weekly, and monthly cryptocurrency time series data
 25 | 
 26 | 3. **Advanced Options Data**
 27 |    - Historical options chain data with Greeks calculations
 28 |    - Advanced filtering and sorting capabilities by strike, volume, IV, etc.
 29 | 
 30 | 4. **Error Handling & Rate Limiting**
 31 |    - Built-in error handling for API failures
 32 |    - Rate limit management and timeout handling
 33 |    - Comprehensive error messages
 34 |    - **CRITICAL**: Never use placeholder data in responses - always handle errors properly and transparently
 35 | 
 36 | ## Available Tools
 37 | 
 38 | The server implements 12 MCP tools:
 39 | 
 40 | 1. **get-stock-quote** - Current stock quote information
 41 | 2. **get-company-info** - Detailed company overview data
 42 | 3. **get-crypto-exchange-rate** - Cryptocurrency exchange rates
 43 | 4. **get-time-series** - Historical daily stock price data
 44 | 5. **get-realtime-options** - Real-time options chain data (premium required)
 45 | 6. **get-historical-options** - Options chain data with sorting
 46 | 7. **get-etf-profile** - Comprehensive ETF profile with holdings and sector allocation
 47 | 8. **get-crypto-daily** - Daily cryptocurrency time series
 48 | 9. **get-crypto-weekly** - Weekly cryptocurrency time series
 49 | 10. **get-crypto-monthly** - Monthly cryptocurrency time series
 50 | 11. **get-earnings-calendar** - Upcoming earnings calendar data
 51 | 12. **get-historical-earnings** - Historical earnings data
 52 | 
 53 | ## Smithery MCP Integration
 54 | 
 55 | ### Configuration (smithery.yaml:3-18)
 56 | 
 57 | The repository uses Smithery for automated MCP deployment and configuration:
 58 | 
 59 | - **startCommand**: Defines STDIO-based communication
 60 | - **configSchema**: JSON Schema requiring `alphaVantageApiKey`
 61 | - **commandFunction**: JavaScript function that generates the CLI command with proper environment variables
 62 | 
 63 | ### Key Smithery Features
 64 | 
 65 | - **Automated Installation**: One-command install via `npx @smithery/cli install @berlinbra/alpha-vantage-mcp --client claude`
 66 | - **Configuration Management**: Handles API key injection and environment setup
 67 | - **Claude Desktop Integration**: Direct integration with Claude desktop client
 68 | 
 69 | ### Smithery Benefits
 70 | 
 71 | 1. **Simplified Deployment**: No manual configuration of `claude_desktop_config.json`
 72 | 2. **Environment Management**: Automatic API key handling
 73 | 3. **Version Management**: Centralized distribution and updates
 74 | 4. **Cross-Platform Support**: Works on macOS, Windows, and Linux
 75 | 
 76 | ## Installation Methods
 77 | 
 78 | ### 1. Via Smithery (Recommended)
 79 | ```bash
 80 | npx -y @smithery/cli install @berlinbra/alpha-vantage-mcp --client claude
 81 | ```
 82 | 
 83 | ### 2. Via Docker
 84 | - Build local image and configure `claude_desktop_config.json`
 85 | - Requires manual environment variable setup
 86 | 
 87 | ### 3. Development Setup
 88 | - Use `uv` package manager for local development
 89 | - Requires manual path configuration
 90 | 
 91 | ## API Integration
 92 | 
 93 | ### Alpha Vantage API (tools.py:12-70)
 94 | 
 95 | - **Base URL**: `https://www.alphavantage.co/query`
 96 | - **Authentication**: API key via `ALPHA_VANTAGE_API_KEY` environment variable
 97 | - **Request Handling**: Async HTTP client with 30-second timeout
 98 | - **Error Management**: Comprehensive error handling for rate limits, authentication, and network issues
 99 | 
100 | ### Supported API Functions
101 | 
102 | **Free Tier Compatible:**
103 | - `GLOBAL_QUOTE` - Stock quotes
104 | - `OVERVIEW` - Company information  
105 | - `CURRENCY_EXCHANGE_RATE` - Crypto exchange rates
106 | - `TIME_SERIES_DAILY` - Historical stock data
107 | - `HISTORICAL_OPTIONS` - Options chain data
108 | - `ETF_PROFILE` - ETF profile data with holdings and sectors
109 | - `EARNINGS_CALENDAR` - Upcoming earnings data
110 | - `EARNINGS` - Historical earnings data
111 | - `DIGITAL_CURRENCY_DAILY/WEEKLY/MONTHLY` - Crypto time series
112 | 
113 | **Premium Subscription Required:**
114 | - `REALTIME_OPTIONS` - Real-time options chain data (returns demo data with free API keys)
115 | 
116 | ## Development Commands
117 | 
118 | ### Running the Server
119 | ```bash
120 | # Direct execution
121 | uv run src/alpha_vantage_mcp/server.py
122 | 
123 | # With MCP Inspector
124 | npx @modelcontextprotocol/inspector uv --directory /path/to/alpha-vantage-mcp run src/alpha_vantage_mcp/server.py
125 | ```
126 | 
127 | ### Package Management
128 | ```bash
129 | uv install -e .
130 | ```
131 | 
132 | ## Dependencies
133 | 
134 | - **Python 3.12+** - Required runtime
135 | - **httpx** - Async HTTP client for API requests
136 | - **mcp** - Model Context Protocol framework
137 | 
138 | ## Contributors
139 | 
140 | - [berlinbra](https://github.com/berlinbra) - Primary maintainer
141 | - [zzulanas](https://github.com/zzulanas) - Contributor
142 | 
143 | ## Error Handling Guidelines
144 | 
145 | ### Core Principles
146 | 
147 | 1. **No Placeholder Data**: Under no circumstances should the server return placeholder or demo data as if it were real market data
148 | 2. **Transparent Error Reporting**: All API errors, rate limits, and access issues must be clearly communicated to users
149 | 3. **Proper Error Detection**: The server must detect and flag demo/placeholder responses from the API
150 | 4. **User Education**: Error messages should help users understand API limitations and subscription requirements
151 | 
152 | ### Common Error Scenarios
153 | 
154 | - **Rate Limiting**: Alpha Vantage free tier has request limits
155 | - **Premium Features**: Some endpoints (like realtime options) require paid subscriptions
156 | - **API Key Issues**: Invalid or expired API keys
157 | - **Demo Data**: Alpha Vantage may return placeholder data for demo purposes
158 | - **Network Issues**: Connection timeouts and service unavailability
159 | 
160 | ### Implementation Requirements
161 | 
162 | All formatting functions in `tools.py` must:
163 | - Detect placeholder/demo data patterns (e.g., "XXYYZZ" symbols, "2099-99-99" dates)
164 | - Return clear error messages instead of formatting fake data
165 | - Provide guidance on resolving access issues
166 | - Log error patterns for debugging
167 | 
168 | ## License
169 | 
170 | MIT License - Free for use, modification, and distribution
```

--------------------------------------------------------------------------------
/src/alpha_vantage_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | from . import server
2 | import asyncio
3 | 
4 | def main():
5 |     """Main entry point for the package."""
6 |     asyncio.run(server.main())
7 | 
8 | # Optionally expose other important items at package level
9 | __all__ = ['main', 'server']
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "alpha-vantage-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.12"
 7 | dependencies = [
 8 |     "httpx>=0.28.1",
 9 |     "mcp>=1.1.2",
10 | ]
11 | 
12 | [build-system]
13 | requires = [ "hatchling",]
14 | build-backend = "hatchling.build"
15 | 
16 | [project.scripts]
17 | alpha-vantage-mcp = "alpha_vantage_mcp:main"
18 | 
19 | [[project.authors]]
20 | name = "berlinbra"
21 | email = "[email protected]"
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - alphaVantageApiKey
10 |     properties:
11 |       alphaVantageApiKey:
12 |         type: string
13 |         description: Your Alpha Vantage API Key for accessing the API.
14 |   commandFunction:
15 |     # A function that produces the CLI command to start the MCP on stdio.
16 |     |-
17 |     (config) => ({ command: 'python', args: ['-m', 'src.alpha_vantage_mcp.server'], env: { ALPHA_VANTAGE_API_KEY: config.alphaVantageApiKey } })
18 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | # Start with a base Python image
 3 | FROM python:3.12-slim-bookworm
 4 | 
 5 | # Set the working directory in the container
 6 | WORKDIR /app
 7 | 
 8 | # Copy the pyproject.toml and uv.lock for dependencies
 9 | COPY pyproject.toml uv.lock /app/
10 | 
11 | # Install dependencies
12 | RUN pip install uvicorn 'httpx>=0.28.1' 'mcp>=1.1.2'
13 | 
14 | # Copy the rest of the application code
15 | COPY src/ /app/src/
16 | 
17 | # Set environment variables
18 | ENV ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
19 | 
20 | # Expose the port that the app runs on
21 | EXPOSE 8000
22 | 
23 | # Run the application
24 | CMD ["python", "-m", "src.alpha_vantage_mcp.server"]
25 | 
```

--------------------------------------------------------------------------------
/src/alpha_vantage_mcp/server.py:
--------------------------------------------------------------------------------

```python
  1 | from typing import Any, List, Dict, Optional
  2 | import asyncio
  3 | import httpx
  4 | from mcp.server.models import InitializationOptions
  5 | import mcp.types as types
  6 | from mcp.server import NotificationOptions, Server
  7 | import mcp.server.stdio
  8 | import os
  9 | 
 10 | # Import functions from tools.py
 11 | from .tools import (
 12 |     make_alpha_request,
 13 |     format_quote,
 14 |     format_company_info,
 15 |     format_crypto_rate,
 16 |     format_time_series,
 17 |     format_historical_options,
 18 |     format_crypto_time_series,
 19 |     format_earnings_calendar,
 20 |     format_historical_earnings,
 21 |     format_realtime_options,
 22 |     format_etf_profile,
 23 |     ALPHA_VANTAGE_BASE,
 24 |     API_KEY
 25 | )
 26 | 
 27 | if not API_KEY:
 28 |     raise ValueError("Missing ALPHA_VANTAGE_API_KEY environment variable")
 29 | 
 30 | server = Server("alpha_vantage_finance")
 31 | 
 32 | @server.list_tools()
 33 | async def handle_list_tools() -> list[types.Tool]:
 34 |     """
 35 |     List available tools.
 36 |     Each tool specifies its arguments using JSON Schema validation.
 37 |     """
 38 |     return [
 39 |         types.Tool(
 40 |             name="get-stock-quote",
 41 |             description="Get current stock quote information",
 42 |             inputSchema={
 43 |                 "type": "object",
 44 |                 "properties": {
 45 |                     "symbol": {
 46 |                         "type": "string",
 47 |                         "description": "Stock symbol (e.g., AAPL, MSFT)",
 48 |                     },
 49 |                 },
 50 |                 "required": ["symbol"],
 51 |             },
 52 |         ),
 53 |         types.Tool(
 54 |             name="get-company-info",
 55 |             description="Get detailed company information",
 56 |             inputSchema={
 57 |                 "type": "object",
 58 |                 "properties": {
 59 |                     "symbol": {
 60 |                         "type": "string",
 61 |                         "description": "Stock symbol (e.g., AAPL, MSFT)",
 62 |                     },
 63 |                 },
 64 |                 "required": ["symbol"],
 65 |             },
 66 |         ),
 67 |         types.Tool(
 68 |             name="get-crypto-exchange-rate",
 69 |             description="Get current cryptocurrency exchange rate",
 70 |             inputSchema={
 71 |                 "type": "object",
 72 |                 "properties": {
 73 |                     "crypto_symbol": {
 74 |                         "type": "string",
 75 |                         "description": "Cryptocurrency symbol (e.g., BTC, ETH)",
 76 |                     },
 77 |                     "market": {
 78 |                         "type": "string",
 79 |                         "description": "Market currency (e.g., USD, EUR)",
 80 |                         "default": "USD"
 81 |                     }
 82 |                 },
 83 |                 "required": ["crypto_symbol"],
 84 |             },
 85 |         ),
 86 |         types.Tool(
 87 |             name="get-time-series",
 88 |             description="Get daily time series data for a stock with optional date filtering",
 89 |             inputSchema={
 90 |                 "type": "object",
 91 |                 "properties": {
 92 |                     "symbol": {
 93 |                         "type": "string",
 94 |                         "description": "Stock symbol (e.g., AAPL, MSFT)",
 95 |                     },
 96 |                     "outputsize": {
 97 |                         "type": "string",
 98 |                         "description": "compact (latest 100 data points) or full (up to 20 years of data). When start_date or end_date is specified, defaults to 'full'",
 99 |                         "enum": ["compact", "full"],
100 |                         "default": "compact"
101 |                     },
102 |                     "start_date": {
103 |                         "type": "string",
104 |                         "description": "Optional: Start date in YYYY-MM-DD format for filtering results",
105 |                         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
106 |                     },
107 |                     "end_date": {
108 |                         "type": "string",
109 |                         "description": "Optional: End date in YYYY-MM-DD format for filtering results",
110 |                         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
111 |                     },
112 |                     "limit": {
113 |                         "type": "integer",
114 |                         "description": "Optional: Number of data points to return when no date filtering is applied (default: 5)",
115 |                         "default": 5,
116 |                         "minimum": 1
117 |                     }
118 |                 },
119 |                 "required": ["symbol"],
120 |             },
121 |         ),
122 |         types.Tool(
123 |             name="get-historical-options",
124 |             description="Get historical options chain data for a stock with advanced filtering and sorting capabilities",
125 |             inputSchema={
126 |                 "type": "object",
127 |                 "properties": {
128 |                     "symbol": {
129 |                         "type": "string",
130 |                         "description": "Stock symbol (e.g., AAPL, MSFT)",
131 |                     },
132 |                     "date": {
133 |                         "type": "string",
134 |                         "description": "Optional: Trading date in YYYY-MM-DD format (defaults to previous trading day, must be after 2008-01-01)",
135 |                         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
136 |                     },
137 |                     "expiry_date": {
138 |                         "type": "string",
139 |                         "description": "Optional: Filter by expiration date in YYYY-MM-DD format",
140 |                         "pattern": "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$"
141 |                     },
142 |                     "min_strike": {
143 |                         "type": "number",
144 |                         "description": "Optional: Minimum strike price filter (e.g., 100.00)",
145 |                         "minimum": 0
146 |                     },
147 |                     "max_strike": {
148 |                         "type": "number",
149 |                         "description": "Optional: Maximum strike price filter (e.g., 200.00)",
150 |                         "minimum": 0
151 |                     },
152 |                     "contract_id": {
153 |                         "type": "string",
154 |                         "description": "Optional: Filter by specific contract ID (e.g., MSTR260116C00000500)"
155 |                     },
156 |                     "contract_type": {
157 |                         "type": "string",
158 |                         "description": "Optional: Filter by contract type (call or put)",
159 |                         "enum": ["call", "put", "C", "P"]
160 |                     },
161 |                     "limit": {
162 |                         "type": "integer",
163 |                         "description": "Optional: Number of contracts to return after filtering (default: 10, use -1 for all contracts)",
164 |                         "default": 10,
165 |                         "minimum": -1
166 |                     },
167 |                     "sort_by": {
168 |                         "type": "string",
169 |                         "description": "Optional: Field to sort by",
170 |                         "enum": [
171 |                             "strike",
172 |                             "expiration",
173 |                             "volume",
174 |                             "open_interest",
175 |                             "implied_volatility",
176 |                             "delta",
177 |                             "gamma",
178 |                             "theta",
179 |                             "vega",
180 |                             "rho",
181 |                             "last",
182 |                             "bid",
183 |                             "ask"
184 |                         ],
185 |                         "default": "strike"
186 |                     },
187 |                     "sort_order": {
188 |                         "type": "string",
189 |                         "description": "Optional: Sort order",
190 |                         "enum": ["asc", "desc"],
191 |                         "default": "asc"
192 |                     }
193 |                 },
194 |                 "required": ["symbol"],
195 |             },
196 |         ),
197 |         types.Tool(
198 |             name="get-crypto-daily",
199 |             description="Get daily time series data for a cryptocurrency",
200 |             inputSchema={
201 |                 "type": "object",
202 |                 "properties": {
203 |                     "symbol": {
204 |                         "type": "string",
205 |                         "description": "Cryptocurrency symbol (e.g., BTC, ETH)",
206 |                     },
207 |                     "market": {
208 |                         "type": "string",
209 |                         "description": "Market currency (e.g., USD, EUR)",
210 |                         "default": "USD"
211 |                     }
212 |                 },
213 |                 "required": ["symbol"],
214 |             },
215 |         ),
216 |         types.Tool(
217 |             name="get-crypto-weekly",
218 |             description="Get weekly time series data for a cryptocurrency",
219 |             inputSchema={
220 |                 "type": "object",
221 |                 "properties": {
222 |                     "symbol": {
223 |                         "type": "string",
224 |                         "description": "Cryptocurrency symbol (e.g., BTC, ETH)",
225 |                     },
226 |                     "market": {
227 |                         "type": "string",
228 |                         "description": "Market currency (e.g., USD, EUR)",
229 |                         "default": "USD"
230 |                     }
231 |                 },
232 |                 "required": ["symbol"],
233 |             },
234 |         ),
235 |         types.Tool(
236 |             name="get-crypto-monthly",
237 |             description="Get monthly time series data for a cryptocurrency",
238 |             inputSchema={
239 |                 "type": "object",
240 |                 "properties": {
241 |                     "symbol": {
242 |                         "type": "string",
243 |                         "description": "Cryptocurrency symbol (e.g., BTC, ETH)",
244 |                     },
245 |                     "market": {
246 |                         "type": "string",
247 |                         "description": "Market currency (e.g., USD, EUR)",
248 |                         "default": "USD"
249 |                     }
250 |                 },
251 |                 "required": ["symbol"],
252 |             },
253 |         ),
254 |         types.Tool(
255 |             name="get-earnings-calendar",
256 |             description="Get upcoming earnings calendar data for companies with sorting capabilities",
257 |             inputSchema={
258 |                 "type": "object",
259 |                 "properties": {
260 |                     "symbol": {
261 |                         "type": "string",
262 |                         "description": "Optional: Stock symbol to filter earnings for a specific company (e.g., AAPL, MSFT, IBM)"
263 |                     },
264 |                     "horizon": {
265 |                         "type": "string",
266 |                         "description": "Optional: Time horizon for earnings data (3month, 6month, or 12month)",
267 |                         "enum": ["3month", "6month", "12month"],
268 |                         "default": "12month"
269 |                     },
270 |                     "limit": {
271 |                         "type": "integer",
272 |                         "description": "Optional: Number of earnings entries to return (default: 100)",
273 |                         "default": 100,
274 |                         "minimum": 1
275 |                     },
276 |                     "sort_by": {
277 |                         "type": "string",
278 |                         "description": "Optional: Field to sort by",
279 |                         "enum": ["reportDate", "symbol", "name", "fiscalDateEnding", "estimate"],
280 |                         "default": "reportDate"
281 |                     },
282 |                     "sort_order": {
283 |                         "type": "string",
284 |                         "description": "Optional: Sort order",
285 |                         "enum": ["asc", "desc"],
286 |                         "default": "desc"
287 |                     }
288 |                 },
289 |                 "required": [],
290 |             },
291 |         ),
292 |         types.Tool(
293 |             name="get-historical-earnings",
294 |             description="Get historical earnings data for a specific company",
295 |             inputSchema={
296 |                 "type": "object",
297 |                 "properties": {
298 |                     "symbol": {
299 |                         "type": "string",
300 |                         "description": "Stock symbol for the company (e.g., AAPL, MSFT, IBM)"
301 |                     },
302 |                     "limit_annual": {
303 |                         "type": "integer",
304 |                         "description": "Optional: Number of annual earnings to return (default: 5)",
305 |                         "default": 5,
306 |                         "minimum": 1
307 |                     },
308 |                     "limit_quarterly": {
309 |                         "type": "integer",
310 |                         "description": "Optional: Number of quarterly earnings to return (default: 8)",
311 |                         "default": 8,
312 |                         "minimum": 1
313 |                     }
314 |                 },
315 |                 "required": ["symbol"],
316 |             },
317 |         ),
318 |         types.Tool(
319 |             name="get-realtime-options",
320 |             description="Get realtime options chain data for a stock with optional Greeks and filtering",
321 |             inputSchema={
322 |                 "type": "object",
323 |                 "properties": {
324 |                     "symbol": {
325 |                         "type": "string",
326 |                         "description": "Stock symbol (e.g., AAPL, MSFT)",
327 |                     },
328 |                     "require_greeks": {
329 |                         "type": "boolean",
330 |                         "description": "Optional: Enable Greeks and implied volatility calculation (default: false)",
331 |                         "default": False
332 |                     },
333 |                     "contract": {
334 |                         "type": "string",
335 |                         "description": "Optional: Specific options contract ID to retrieve"
336 |                     },
337 |                     "datatype": {
338 |                         "type": "string",
339 |                         "description": "Optional: Response format (json or csv, default: json)",
340 |                         "enum": ["json", "csv"],
341 |                         "default": "json"
342 |                     }
343 |                 },
344 |                 "required": ["symbol"],
345 |             },
346 |         ),
347 |         types.Tool(
348 |             name="get-etf-profile",
349 |             description="Get comprehensive ETF profile information including holdings, sector allocation, and key metrics",
350 |             inputSchema={
351 |                 "type": "object",
352 |                 "properties": {
353 |                     "symbol": {
354 |                         "type": "string",
355 |                         "description": "ETF symbol (e.g., QQQ, SPY, VTI)",
356 |                     }
357 |                 },
358 |                 "required": ["symbol"],
359 |             },
360 |         )
361 |     ]
362 | 
363 | @server.call_tool()
364 | async def handle_call_tool(
365 |     name: str, arguments: dict | None
366 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
367 |     """
368 |     Handle tool execution requests.
369 |     Tools can fetch financial data and notify clients of changes.
370 |     """
371 |     if not arguments:
372 |         return [types.TextContent(type="text", text="Missing arguments for the request")]
373 | 
374 |     if name == "get-stock-quote":
375 |         symbol = arguments.get("symbol")
376 |         if not symbol:
377 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
378 | 
379 |         symbol = symbol.upper()
380 | 
381 |         async with httpx.AsyncClient() as client:
382 |             quote_data = await make_alpha_request(
383 |                 client,
384 |                 "GLOBAL_QUOTE",
385 |                 symbol
386 |             )
387 | 
388 |             if isinstance(quote_data, str):
389 |                 return [types.TextContent(type="text", text=f"Error: {quote_data}")]
390 | 
391 |             formatted_quote = format_quote(quote_data)
392 |             quote_text = f"Stock quote for {symbol}:\n\n{formatted_quote}"
393 | 
394 |             return [types.TextContent(type="text", text=quote_text)]
395 | 
396 |     elif name == "get-company-info":
397 |         symbol = arguments.get("symbol")
398 |         if not symbol:
399 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
400 | 
401 |         symbol = symbol.upper()
402 | 
403 |         async with httpx.AsyncClient() as client:
404 |             company_data = await make_alpha_request(
405 |                 client,
406 |                 "OVERVIEW",
407 |                 symbol
408 |             )
409 | 
410 |             if isinstance(company_data, str):
411 |                 return [types.TextContent(type="text", text=f"Error: {company_data}")]
412 | 
413 |             formatted_info = format_company_info(company_data)
414 |             info_text = f"Company information for {symbol}:\n\n{formatted_info}"
415 | 
416 |             return [types.TextContent(type="text", text=info_text)]
417 | 
418 |     elif name == "get-crypto-exchange-rate":
419 |         crypto_symbol = arguments.get("crypto_symbol")
420 |         if not crypto_symbol:
421 |             return [types.TextContent(type="text", text="Missing crypto_symbol parameter")]
422 | 
423 |         market = arguments.get("market", "USD")
424 |         crypto_symbol = crypto_symbol.upper()
425 |         market = market.upper()
426 | 
427 |         async with httpx.AsyncClient() as client:
428 |             crypto_data = await make_alpha_request(
429 |                 client,
430 |                 "CURRENCY_EXCHANGE_RATE",
431 |                 None,
432 |                 {
433 |                     "from_currency": crypto_symbol,
434 |                     "to_currency": market
435 |                 }
436 |             )
437 | 
438 |             if isinstance(crypto_data, str):
439 |                 return [types.TextContent(type="text", text=f"Error: {crypto_data}")]
440 | 
441 |             formatted_rate = format_crypto_rate(crypto_data)
442 |             rate_text = f"Cryptocurrency exchange rate for {crypto_symbol}/{market}:\n\n{formatted_rate}"
443 | 
444 |             return [types.TextContent(type="text", text=rate_text)]
445 | 
446 |     elif name == "get-time-series":
447 |         symbol = arguments.get("symbol")
448 |         if not symbol:
449 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
450 | 
451 |         symbol = symbol.upper()
452 |         start_date = arguments.get("start_date")
453 |         end_date = arguments.get("end_date")
454 |         limit = arguments.get("limit", 5)
455 |         
456 |         # Auto-select outputsize: use 'full' when date filtering is requested
457 |         outputsize = arguments.get("outputsize")
458 |         if not outputsize:
459 |             outputsize = "full" if (start_date or end_date) else "compact"
460 | 
461 |         async with httpx.AsyncClient() as client:
462 |             time_series_data = await make_alpha_request(
463 |                 client,
464 |                 "TIME_SERIES_DAILY",
465 |                 symbol,
466 |                 {"outputsize": outputsize}
467 |             )
468 | 
469 |             if isinstance(time_series_data, str):
470 |                 return [types.TextContent(type="text", text=f"Error: {time_series_data}")]
471 | 
472 |             formatted_series = format_time_series(time_series_data, start_date, end_date, limit)
473 |             series_text = f"Time series data for {symbol}:\n\n{formatted_series}"
474 | 
475 |             return [types.TextContent(type="text", text=series_text)]
476 | 
477 |     elif name == "get-historical-options":
478 |         symbol = arguments.get("symbol")
479 |         date = arguments.get("date")
480 |         expiry_date = arguments.get("expiry_date")
481 |         min_strike = arguments.get("min_strike")
482 |         max_strike = arguments.get("max_strike")
483 |         contract_id = arguments.get("contract_id")
484 |         contract_type = arguments.get("contract_type")
485 |         limit = arguments.get("limit", 10)
486 |         sort_by = arguments.get("sort_by", "strike")
487 |         sort_order = arguments.get("sort_order", "asc")
488 | 
489 |         if not symbol:
490 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
491 | 
492 |         symbol = symbol.upper()
493 | 
494 |         async with httpx.AsyncClient() as client:
495 |             params = {}
496 |             if date:
497 |                 params["date"] = date
498 | 
499 |             options_data = await make_alpha_request(
500 |                 client,
501 |                 "HISTORICAL_OPTIONS",
502 |                 symbol,
503 |                 params
504 |             )
505 | 
506 |             if isinstance(options_data, str):
507 |                 return [types.TextContent(type="text", text=f"Error: {options_data}")]
508 | 
509 |             formatted_options = format_historical_options(
510 |                 options_data, 
511 |                 limit, 
512 |                 sort_by, 
513 |                 sort_order,
514 |                 expiry_date,
515 |                 min_strike,
516 |                 max_strike,
517 |                 contract_id,
518 |                 contract_type
519 |             )
520 |             options_text = f"Historical options data for {symbol}"
521 |             if date:
522 |                 options_text += f" on {date}"
523 |             options_text += f":\n\n{formatted_options}"
524 | 
525 |             return [types.TextContent(type="text", text=options_text)]
526 |             
527 |     elif name == "get-crypto-daily":
528 |         symbol = arguments.get("symbol")
529 |         market = arguments.get("market", "USD")
530 |         
531 |         if not symbol:
532 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
533 | 
534 |         symbol = symbol.upper()
535 |         market = market.upper()
536 | 
537 |         async with httpx.AsyncClient() as client:
538 |             crypto_data = await make_alpha_request(
539 |                 client,
540 |                 "DIGITAL_CURRENCY_DAILY",
541 |                 symbol,
542 |                 {"market": market}
543 |             )
544 | 
545 |             if isinstance(crypto_data, str):
546 |                 return [types.TextContent(type="text", text=f"Error: {crypto_data}")]
547 | 
548 |             formatted_data = format_crypto_time_series(crypto_data, "daily")
549 |             data_text = f"Daily cryptocurrency time series for {symbol} in {market}:\n\n{formatted_data}"
550 | 
551 |             return [types.TextContent(type="text", text=data_text)]
552 |             
553 |     elif name == "get-crypto-weekly":
554 |         symbol = arguments.get("symbol")
555 |         market = arguments.get("market", "USD")
556 |         
557 |         if not symbol:
558 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
559 | 
560 |         symbol = symbol.upper()
561 |         market = market.upper()
562 | 
563 |         async with httpx.AsyncClient() as client:
564 |             crypto_data = await make_alpha_request(
565 |                 client,
566 |                 "DIGITAL_CURRENCY_WEEKLY",
567 |                 symbol,
568 |                 {"market": market}
569 |             )
570 | 
571 |             if isinstance(crypto_data, str):
572 |                 return [types.TextContent(type="text", text=f"Error: {crypto_data}")]
573 | 
574 |             formatted_data = format_crypto_time_series(crypto_data, "weekly")
575 |             data_text = f"Weekly cryptocurrency time series for {symbol} in {market}:\n\n{formatted_data}"
576 | 
577 |             return [types.TextContent(type="text", text=data_text)]
578 |             
579 |     elif name == "get-crypto-monthly":
580 |         symbol = arguments.get("symbol")
581 |         market = arguments.get("market", "USD")
582 |         
583 |         if not symbol:
584 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
585 | 
586 |         symbol = symbol.upper()
587 |         market = market.upper()
588 | 
589 |         async with httpx.AsyncClient() as client:
590 |             crypto_data = await make_alpha_request(
591 |                 client,
592 |                 "DIGITAL_CURRENCY_MONTHLY",
593 |                 symbol,
594 |                 {"market": market}
595 |             )
596 | 
597 |             if isinstance(crypto_data, str):
598 |                 return [types.TextContent(type="text", text=f"Error: {crypto_data}")]
599 | 
600 |             formatted_data = format_crypto_time_series(crypto_data, "monthly")
601 |             data_text = f"Monthly cryptocurrency time series for {symbol} in {market}:\n\n{formatted_data}"
602 | 
603 |             return [types.TextContent(type="text", text=data_text)]
604 |             
605 |     elif name == "get-earnings-calendar":
606 |         symbol = arguments.get("symbol")
607 |         horizon = arguments.get("horizon", "12month")
608 |         limit = arguments.get("limit", 100)
609 |         sort_by = arguments.get("sort_by", "reportDate")
610 |         sort_order = arguments.get("sort_order", "desc")
611 |         
612 |         async with httpx.AsyncClient() as client:
613 |             params = {"horizon": horizon}
614 |             if symbol:
615 |                 params["symbol"] = symbol.upper()
616 |                 
617 |             earnings_data = await make_alpha_request(
618 |                 client,
619 |                 "EARNINGS_CALENDAR",
620 |                 None,
621 |                 params
622 |             )
623 | 
624 |             if isinstance(earnings_data, str):
625 |                 return [types.TextContent(type="text", text=f"Error: {earnings_data}")]
626 | 
627 |             formatted_earnings = format_earnings_calendar(earnings_data, limit, sort_by, sort_order)
628 |             earnings_text = f"Earnings calendar"
629 |             if symbol:
630 |                 earnings_text += f" for {symbol.upper()}"
631 |             if horizon:
632 |                 earnings_text += f" ({horizon})"
633 |             earnings_text += f":\n\n{formatted_earnings}"
634 | 
635 |             return [types.TextContent(type="text", text=earnings_text)]
636 |             
637 |     elif name == "get-historical-earnings":
638 |         symbol = arguments.get("symbol")
639 |         limit_annual = arguments.get("limit_annual", 5)
640 |         limit_quarterly = arguments.get("limit_quarterly", 8)
641 |         
642 |         if not symbol:
643 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
644 | 
645 |         symbol = symbol.upper()
646 | 
647 |         async with httpx.AsyncClient() as client:
648 |             earnings_data = await make_alpha_request(
649 |                 client,
650 |                 "EARNINGS",
651 |                 symbol
652 |             )
653 | 
654 |             if isinstance(earnings_data, str):
655 |                 return [types.TextContent(type="text", text=f"Error: {earnings_data}")]
656 | 
657 |             formatted_earnings = format_historical_earnings(earnings_data, limit_annual, limit_quarterly)
658 |             earnings_text = f"Historical earnings for {symbol}:\n\n{formatted_earnings}"
659 | 
660 |             return [types.TextContent(type="text", text=earnings_text)]
661 |             
662 |     elif name == "get-realtime-options":
663 |         symbol = arguments.get("symbol")
664 |         require_greeks = arguments.get("require_greeks", False)
665 |         contract = arguments.get("contract")
666 |         datatype = arguments.get("datatype", "json")
667 |         
668 |         if not symbol:
669 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
670 | 
671 |         symbol = symbol.upper()
672 | 
673 |         async with httpx.AsyncClient() as client:
674 |             params = {}
675 |             if require_greeks:
676 |                 params["require_greeks"] = "true"
677 |             if contract:
678 |                 params["contract"] = contract
679 |             if datatype:
680 |                 params["datatype"] = datatype
681 | 
682 |             options_data = await make_alpha_request(
683 |                 client,
684 |                 "REALTIME_OPTIONS",
685 |                 symbol,
686 |                 params
687 |             )
688 | 
689 |             if isinstance(options_data, str):
690 |                 return [types.TextContent(type="text", text=f"Error: {options_data}")]
691 | 
692 |             formatted_options = format_realtime_options(options_data)
693 |             options_text = f"Realtime options data for {symbol}:\n\n{formatted_options}"
694 | 
695 |             return [types.TextContent(type="text", text=options_text)]
696 |             
697 |     elif name == "get-etf-profile":
698 |         symbol = arguments.get("symbol")
699 |         
700 |         if not symbol:
701 |             return [types.TextContent(type="text", text="Missing symbol parameter")]
702 | 
703 |         symbol = symbol.upper()
704 | 
705 |         async with httpx.AsyncClient() as client:
706 |             etf_data = await make_alpha_request(
707 |                 client,
708 |                 "ETF_PROFILE",
709 |                 symbol
710 |             )
711 | 
712 |             if isinstance(etf_data, str):
713 |                 return [types.TextContent(type="text", text=f"Error: {etf_data}")]
714 | 
715 |             formatted_etf = format_etf_profile(etf_data)
716 |             etf_text = f"ETF profile for {symbol}:\n\n{formatted_etf}"
717 | 
718 |             return [types.TextContent(type="text", text=etf_text)]
719 |     else:
720 |         return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
721 | 
722 | async def main():
723 |     async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
724 |         await server.run(
725 |             read_stream,
726 |             write_stream,
727 |             InitializationOptions(
728 |                 server_name="alpha_vantage_finance",
729 |                 server_version="0.1.0",
730 |                 capabilities=server.get_capabilities(
731 |                     notification_options=NotificationOptions(),
732 |                     experimental_capabilities={},
733 |                 ),
734 |             ),
735 |         )
736 | 
737 | # This is needed if you'd like to connect to a custom client
738 | if __name__ == "__main__":
739 |     asyncio.run(main())
740 | 
```

--------------------------------------------------------------------------------
/src/alpha_vantage_mcp/tools.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Alpha Vantage MCP Tools Module
  3 | 
  4 | This module contains utility functions for making requests to the Alpha Vantage API
  5 | and formatting the responses.
  6 | """
  7 | 
  8 | from typing import Any, Dict, Optional, List
  9 | import httpx
 10 | import os
 11 | import csv
 12 | import io
 13 | from datetime import datetime
 14 | 
 15 | ALPHA_VANTAGE_BASE = "https://www.alphavantage.co/query"
 16 | API_KEY = os.getenv('ALPHA_VANTAGE_API_KEY')
 17 | 
 18 | async def make_alpha_request(client: httpx.AsyncClient, function: str, symbol: Optional[str], additional_params: Optional[Dict[str, Any]] = None) -> Dict[str, Any] | str:
 19 |     """Make a request to the Alpha Vantage API with proper error handling.
 20 |     
 21 |     Args:
 22 |         client: An httpx AsyncClient instance
 23 |         function: The Alpha Vantage API function to call
 24 |         symbol: The stock/crypto symbol (can be None for some endpoints)
 25 |         additional_params: Additional parameters to include in the request
 26 |         
 27 |     Returns:
 28 |         Either a dictionary containing the API response, or a string with an error message
 29 |     """
 30 |     params = {
 31 |         "function": function,
 32 |         "apikey": API_KEY
 33 |     }
 34 |     
 35 |     if symbol:
 36 |         params["symbol"] = symbol
 37 |         
 38 |     if additional_params:
 39 |         params.update(additional_params)
 40 | 
 41 |     try:
 42 |         response = await client.get(
 43 |             ALPHA_VANTAGE_BASE,
 44 |             params=params,
 45 |             timeout=30.0
 46 |         )
 47 | 
 48 |         # Check for specific error responses
 49 |         if response.status_code == 429:
 50 |             return f"Rate limit exceeded. Error details: {response.text}"
 51 |         elif response.status_code == 403:
 52 |             return f"API key invalid or expired. Error details: {response.text}"
 53 | 
 54 |         response.raise_for_status()
 55 | 
 56 |         # Check if response is empty
 57 |         if not response.text.strip():
 58 |             return "Empty response received from Alpha Vantage API"
 59 |         
 60 |         # Special handling for EARNINGS_CALENDAR which returns CSV by default
 61 |         if function == "EARNINGS_CALENDAR":
 62 |             try:
 63 |                 # Parse CSV response
 64 |                 csv_reader = csv.DictReader(io.StringIO(response.text))
 65 |                 earnings_list = list(csv_reader)
 66 |                 return earnings_list
 67 |             except Exception as e:
 68 |                 return f"Error parsing CSV response: {str(e)}"
 69 |         
 70 |         # For other functions, expect JSON
 71 |         try:
 72 |             data = response.json()
 73 |         except ValueError as e:
 74 |             return f"Invalid JSON response from Alpha Vantage API: {response.text[:200]}"
 75 | 
 76 |         # Check for Alpha Vantage specific error messages
 77 |         if "Error Message" in data:
 78 |             return f"Alpha Vantage API error: {data['Error Message']}"
 79 |         if "Note" in data and "API call frequency" in data["Note"]:
 80 |             return f"Rate limit warning: {data['Note']}"
 81 | 
 82 |         return data
 83 |     except httpx.TimeoutException:
 84 |         return "Request timed out after 30 seconds. The Alpha Vantage API may be experiencing delays."
 85 |     except httpx.ConnectError:
 86 |         return "Failed to connect to Alpha Vantage API. Please check your internet connection."
 87 |     except httpx.HTTPStatusError as e:
 88 |         return f"HTTP error occurred: {str(e)} - Response: {e.response.text}"
 89 |     except Exception as e:
 90 |         return f"Unexpected error occurred: {str(e)}"
 91 | 
 92 | 
 93 | def format_quote(quote_data: Dict[str, Any]) -> str:
 94 |     """Format quote data into a concise string.
 95 |     
 96 |     Args:
 97 |         quote_data: The response data from the Alpha Vantage Global Quote endpoint
 98 |         
 99 |     Returns:
100 |         A formatted string containing the quote information
101 |     """
102 |     try:
103 |         global_quote = quote_data.get("Global Quote", {})
104 |         if not global_quote:
105 |             return "No quote data available in the response"
106 | 
107 |         return (
108 |             f"Price: ${global_quote.get('05. price', 'N/A')}\n"
109 |             f"Change: ${global_quote.get('09. change', 'N/A')} "
110 |             f"({global_quote.get('10. change percent', 'N/A')})\n"
111 |             f"Volume: {global_quote.get('06. volume', 'N/A')}\n"
112 |             f"High: ${global_quote.get('03. high', 'N/A')}\n"
113 |             f"Low: ${global_quote.get('04. low', 'N/A')}\n"
114 |             "---"
115 |         )
116 |     except Exception as e:
117 |         return f"Error formatting quote data: {str(e)}"
118 | 
119 | 
120 | def format_company_info(overview_data: Dict[str, Any]) -> str:
121 |     """Format company information into a concise string.
122 |     
123 |     Args:
124 |         overview_data: The response data from the Alpha Vantage OVERVIEW endpoint
125 |         
126 |     Returns:
127 |         A formatted string containing the company information
128 |     """
129 |     try:
130 |         if not overview_data:
131 |             return "No company information available in the response"
132 | 
133 |         return (
134 |             f"Name: {overview_data.get('Name', 'N/A')}\n"
135 |             f"Sector: {overview_data.get('Sector', 'N/A')}\n"
136 |             f"Industry: {overview_data.get('Industry', 'N/A')}\n"
137 |             f"Market Cap: ${overview_data.get('MarketCapitalization', 'N/A')}\n"
138 |             f"Description: {overview_data.get('Description', 'N/A')}\n"
139 |             f"Exchange: {overview_data.get('Exchange', 'N/A')}\n"
140 |             f"Currency: {overview_data.get('Currency', 'N/A')}\n"
141 |             "---"
142 |         )
143 |     except Exception as e:
144 |         return f"Error formatting company data: {str(e)}"
145 | 
146 | 
147 | def format_crypto_rate(crypto_data: Dict[str, Any]) -> str:
148 |     """Format cryptocurrency exchange rate data into a concise string.
149 |     
150 |     Args:
151 |         crypto_data: The response data from the Alpha Vantage CURRENCY_EXCHANGE_RATE endpoint
152 |         
153 |     Returns:
154 |         A formatted string containing the cryptocurrency exchange rate information
155 |     """
156 |     try:
157 |         realtime_data = crypto_data.get("Realtime Currency Exchange Rate", {})
158 |         if not realtime_data:
159 |             return "No exchange rate data available in the response"
160 | 
161 |         return (
162 |             f"From: {realtime_data.get('2. From_Currency Name', 'N/A')} ({realtime_data.get('1. From_Currency Code', 'N/A')})\n"
163 |             f"To: {realtime_data.get('4. To_Currency Name', 'N/A')} ({realtime_data.get('3. To_Currency Code', 'N/A')})\n"
164 |             f"Exchange Rate: {realtime_data.get('5. Exchange Rate', 'N/A')}\n"
165 |             f"Last Updated: {realtime_data.get('6. Last Refreshed', 'N/A')} {realtime_data.get('7. Time Zone', 'N/A')}\n"
166 |             f"Bid Price: {realtime_data.get('8. Bid Price', 'N/A')}\n"
167 |             f"Ask Price: {realtime_data.get('9. Ask Price', 'N/A')}\n"
168 |             "---"
169 |         )
170 |     except Exception as e:
171 |         return f"Error formatting cryptocurrency data: {str(e)}"
172 | 
173 | 
174 | def format_time_series(time_series_data: Dict[str, Any], start_date: Optional[str] = None, end_date: Optional[str] = None, limit: int = 5) -> str:
175 |     """Format time series data into a concise string with optional date filtering.
176 |     
177 |     Args:
178 |         time_series_data: The response data from the Alpha Vantage TIME_SERIES_DAILY endpoint
179 |         start_date: Optional start date in YYYY-MM-DD format for filtering
180 |         end_date: Optional end date in YYYY-MM-DD format for filtering  
181 |         limit: Number of data points to return when no date filtering is applied
182 |         
183 |     Returns:
184 |         A formatted string containing the time series information
185 |     """
186 |     try:
187 |         # Get the daily time series data
188 |         time_series = time_series_data.get("Time Series (Daily)", {})
189 |         if not time_series:
190 |             return "No time series data available in the response"
191 | 
192 |         # Get metadata
193 |         metadata = time_series_data.get("Meta Data", {})
194 |         symbol = metadata.get("2. Symbol", "Unknown")
195 |         last_refreshed = metadata.get("3. Last Refreshed", "Unknown")
196 | 
197 |         # Filter by date range if specified
198 |         filtered_data = {}
199 |         if start_date or end_date:
200 |             for date_str, values in time_series.items():
201 |                 try:
202 |                     date_obj = datetime.strptime(date_str, "%Y-%m-%d")
203 |                     
204 |                     # Check start date filter
205 |                     if start_date:
206 |                         start_obj = datetime.strptime(start_date, "%Y-%m-%d")
207 |                         if date_obj < start_obj:
208 |                             continue
209 |                     
210 |                     # Check end date filter
211 |                     if end_date:
212 |                         end_obj = datetime.strptime(end_date, "%Y-%m-%d")
213 |                         if date_obj > end_obj:
214 |                             continue
215 |                     
216 |                     filtered_data[date_str] = values
217 |                 except ValueError:
218 |                     # Skip invalid date formats
219 |                     continue
220 |             
221 |             # Sort filtered data by date (most recent first)
222 |             sorted_items = sorted(filtered_data.items(), key=lambda x: x[0], reverse=True)
223 |         else:
224 |             # Use original data with limit
225 |             sorted_items = list(time_series.items())[:limit]
226 | 
227 |         if not sorted_items:
228 |             return f"No time series data found for the specified date range"
229 | 
230 |         # Build header
231 |         formatted_data = [
232 |             f"Time Series Data for {symbol} (Last Refreshed: {last_refreshed})\n"
233 |         ]
234 |         
235 |         # Add date range info if filtering was applied
236 |         if start_date or end_date:
237 |             date_range = ""
238 |             if start_date and end_date:
239 |                 date_range = f"Date Range: {start_date} to {end_date}"
240 |             elif start_date:
241 |                 date_range = f"From: {start_date}"
242 |             elif end_date:
243 |                 date_range = f"Until: {end_date}"
244 |             formatted_data.append(f"{date_range} ({len(sorted_items)} data points)\n\n")
245 |         else:
246 |             formatted_data.append(f"(Showing {len(sorted_items)} most recent data points)\n\n")
247 | 
248 |         # Format the data points
249 |         for date, values in sorted_items:
250 |             formatted_data.append(
251 |                 f"Date: {date}\n"
252 |                 f"Open: ${values.get('1. open', 'N/A')}\n"
253 |                 f"High: ${values.get('2. high', 'N/A')}\n"
254 |                 f"Low: ${values.get('3. low', 'N/A')}\n"
255 |                 f"Close: ${values.get('4. close', 'N/A')}\n"
256 |                 f"Volume: {values.get('5. volume', 'N/A')}\n"
257 |                 "---\n"
258 |             )
259 | 
260 |         return "\n".join(formatted_data)
261 |     except Exception as e:
262 |         return f"Error formatting time series data: {str(e)}"
263 | 
264 | 
265 | def format_crypto_time_series(time_series_data: Dict[str, Any], series_type: str) -> str:
266 |     """Format cryptocurrency time series data into a concise string.
267 |     
268 |     Args:
269 |         time_series_data: The response data from Alpha Vantage Digital Currency endpoints
270 |         series_type: Type of time series (daily, weekly, monthly)
271 |         
272 |     Returns:
273 |         A formatted string containing the cryptocurrency time series information
274 |     """
275 |     try:
276 |         # Determine the time series key based on series_type
277 |         time_series_key = ""
278 |         if series_type == "daily":
279 |             time_series_key = "Time Series (Digital Currency Daily)"
280 |         elif series_type == "weekly":
281 |             time_series_key = "Time Series (Digital Currency Weekly)"
282 |         elif series_type == "monthly":
283 |             time_series_key = "Time Series (Digital Currency Monthly)"
284 |         else:
285 |             return f"Unknown series type: {series_type}"
286 |             
287 |         # Get the time series data
288 |         time_series = time_series_data.get(time_series_key, {})
289 |         if not time_series:
290 |             all_keys = ", ".join(time_series_data.keys())
291 |             return f"No cryptocurrency time series data found with key: '{time_series_key}'.\nAvailable keys: {all_keys}"
292 | 
293 |         # Get metadata
294 |         metadata = time_series_data.get("Meta Data", {})
295 |         crypto_symbol = metadata.get("2. Digital Currency Code", "Unknown")
296 |         crypto_name = metadata.get("3. Digital Currency Name", "Unknown")
297 |         market = metadata.get("4. Market Code", "Unknown")
298 |         market_name = metadata.get("5. Market Name", "Unknown")
299 |         last_refreshed = metadata.get("6. Last Refreshed", "Unknown")
300 |         time_zone = metadata.get("7. Time Zone", "Unknown")
301 | 
302 |         # Format the header
303 |         formatted_data = [
304 |             f"{series_type.capitalize()} Time Series for {crypto_name} ({crypto_symbol})",
305 |             f"Market: {market_name} ({market})",
306 |             f"Last Refreshed: {last_refreshed} {time_zone}",
307 |             ""
308 |         ]
309 | 
310 |         # Format the most recent 5 data points
311 |         for date, values in list(time_series.items())[:5]:
312 |             # Get price information - based on the API response, we now know the correct field names
313 |             open_price = values.get("1. open", "N/A")
314 |             high_price = values.get("2. high", "N/A")
315 |             low_price = values.get("3. low", "N/A")
316 |             close_price = values.get("4. close", "N/A")
317 |             volume = values.get("5. volume", "N/A")
318 |             
319 |             formatted_data.append(f"Date: {date}")
320 |             formatted_data.append(f"Open: {open_price} {market}")
321 |             formatted_data.append(f"High: {high_price} {market}")
322 |             formatted_data.append(f"Low: {low_price} {market}")
323 |             formatted_data.append(f"Close: {close_price} {market}")
324 |             formatted_data.append(f"Volume: {volume}")
325 |             formatted_data.append("---")
326 |         
327 |         return "\n".join(formatted_data)
328 |     except Exception as e:
329 |         return f"Error formatting cryptocurrency time series data: {str(e)}"
330 | 
331 | 
332 | def format_earnings_calendar(earnings_data: List[Dict[str, str]], limit: int = 100, sort_by: str = "reportDate", sort_order: str = "desc") -> str:
333 |     """Format earnings calendar data into a concise string with sorting.
334 |     
335 |     Args:
336 |         earnings_data: List of earnings records from the Alpha Vantage EARNINGS_CALENDAR endpoint (CSV format)
337 |         limit: Number of earnings entries to display (default: 100)
338 |         sort_by: Field to sort by (default: reportDate)
339 |         sort_order: Sort order asc or desc (default: desc)
340 |         
341 |     Returns:
342 |         A formatted string containing the sorted earnings calendar information
343 |     """
344 |     try:
345 |         if not isinstance(earnings_data, list):
346 |             return f"Unexpected data format: {type(earnings_data)}"
347 |             
348 |         if not earnings_data:
349 |             return "No earnings calendar data available"
350 | 
351 |         # Sort the earnings data
352 |         def get_sort_key(earning):
353 |             value = earning.get(sort_by, "")
354 |             
355 |             # Special handling for dates to ensure proper chronological sorting
356 |             if sort_by in ["reportDate", "fiscalDateEnding"]:
357 |                 try:
358 |                     # Convert date string to datetime for proper sorting
359 |                     if value:
360 |                         return datetime.strptime(value, "%Y-%m-%d")
361 |                     else:
362 |                         return datetime.min  # Put empty dates at the beginning
363 |                 except ValueError:
364 |                     return datetime.min
365 |             
366 |             # Special handling for numeric fields like estimate
367 |             elif sort_by == "estimate":
368 |                 try:
369 |                     if value and value.strip():
370 |                         return float(value)
371 |                     else:
372 |                         return 0.0
373 |                 except ValueError:
374 |                     return 0.0
375 |             
376 |             # For text fields (symbol, name), return as-is for alphabetical sorting
377 |             else:
378 |                 return str(value).upper()
379 | 
380 |         sorted_earnings = sorted(
381 |             earnings_data,
382 |             key=get_sort_key,
383 |             reverse=(sort_order == "desc")
384 |         )
385 | 
386 |         formatted = [f"Upcoming Earnings Calendar (Sorted by {sort_by} {sort_order}):\n\n"]
387 |         
388 |         # Display limited number of entries
389 |         display_earnings = sorted_earnings[:limit] if limit > 0 else sorted_earnings
390 |         
391 |         for earning in display_earnings:
392 |             symbol = earning.get('symbol', 'N/A')
393 |             name = earning.get('name', 'N/A')
394 |             report_date = earning.get('reportDate', 'N/A')
395 |             fiscal_date = earning.get('fiscalDateEnding', 'N/A')
396 |             estimate = earning.get('estimate', 'N/A')
397 |             currency = earning.get('currency', 'N/A')
398 |             
399 |             formatted.append(f"Company: {symbol} - {name}\n")
400 |             formatted.append(f"Report Date: {report_date}\n")
401 |             formatted.append(f"Fiscal Date End: {fiscal_date}\n")
402 |             
403 |             # Format estimate nicely
404 |             if estimate and estimate != 'N/A' and estimate.strip():
405 |                 try:
406 |                     est_float = float(estimate)
407 |                     formatted.append(f"Estimate: ${est_float:.2f} {currency}\n")
408 |                 except ValueError:
409 |                     formatted.append(f"Estimate: {estimate} {currency}\n")
410 |             else:
411 |                 formatted.append(f"Estimate: Not available\n")
412 |             
413 |             formatted.append("---\n")
414 |         
415 |         if limit > 0 and len(earnings_data) > limit:
416 |             formatted.append(f"\n... and {len(earnings_data) - limit} more earnings reports")
417 |             
418 |         return "".join(formatted)
419 |     except Exception as e:
420 |         return f"Error formatting earnings calendar data: {str(e)}"
421 | 
422 | 
423 | def format_historical_earnings(earnings_data: Dict[str, Any], limit_annual: int = 5, limit_quarterly: int = 8) -> str:
424 |     """Format historical earnings data into a concise string.
425 |     
426 |     Args:
427 |         earnings_data: The response data from the Alpha Vantage EARNINGS endpoint
428 |         limit_annual: Number of annual earnings to display (default: 5)
429 |         limit_quarterly: Number of quarterly earnings to display (default: 8)
430 |         
431 |     Returns:
432 |         A formatted string containing the historical earnings information
433 |     """
434 |     try:
435 |         if "Error Message" in earnings_data:
436 |             return f"Error: {earnings_data['Error Message']}"
437 | 
438 |         symbol = earnings_data.get("symbol", "Unknown")
439 |         formatted = [f"Historical Earnings for {symbol}:\n\n"]
440 |         
441 |         # Format Annual Earnings
442 |         annual_earnings = earnings_data.get("annualEarnings", [])
443 |         if annual_earnings:
444 |             formatted.append("=== ANNUAL EARNINGS ===\n")
445 |             display_annual = annual_earnings[:limit_annual] if limit_annual > 0 else annual_earnings
446 |             
447 |             for earning in display_annual:
448 |                 fiscal_date = earning.get("fiscalDateEnding", "N/A")
449 |                 reported_eps = earning.get("reportedEPS", "N/A")
450 |                 
451 |                 formatted.append(f"Fiscal Year End: {fiscal_date}\n")
452 |                 formatted.append(f"Reported EPS: ${reported_eps}\n")
453 |                 formatted.append("---\n")
454 |             
455 |             if limit_annual > 0 and len(annual_earnings) > limit_annual:
456 |                 formatted.append(f"... and {len(annual_earnings) - limit_annual} more annual reports\n")
457 |             formatted.append("\n")
458 |         
459 |         # Format Quarterly Earnings
460 |         quarterly_earnings = earnings_data.get("quarterlyEarnings", [])
461 |         if quarterly_earnings:
462 |             formatted.append("=== QUARTERLY EARNINGS ===\n")
463 |             display_quarterly = quarterly_earnings[:limit_quarterly] if limit_quarterly > 0 else quarterly_earnings
464 |             
465 |             for earning in display_quarterly:
466 |                 fiscal_date = earning.get("fiscalDateEnding", "N/A")
467 |                 reported_date = earning.get("reportedDate", "N/A")
468 |                 reported_eps = earning.get("reportedEPS", "N/A")
469 |                 estimated_eps = earning.get("estimatedEPS", "N/A")
470 |                 surprise = earning.get("surprise", "N/A")
471 |                 surprise_pct = earning.get("surprisePercentage", "N/A")
472 |                 report_time = earning.get("reportTime", "N/A")
473 |                 
474 |                 formatted.append(f"Fiscal Quarter End: {fiscal_date}\n")
475 |                 formatted.append(f"Reported Date: {reported_date}\n")
476 |                 formatted.append(f"Reported EPS: ${reported_eps}\n")
477 |                 formatted.append(f"Estimated EPS: ${estimated_eps}\n")
478 |                 
479 |                 # Format surprise with proper handling
480 |                 if surprise != "N/A" and surprise_pct != "N/A":
481 |                     try:
482 |                         surprise_float = float(surprise)
483 |                         surprise_pct_float = float(surprise_pct)
484 |                         if surprise_float >= 0:
485 |                             formatted.append(f"Surprise: +${surprise_float:.2f} (+{surprise_pct_float:.2f}%)\n")
486 |                         else:
487 |                             formatted.append(f"Surprise: ${surprise_float:.2f} ({surprise_pct_float:.2f}%)\n")
488 |                     except ValueError:
489 |                         formatted.append(f"Surprise: {surprise} ({surprise_pct}%)\n")
490 |                 else:
491 |                     formatted.append(f"Surprise: {surprise} ({surprise_pct}%)\n")
492 |                 
493 |                 formatted.append(f"Report Time: {report_time}\n")
494 |                 formatted.append("---\n")
495 |             
496 |             if limit_quarterly > 0 and len(quarterly_earnings) > limit_quarterly:
497 |                 formatted.append(f"... and {len(quarterly_earnings) - limit_quarterly} more quarterly reports\n")
498 |         
499 |         if not annual_earnings and not quarterly_earnings:
500 |             formatted.append("No historical earnings data available\n")
501 |             
502 |         return "".join(formatted)
503 |     except Exception as e:
504 |         return f"Error formatting historical earnings data: {str(e)}"
505 | 
506 | 
507 | def format_historical_options(
508 |     options_data: Dict[str, Any], 
509 |     limit: int = 10, 
510 |     sort_by: str = "strike", 
511 |     sort_order: str = "asc",
512 |     expiry_date: Optional[str] = None,
513 |     min_strike: Optional[float] = None,
514 |     max_strike: Optional[float] = None,
515 |     contract_id: Optional[str] = None,
516 |     contract_type: Optional[str] = None
517 | ) -> str:
518 |     """Format historical options chain data into a concise string with advanced filtering and sorting.
519 |     
520 |     Args:
521 |         options_data: The response data from the Alpha Vantage HISTORICAL_OPTIONS endpoint
522 |         limit: Number of contracts to return after filtering (-1 for all)
523 |         sort_by: Field to sort by
524 |         sort_order: Sort order (asc or desc)
525 |         expiry_date: Optional expiration date filter (YYYY-MM-DD)
526 |         min_strike: Optional minimum strike price filter
527 |         max_strike: Optional maximum strike price filter
528 |         contract_id: Optional specific contract ID filter
529 |         contract_type: Optional contract type filter (call/put/C/P)
530 |         
531 |     Returns:
532 |         A formatted string containing the filtered and sorted historical options information
533 |     """
534 |     try:
535 |         if "Error Message" in options_data:
536 |             return f"Error: {options_data['Error Message']}"
537 | 
538 |         options_chain = options_data.get("data", [])
539 | 
540 |         if not options_chain:
541 |             return "No options data available in the response"
542 | 
543 |         # Apply filters
544 |         filtered_chain = []
545 |         for contract in options_chain:
546 |             # Contract ID filter (exact match)
547 |             if contract_id and contract.get('contractID', '') != contract_id:
548 |                 continue
549 |             
550 |             # Expiry date filter
551 |             if expiry_date:
552 |                 contract_expiry = contract.get('expiration', '')
553 |                 if contract_expiry != expiry_date:
554 |                     continue
555 |             
556 |             # Strike price filters
557 |             if min_strike is not None or max_strike is not None:
558 |                 try:
559 |                     strike_str = str(contract.get('strike', '0')).replace('$', '').strip()
560 |                     if strike_str:
561 |                         strike_price = float(strike_str)
562 |                         if min_strike is not None and strike_price < min_strike:
563 |                             continue
564 |                         if max_strike is not None and strike_price > max_strike:
565 |                             continue
566 |                 except (ValueError, TypeError):
567 |                     continue
568 |             
569 |             # Contract type filter
570 |             if contract_type:
571 |                 contract_type_norm = contract_type.upper()
572 |                 # Handle both full names and single letters
573 |                 if contract_type_norm in ['CALL', 'C']:
574 |                     expected_types = ['call', 'C', 'CALL']
575 |                 elif contract_type_norm in ['PUT', 'P']:
576 |                     expected_types = ['put', 'P', 'PUT']
577 |                 else:
578 |                     expected_types = [contract_type]
579 |                 
580 |                 actual_type = contract.get('type', '')
581 |                 if actual_type not in expected_types:
582 |                     continue
583 |             
584 |             filtered_chain.append(contract)
585 | 
586 |         if not filtered_chain:
587 |             filters_applied = []
588 |             if contract_id:
589 |                 filters_applied.append(f"contract_id={contract_id}")
590 |             if expiry_date:
591 |                 filters_applied.append(f"expiry={expiry_date}")
592 |             if min_strike is not None:
593 |                 filters_applied.append(f"min_strike={min_strike}")
594 |             if max_strike is not None:
595 |                 filters_applied.append(f"max_strike={max_strike}")
596 |             if contract_type:
597 |                 filters_applied.append(f"type={contract_type}")
598 |             
599 |             filter_text = ", ".join(filters_applied) if filters_applied else "applied"
600 |             return f"No options contracts found matching the specified filters: {filter_text}"
601 | 
602 |         formatted = [
603 |             f"Historical Options Data (Filtered):\n",
604 |             f"Status: {options_data.get('message', 'N/A')}\n",
605 |         ]
606 |         
607 |         # Add filter summary
608 |         filters_applied = []
609 |         if contract_id:
610 |             filters_applied.append(f"Contract ID: {contract_id}")
611 |         if expiry_date:
612 |             filters_applied.append(f"Expiry: {expiry_date}")
613 |         if min_strike is not None or max_strike is not None:
614 |             strike_range = []
615 |             if min_strike is not None:
616 |                 strike_range.append(f"min ${min_strike}")
617 |             if max_strike is not None:
618 |                 strike_range.append(f"max ${max_strike}")
619 |             filters_applied.append(f"Strike: {' - '.join(strike_range)}")
620 |         if contract_type:
621 |             filters_applied.append(f"Type: {contract_type}")
622 |         
623 |         if filters_applied:
624 |             formatted.append(f"Filters: {', '.join(filters_applied)}\n")
625 |         
626 |         formatted.append(f"Found {len(filtered_chain)} contracts, sorted by: {sort_by} ({sort_order})\n\n")
627 | 
628 |         # Convert string values to float for numeric sorting
629 |         def get_sort_key(contract):
630 |             value = contract.get(sort_by, 0)
631 |             try:
632 |                 # Remove $ and % signs if present
633 |                 if isinstance(value, str):
634 |                     value = value.replace('$', '').replace('%', '')
635 |                 return float(value)
636 |             except (ValueError, TypeError):
637 |                 return value
638 | 
639 |         # Sort the filtered chain
640 |         sorted_chain = sorted(
641 |             filtered_chain,
642 |             key=get_sort_key,
643 |             reverse=(sort_order == "desc")
644 |         )
645 | 
646 |         # If limit is -1, show all contracts
647 |         display_contracts = sorted_chain if limit == -1 else sorted_chain[:limit]
648 | 
649 |         for contract in display_contracts:
650 |             formatted.append(f"Contract Details:\n")
651 |             formatted.append(f"Contract ID: {contract.get('contractID', 'N/A')}\n")
652 |             formatted.append(f"Expiration: {contract.get('expiration', 'N/A')}\n")
653 |             formatted.append(f"Strike: ${contract.get('strike', 'N/A')}\n")
654 |             formatted.append(f"Type: {contract.get('type', 'N/A')}\n")
655 |             formatted.append(f"Last: ${contract.get('last', 'N/A')}\n")
656 |             formatted.append(f"Mark: ${contract.get('mark', 'N/A')}\n")
657 |             formatted.append(f"Bid: ${contract.get('bid', 'N/A')} (Size: {contract.get('bid_size', 'N/A')})\n")
658 |             formatted.append(f"Ask: ${contract.get('ask', 'N/A')} (Size: {contract.get('ask_size', 'N/A')})\n")
659 |             formatted.append(f"Volume: {contract.get('volume', 'N/A')}\n")
660 |             formatted.append(f"Open Interest: {contract.get('open_interest', 'N/A')}\n")
661 |             formatted.append(f"IV: {contract.get('implied_volatility', 'N/A')}\n")
662 |             formatted.append(f"Delta: {contract.get('delta', 'N/A')}\n")
663 |             formatted.append(f"Gamma: {contract.get('gamma', 'N/A')}\n")
664 |             formatted.append(f"Theta: {contract.get('theta', 'N/A')}\n")
665 |             formatted.append(f"Vega: {contract.get('vega', 'N/A')}\n")
666 |             formatted.append(f"Rho: {contract.get('rho', 'N/A')}\n")
667 |             formatted.append("---\n")
668 | 
669 |         if limit != -1 and len(sorted_chain) > limit:
670 |             formatted.append(f"\n... and {len(sorted_chain) - limit} more contracts")
671 | 
672 |         return "".join(formatted)
673 |     except Exception as e:
674 |         return f"Error formatting options data: {str(e)}"
675 | 
676 | 
677 | def format_realtime_options(options_data: Dict[str, Any]) -> str:
678 |     """Format realtime options data into a concise string.
679 |     
680 |     Args:
681 |         options_data: The response data from the Alpha Vantage REALTIME_OPTIONS endpoint
682 |         
683 |     Returns:
684 |         A formatted string containing the realtime options information
685 |     """
686 |     try:
687 |         if "Error Message" in options_data:
688 |             return f"Error: {options_data['Error Message']}"
689 | 
690 |         # Get the options contracts list
691 |         options_list = options_data.get("data", [])
692 |         if not options_list:
693 |             return "No realtime options data available in the response"
694 | 
695 |         # Detect placeholder/demo data patterns
696 |         def is_placeholder_data(contracts_list):
697 |             """Detect if the response contains placeholder/demo data"""
698 |             for contract in contracts_list:
699 |                 symbol = contract.get("symbol", "")
700 |                 contract_id = contract.get("contractID", "")
701 |                 expiration = contract.get("expiration", "")
702 |                 
703 |                 # Check for common placeholder patterns
704 |                 if (symbol == "XXYYZZ" or 
705 |                     "XXYYZZ" in contract_id or 
706 |                     expiration == "2099-99-99" or 
707 |                     "999999" in contract_id):
708 |                     return True
709 |             return False
710 | 
711 |         if is_placeholder_data(options_list):
712 |             # Check if we're using demo API key
713 |             api_key_status = "demo API key" if API_KEY == "demo" else f"API key ending in ...{API_KEY[-4:]}" if API_KEY and len(API_KEY) > 4 else "no API key set"
714 |             
715 |             return (
716 |                 "❌ PREMIUM FEATURE REQUIRED ❌\n\n"
717 |                 "The realtime options data you requested requires a premium Alpha Vantage subscription.\n"
718 |                 "The API returned placeholder/demo data instead of real market data.\n\n"
719 |                 f"Current API key status: {api_key_status}\n\n"
720 |                 "Possible causes:\n"
721 |                 "1. Using demo API key instead of your actual API key\n"
722 |                 "2. API key is valid but account doesn't have premium access\n"
723 |                 "3. Alpha Vantage returns demo data for free accounts on this endpoint\n\n"
724 |                 "Solutions:\n"
725 |                 "1. Ensure your actual API key is set in ALPHA_VANTAGE_API_KEY environment variable\n"
726 |                 "2. Upgrade to Alpha Vantage Premium (600 or 1200 requests/minute plan)\n"
727 |                 "3. Use 'get-historical-options' tool for historical data (available with free accounts)\n\n"
728 |                 "Learn more: https://www.alphavantage.co/premium/\n\n"
729 |                 "Note: Historical options data may meet your analysis needs and works with free accounts."
730 |             )
731 | 
732 |         # Group contracts by expiration date and then by strike price
733 |         contracts_by_expiry = {}
734 |         for contract in options_list:
735 |             expiry = contract.get("expiration", "Unknown")
736 |             strike = contract.get("strike", "0.00")
737 |             contract_type = contract.get("type", "unknown")
738 |             
739 |             if expiry not in contracts_by_expiry:
740 |                 contracts_by_expiry[expiry] = {}
741 |             if strike not in contracts_by_expiry[expiry]:
742 |                 contracts_by_expiry[expiry][strike] = {}
743 |             
744 |             contracts_by_expiry[expiry][strike][contract_type] = contract
745 | 
746 |         # Extract symbol from first contract
747 |         symbol = options_list[0].get("symbol", "Unknown") if options_list else "Unknown"
748 |         
749 |         formatted = [
750 |             f"Realtime Options Data for {symbol}\n",
751 |             f"Found {len(options_list)} contracts\n\n"
752 |         ]
753 | 
754 |         # Sort by expiration dates
755 |         sorted_expiries = sorted(contracts_by_expiry.keys())
756 |         
757 |         for expiry in sorted_expiries:
758 |             formatted.append(f"=== Expiration: {expiry} ===\n")
759 |             
760 |             # Sort strikes numerically
761 |             strikes = contracts_by_expiry[expiry]
762 |             sorted_strikes = sorted(strikes.keys(), key=lambda x: float(x) if str(x).replace('.', '').isdigit() else 0)
763 |             
764 |             for strike in sorted_strikes:
765 |                 contract_types = strikes[strike]
766 |                 
767 |                 for contract_type in ["call", "put"]:
768 |                     if contract_type in contract_types:
769 |                         contract = contract_types[contract_type]
770 |                         
771 |                         formatted.append(f"\nStrike: ${strike} ({contract_type.upper()})\n")
772 |                         formatted.append(f"Contract ID: {contract.get('contractID', 'N/A')}\n")
773 |                         formatted.append(f"Last: ${contract.get('last', 'N/A')}\n")
774 |                         formatted.append(f"Mark: ${contract.get('mark', 'N/A')}\n")
775 |                         formatted.append(f"Bid: ${contract.get('bid', 'N/A')} (Size: {contract.get('bid_size', 'N/A')})\n")
776 |                         formatted.append(f"Ask: ${contract.get('ask', 'N/A')} (Size: {contract.get('ask_size', 'N/A')})\n")
777 |                         formatted.append(f"Volume: {contract.get('volume', 'N/A')}\n")
778 |                         formatted.append(f"Open Interest: {contract.get('open_interest', 'N/A')}\n")
779 |                         formatted.append(f"Date: {contract.get('date', 'N/A')}\n")
780 |                         
781 |                         # Include Greeks if available
782 |                         if 'implied_volatility' in contract:
783 |                             formatted.append(f"IV: {contract.get('implied_volatility', 'N/A')}\n")
784 |                         if 'delta' in contract:
785 |                             formatted.append(f"Delta: {contract.get('delta', 'N/A')}\n")
786 |                         if 'gamma' in contract:
787 |                             formatted.append(f"Gamma: {contract.get('gamma', 'N/A')}\n")
788 |                         if 'theta' in contract:
789 |                             formatted.append(f"Theta: {contract.get('theta', 'N/A')}\n")
790 |                         if 'vega' in contract:
791 |                             formatted.append(f"Vega: {contract.get('vega', 'N/A')}\n")
792 |                         if 'rho' in contract:
793 |                             formatted.append(f"Rho: {contract.get('rho', 'N/A')}\n")
794 |                         
795 |                         formatted.append("---\n")
796 |             
797 |             formatted.append("\n")
798 | 
799 |         return "".join(formatted)
800 |     except Exception as e:
801 |         return f"Error formatting realtime options data: {str(e)}"
802 | 
803 | 
804 | def format_etf_profile(etf_data: Dict[str, Any]) -> str:
805 |     """Format ETF profile data into a concise string.
806 |     
807 |     Args:
808 |         etf_data: The response data from the Alpha Vantage ETF_PROFILE endpoint
809 |         
810 |     Returns:
811 |         A formatted string containing the ETF profile information
812 |     """
813 |     try:
814 |         if "Error Message" in etf_data:
815 |             return f"Error: {etf_data['Error Message']}"
816 | 
817 |         if not etf_data:
818 |             return "No ETF profile data available in the response"
819 | 
820 |         # Extract basic ETF information
821 |         net_assets = etf_data.get("net_assets", "N/A")
822 |         net_expense_ratio = etf_data.get("net_expense_ratio", "N/A")
823 |         portfolio_turnover = etf_data.get("portfolio_turnover", "N/A")
824 |         dividend_yield = etf_data.get("dividend_yield", "N/A")
825 |         inception_date = etf_data.get("inception_date", "N/A")
826 |         leveraged = etf_data.get("leveraged", "N/A")
827 | 
828 |         formatted = [
829 |             f"ETF Profile\n\n",
830 |             f"Basic Information:\n"
831 |         ]
832 |         
833 |         # Format net assets
834 |         if net_assets != "N/A" and net_assets.replace('.', '').isdigit():
835 |             formatted.append(f"Net Assets: ${float(net_assets):,.0f}\n")
836 |         else:
837 |             formatted.append(f"Net Assets: {net_assets}\n")
838 |             
839 |         # Format net expense ratio
840 |         if net_expense_ratio != "N/A" and net_expense_ratio.replace('.', '').replace('-', '').isdigit():
841 |             formatted.append(f"Net Expense Ratio: {float(net_expense_ratio):.3%}\n")
842 |         else:
843 |             formatted.append(f"Net Expense Ratio: {net_expense_ratio}\n")
844 |             
845 |         # Format portfolio turnover
846 |         if portfolio_turnover != "N/A" and portfolio_turnover.replace('.', '').replace('-', '').isdigit():
847 |             formatted.append(f"Portfolio Turnover: {float(portfolio_turnover):.1%}\n")
848 |         else:
849 |             formatted.append(f"Portfolio Turnover: {portfolio_turnover}\n")
850 |             
851 |         # Format dividend yield
852 |         if dividend_yield != "N/A" and dividend_yield.replace('.', '').replace('-', '').isdigit():
853 |             formatted.append(f"Dividend Yield: {float(dividend_yield):.2%}\n")
854 |         else:
855 |             formatted.append(f"Dividend Yield: {dividend_yield}\n")
856 |             
857 |         formatted.extend([
858 |             f"Inception Date: {inception_date}\n",
859 |             f"Leveraged: {leveraged}\n\n"
860 |         ])
861 | 
862 |         # Format sectors if available
863 |         sectors = etf_data.get("sectors", [])
864 |         if sectors:
865 |             formatted.append("Sector Allocation:\n")
866 |             for sector in sectors:
867 |                 sector_name = sector.get("sector", "Unknown")
868 |                 weight = sector.get("weight", "0")
869 |                 try:
870 |                     weight_pct = float(weight) * 100
871 |                     formatted.append(f"{sector_name}: {weight_pct:.1f}%\n")
872 |                 except (ValueError, TypeError):
873 |                     formatted.append(f"{sector_name}: {weight}\n")
874 |             formatted.append("\n")
875 | 
876 |         # Format top holdings if available
877 |         holdings = etf_data.get("holdings", [])
878 |         if holdings:
879 |             formatted.append("Top Holdings:\n")
880 |             # Show top 10 holdings
881 |             for i, holding in enumerate(holdings[:10]):
882 |                 symbol = holding.get("symbol", "N/A")
883 |                 description = holding.get("description", "N/A")
884 |                 weight = holding.get("weight", "0")
885 |                 
886 |                 try:
887 |                     weight_pct = float(weight) * 100
888 |                     formatted.append(f"{i+1:2d}. {symbol} - {description}: {weight_pct:.2f}%\n")
889 |                 except (ValueError, TypeError):
890 |                     formatted.append(f"{i+1:2d}. {symbol} - {description}: {weight}\n")
891 |             
892 |             if len(holdings) > 10:
893 |                 formatted.append(f"\n... and {len(holdings) - 10} more holdings\n")
894 |             formatted.append(f"\nTotal Holdings: {len(holdings)}\n")
895 | 
896 |         return "".join(formatted)
897 |     except Exception as e:
898 |         return f"Error formatting ETF profile data: {str(e)}"
899 | 
```