|
|
import os |
|
|
import requests |
|
|
import gradio as gr |
|
|
from typing import Optional, List, Dict, Any |
|
|
from datetime import datetime, timedelta |
|
|
from mcp.server.fastmcp import FastMCP |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mcp = FastMCP("finnhub-market-info") |
|
|
|
|
|
|
|
|
API_KEY = None |
|
|
|
|
|
|
|
|
def set_api_key(key: str): |
|
|
"""Set the Finnhub API key""" |
|
|
global API_KEY |
|
|
API_KEY = key |
|
|
|
|
|
|
|
|
def get_api_key() -> Optional[str]: |
|
|
"""Get the Finnhub API key""" |
|
|
global API_KEY |
|
|
|
|
|
api_key = ( |
|
|
API_KEY or |
|
|
os.getenv("FINNHUB_API_KEY") |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
print(f"API Key sources:") |
|
|
print(f" Global API_KEY: {API_KEY}") |
|
|
print(f" os.getenv('FINNHUB_API_KEY'): {os.getenv('FINNHUB_API_KEY')}") |
|
|
|
|
|
print(f" Final API key: {'*' * len(api_key) if api_key else 'None'}") |
|
|
|
|
|
return api_key |
|
|
|
|
|
|
|
|
def make_finnhub_request(endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]: |
|
|
""" |
|
|
Make a request to Finnhub API |
|
|
|
|
|
Args: |
|
|
endpoint: API endpoint path |
|
|
params: Query parameters |
|
|
|
|
|
Returns: |
|
|
API response as dictionary |
|
|
""" |
|
|
api_key = get_api_key() |
|
|
if not api_key: |
|
|
return {"error": "API key not configured. Please set your Finnhub API key."} |
|
|
|
|
|
params["token"] = api_key |
|
|
base_url = "https://finnhub.io/api/v1" |
|
|
url = f"{base_url}/{endpoint}" |
|
|
|
|
|
try: |
|
|
response = requests.get(url, params=params, timeout=10) |
|
|
response.raise_for_status() |
|
|
return response.json() |
|
|
except requests.exceptions.RequestException as e: |
|
|
return {"error": f"API request failed: {str(e)}"} |
|
|
|
|
|
|
|
|
@mcp.tool() |
|
|
def get_quote(symbol: str) -> dict: |
|
|
""" |
|
|
Get real-time quote data for US stocks. Use this tool when you need current stock price |
|
|
information and market performance metrics for any US-listed stock. |
|
|
|
|
|
When to use: |
|
|
- User asks "What's the current price of [stock]?" |
|
|
- Need real-time stock quote data |
|
|
- User mentions "stock price", "current value", "how is [stock] trading?" |
|
|
- Want to check latest market price and daily changes |
|
|
|
|
|
Examples: |
|
|
- "What's Apple's stock price?" → get_quote(symbol="AAPL") |
|
|
- "How is Tesla trading today?" → get_quote(symbol="TSLA") |
|
|
- "Show me Microsoft's current quote" → get_quote(symbol="MSFT") |
|
|
|
|
|
Args: |
|
|
symbol: Stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL') |
|
|
|
|
|
Returns: |
|
|
dict: Real-time quote data containing: |
|
|
- symbol: Stock ticker symbol |
|
|
- current_price: Current trading price (c) |
|
|
- change: Price change in dollars (d) |
|
|
- percent_change: Price change in percentage (dp) |
|
|
- high: Today's high price (h) |
|
|
- low: Today's low price (l) |
|
|
- open: Opening price (o) |
|
|
- previous_close: Previous trading day's closing price (pc) |
|
|
- timestamp: Quote timestamp |
|
|
""" |
|
|
result = make_finnhub_request("quote", {"symbol": symbol.upper()}) |
|
|
|
|
|
if "error" in result: |
|
|
return { |
|
|
"error": result["error"], |
|
|
"symbol": symbol.upper() |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
"symbol": symbol.upper(), |
|
|
"current_price": result.get('c'), |
|
|
"change": result.get('d'), |
|
|
"percent_change": result.get('dp'), |
|
|
"high": result.get('h'), |
|
|
"low": result.get('l'), |
|
|
"open": result.get('o'), |
|
|
"previous_close": result.get('pc'), |
|
|
"timestamp": datetime.fromtimestamp(result.get('t', 0)).strftime('%Y-%m-%d %H:%M:%S') if result.get('t') else None |
|
|
} |
|
|
|
|
|
|
|
|
@mcp.tool() |
|
|
def get_market_news(category: str = "general", min_id: int = 0) -> dict: |
|
|
""" |
|
|
Get latest market news across different categories. Use this tool when you need current market |
|
|
news, trends, and developments in general markets, forex, cryptocurrency, or mergers. |
|
|
|
|
|
When to use: |
|
|
- User asks "What's the latest market news?" |
|
|
- Need current financial news and market updates |
|
|
- User mentions "news", "market trends", "what's happening in the market?" |
|
|
- Want to get news for specific categories (forex, crypto, M&A) |
|
|
|
|
|
Categories explained: |
|
|
- general: General market news, stocks, economy, major companies |
|
|
- forex: Foreign exchange and currency market news |
|
|
- crypto: Cryptocurrency and blockchain news |
|
|
- merger: Mergers & acquisitions, corporate deals |
|
|
|
|
|
Examples: |
|
|
- "What's the latest market news?" → get_market_news(category="general") |
|
|
- "Show me crypto news" → get_market_news(category="crypto") |
|
|
- "Any forex updates?" → get_market_news(category="forex") |
|
|
- "Recent merger news" → get_market_news(category="merger") |
|
|
|
|
|
Args: |
|
|
category: News category - "general", "forex", "crypto", or "merger" (default: "general") |
|
|
min_id: Minimum news ID for pagination (default: 0, use 0 to get latest news) |
|
|
|
|
|
Returns: |
|
|
dict: Market news data containing: |
|
|
- category: News category requested |
|
|
- total_articles: Total number of articles returned |
|
|
- articles: List of news articles (max 10), each with: |
|
|
* id: Article ID |
|
|
* headline: News headline |
|
|
* summary: Brief summary of the article |
|
|
* source: News source |
|
|
* url: Link to full article |
|
|
* published: Publication timestamp |
|
|
* image: Article image URL (if available) |
|
|
""" |
|
|
params = {"category": category} |
|
|
if min_id > 0: |
|
|
params["minId"] = min_id |
|
|
|
|
|
result = make_finnhub_request("news", params) |
|
|
|
|
|
if isinstance(result, dict) and "error" in result: |
|
|
return { |
|
|
"error": result["error"], |
|
|
"category": category |
|
|
} |
|
|
|
|
|
if not result or len(result) == 0: |
|
|
return { |
|
|
"category": category, |
|
|
"total_articles": 0, |
|
|
"articles": [], |
|
|
"message": "No news articles found for this category" |
|
|
} |
|
|
|
|
|
|
|
|
articles = [] |
|
|
for article in result[:10]: |
|
|
articles.append({ |
|
|
"id": article.get('id'), |
|
|
"headline": article.get('headline', 'No headline'), |
|
|
"summary": article.get('summary', ''), |
|
|
"source": article.get('source', 'Unknown'), |
|
|
"url": article.get('url'), |
|
|
"published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None, |
|
|
"image": article.get('image') |
|
|
}) |
|
|
|
|
|
return { |
|
|
"category": category, |
|
|
"total_articles": len(articles), |
|
|
"articles": articles |
|
|
} |
|
|
|
|
|
|
|
|
@mcp.tool() |
|
|
def get_company_news(symbol: str, from_date: Optional[str] = None, to_date: Optional[str] = None) -> dict: |
|
|
""" |
|
|
Get latest news for a specific company by stock symbol. This endpoint provides company-specific |
|
|
news, press releases, and announcements. Only available for North American companies. |
|
|
|
|
|
When to use: |
|
|
- User asks about a specific company's news (e.g., "Apple news", "Tesla updates") |
|
|
- Need company-specific announcements or press releases |
|
|
- User mentions "[company name] news", "recent [company] developments" |
|
|
- Want to filter news by date range |
|
|
|
|
|
Date range tips: |
|
|
- Default: Last 7 days if no dates specified |
|
|
- Can go back up to several years |
|
|
- Use YYYY-MM-DD format (e.g., "2024-01-01") |
|
|
|
|
|
Examples: |
|
|
- "What's the latest Apple news?" → get_company_news(symbol="AAPL") |
|
|
- "Tesla news from last month" → get_company_news(symbol="TSLA", from_date="2024-10-01", to_date="2024-10-31") |
|
|
- "Microsoft announcements this week" → get_company_news(symbol="MSFT") |
|
|
- "Show me Amazon news from January 2024" → get_company_news(symbol="AMZN", from_date="2024-01-01", to_date="2024-01-31") |
|
|
|
|
|
Args: |
|
|
symbol: Company stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL') |
|
|
Must be a North American (US/Canada) listed company |
|
|
from_date: Start date in YYYY-MM-DD format (default: 7 days ago) |
|
|
to_date: End date in YYYY-MM-DD format (default: today) |
|
|
|
|
|
Returns: |
|
|
dict: Company news data containing: |
|
|
- symbol: Stock ticker symbol |
|
|
- from_date: Start date of news search |
|
|
- to_date: End date of news search |
|
|
- total_articles: Number of articles found |
|
|
- articles: List of news articles (max 10), each with: |
|
|
* id: Article ID |
|
|
* headline: News headline |
|
|
* summary: Article summary |
|
|
* source: News source |
|
|
* url: Link to full article |
|
|
* published: Publication date and time |
|
|
* image: Article image URL (if available) |
|
|
* related_symbols: Other stock symbols mentioned |
|
|
""" |
|
|
|
|
|
if not to_date: |
|
|
to_date = datetime.now().strftime('%Y-%m-%d') |
|
|
if not from_date: |
|
|
from_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') |
|
|
|
|
|
params = { |
|
|
"symbol": symbol.upper(), |
|
|
"from": from_date, |
|
|
"to": to_date |
|
|
} |
|
|
|
|
|
result = make_finnhub_request("company-news", params) |
|
|
|
|
|
if isinstance(result, dict) and "error" in result: |
|
|
return { |
|
|
"error": result["error"], |
|
|
"symbol": symbol.upper(), |
|
|
"from_date": from_date, |
|
|
"to_date": to_date |
|
|
} |
|
|
|
|
|
if not result or len(result) == 0: |
|
|
|
|
|
suggestion = "Try expanding the date range or check if the symbol is correct." |
|
|
if symbol.upper() in ["BABA", "BIDU", "JD", "PDD", "NIO"]: |
|
|
suggestion = "Note: Chinese ADRs may have limited news coverage. Try US companies like AAPL, MSFT, TSLA, or GOOGL for better results." |
|
|
|
|
|
return { |
|
|
"symbol": symbol.upper(), |
|
|
"from_date": from_date, |
|
|
"to_date": to_date, |
|
|
"total_articles": 0, |
|
|
"articles": [], |
|
|
"message": f"No news articles found for {symbol.upper()} between {from_date} and {to_date}.", |
|
|
"suggestion": suggestion, |
|
|
"note": "Company news is only available for North American companies. Some companies may have limited news coverage during certain periods." |
|
|
} |
|
|
|
|
|
|
|
|
articles = [] |
|
|
for article in result[:10]: |
|
|
articles.append({ |
|
|
"id": article.get('id'), |
|
|
"headline": article.get('headline', 'No headline'), |
|
|
"summary": article.get('summary', ''), |
|
|
"source": article.get('source', 'Unknown'), |
|
|
"url": article.get('url'), |
|
|
"published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None, |
|
|
"image": article.get('image'), |
|
|
"related_symbols": article.get('related', []) |
|
|
}) |
|
|
|
|
|
return { |
|
|
"symbol": symbol.upper(), |
|
|
"from_date": from_date, |
|
|
"to_date": to_date, |
|
|
"total_articles": len(articles), |
|
|
"articles": articles |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def configure_api_key(api_key: str) -> str: |
|
|
"""Configure the Finnhub API key""" |
|
|
if not api_key or api_key.strip() == "": |
|
|
return "❌ Please provide a valid API key" |
|
|
set_api_key(api_key.strip()) |
|
|
return "✅ API Key configured successfully! You can now use the MCP tools." |
|
|
|
|
|
|
|
|
def test_quote_tool(symbol: str) -> str: |
|
|
"""Test the Quote tool""" |
|
|
if not symbol or symbol.strip() == "": |
|
|
return "❌ Please provide a stock symbol" |
|
|
|
|
|
result = get_quote(symbol.strip()) |
|
|
|
|
|
if "error" in result: |
|
|
return f"❌ Error: {result['error']}" |
|
|
|
|
|
|
|
|
output = f"""📊 Real-time Quote for {result['symbol']} |
|
|
|
|
|
Current Price: ${result.get('current_price', 'N/A')} |
|
|
Change: ${result.get('change', 'N/A')} |
|
|
Percent Change: {result.get('percent_change', 'N/A')}% |
|
|
High: ${result.get('high', 'N/A')} |
|
|
Low: ${result.get('low', 'N/A')} |
|
|
Open: ${result.get('open', 'N/A')} |
|
|
Previous Close: ${result.get('previous_close', 'N/A')} |
|
|
Timestamp: {result.get('timestamp', 'N/A')} |
|
|
""" |
|
|
return output.strip() |
|
|
|
|
|
|
|
|
def test_market_news_tool(category: str) -> str: |
|
|
"""Test the Market News tool""" |
|
|
result = get_market_news(category) |
|
|
|
|
|
if "error" in result: |
|
|
return f"❌ Error: {result['error']}" |
|
|
|
|
|
if result.get('total_articles', 0) == 0: |
|
|
return result.get('message', 'No news articles found') |
|
|
|
|
|
|
|
|
output = f"📰 Latest Market News ({result['category']})\n" |
|
|
output += f"Total Articles: {result['total_articles']}\n\n" |
|
|
|
|
|
for idx, article in enumerate(result['articles'], 1): |
|
|
output += f"{idx}. {article['headline']}\n" |
|
|
output += f" Source: {article['source']}\n" |
|
|
if article.get('summary'): |
|
|
summary = article['summary'][:200] + "..." if len(article['summary']) > 200 else article['summary'] |
|
|
output += f" Summary: {summary}\n" |
|
|
output += f" URL: {article.get('url', 'N/A')}\n" |
|
|
output += f" Published: {article.get('published', 'N/A')}\n\n" |
|
|
|
|
|
return output.strip() |
|
|
|
|
|
|
|
|
def test_company_news_tool(symbol: str, from_date: str, to_date: str) -> str: |
|
|
"""Test the Company News tool""" |
|
|
if not symbol or symbol.strip() == "": |
|
|
return "❌ Please provide a stock symbol" |
|
|
|
|
|
|
|
|
from_d = from_date.strip() if from_date and from_date.strip() else None |
|
|
to_d = to_date.strip() if to_date and to_date.strip() else None |
|
|
|
|
|
result = get_company_news(symbol.strip(), from_d, to_d) |
|
|
|
|
|
if "error" in result: |
|
|
return f"❌ Error: {result['error']}" |
|
|
|
|
|
if result.get('total_articles', 0) == 0: |
|
|
|
|
|
output = f"⚠️ {result.get('message', 'No news articles found')}\n\n" |
|
|
if 'suggestion' in result: |
|
|
output += f"💡 {result['suggestion']}\n\n" |
|
|
if 'note' in result: |
|
|
output += f"📝 {result['note']}" |
|
|
return output |
|
|
|
|
|
|
|
|
output = f"📰 Company News for {result['symbol']}\n" |
|
|
output += f"Period: {result['from_date']} to {result['to_date']}\n" |
|
|
output += f"Total Articles: {result['total_articles']}\n\n" |
|
|
|
|
|
for idx, article in enumerate(result['articles'], 1): |
|
|
output += f"{idx}. {article['headline']}\n" |
|
|
output += f" Source: {article['source']}\n" |
|
|
if article.get('summary'): |
|
|
summary = article['summary'][:200] + "..." if len(article['summary']) > 200 else article['summary'] |
|
|
output += f" Summary: {summary}\n" |
|
|
output += f" URL: {article.get('url', 'N/A')}\n" |
|
|
output += f" Published: {article.get('published', 'N/A')}\n\n" |
|
|
|
|
|
return output.strip() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import sys |
|
|
|
|
|
|
|
|
port = int(os.getenv("PORT", "7870")) |
|
|
host = os.getenv("HOST", "0.0.0.0") |
|
|
|
|
|
|
|
|
env_api_key = os.getenv("FINNHUB_API_KEY") |
|
|
if env_api_key: |
|
|
set_api_key(env_api_key) |
|
|
print("✅ API Key loaded from environment variable") |
|
|
|
|
|
|
|
|
print("▶️ Starting MarketandStockMCP Server...") |
|
|
print(f"📡 MCP server will listen on {host}:{port}") |
|
|
print("✅ Available tools: get_quote, get_market_news, get_company_news") |
|
|
print(f"🔗 MCP endpoint: http://{host}:{port}/mcp") |
|
|
|
|
|
|
|
|
import uvicorn |
|
|
original_config_init = uvicorn.Config.__init__ |
|
|
|
|
|
def patched_init(self, *args, **kwargs): |
|
|
kwargs['host'] = host |
|
|
kwargs['port'] = port |
|
|
return original_config_init(self, *args, **kwargs) |
|
|
|
|
|
uvicorn.Config.__init__ = patched_init |
|
|
|
|
|
|
|
|
|
|
|
mcp.run(transport="http") |