Skip to main content
Glama
analysis.pyâ€ĸ34.4 kB
""" Analysis & Valuation Tools Tools for stock analysis and valuation: - Analyst forecasts and price targets - Consensus recommendations - EPS and revenue forecasts - Revision trends and upgrades/downgrades """ import yfinance as yf from typing import Dict, Any, List import json import pandas as pd from datetime import datetime, timedelta import logging import requests import os from models import ( MCPTool, MCPToolInputSchema, AnalystForecasts, PriceTarget, Recommendations, EPSForecasts, EPSForecast, RevenueForecasts, RevenueForecast, ForecastRevisionTrend, AnalystForecastsData, GrowthProjections, GrowthProjectionsData, GrowthMetric, GrowthProjectionPeriod, GrowthProjectionMultiYear, GrowthSummary, ValuationInsights, ValuationInsightsData, ValuationMultiples, SectorComparison, ValuationScore, HistoricalContext ) def get_analysis_tools() -> List[MCPTool]: """Return list of analysis and valuation tools""" return [ MCPTool( name="get_analyst_forecasts", title="Analyst Targets & EPS Forecasts", description="Retrieve analyst price targets (high/low/mean), consensus ratings (Buy/Hold/Sell), and future EPS forecasts. Essential for understanding market sentiment and professional analyst expectations for investment decision making.", inputSchema=MCPToolInputSchema( type="object", properties={ "symbol": { "type": "string", "description": "Stock ticker symbol (e.g., 'AAPL' for Apple, 'MSFT' for Microsoft, 'GOOGL' for Google)" } }, required=["symbol"] ) ), MCPTool( name="get_growth_projections", title="Forward Growth Projections", description="Provide forward growth projections for revenue, earnings (EPS), and free cash flow based on analyst estimates and company projections. Includes 1-year estimates, 3-year and 5-year CAGR projections with cumulative growth rates.", inputSchema=MCPToolInputSchema( type="object", properties={ "symbol": { "type": "string", "description": "Stock ticker symbol (e.g., 'AAPL' for Apple, 'MSFT' for Microsoft, 'GOOGL' for Google)" } }, required=["symbol"] ) ), MCPTool( name="get_valuation_insights", title="Relative & Forward Valuation Analysis", description="Provide comprehensive valuation analysis including current and forward multiples (P/E, PEG, P/B, P/S, EV/EBITDA), sector comparison, valuation score with qualitative assessment, and historical context for investment decision-making.", inputSchema=MCPToolInputSchema( type="object", properties={ "symbol": { "type": "string", "description": "Stock ticker symbol (e.g., 'AAPL' for Apple, 'MSFT' for Microsoft, 'GOOGL' for Google)" } }, required=["symbol"] ) ), ] def get_analyst_forecasts(arguments: Dict[str, Any]) -> str: """ Get analyst forecasts including price targets, consensus ratings, and EPS forecasts using Alpha Vantage API combined with yfinance for comprehensive data Args: arguments: Dictionary containing 'symbol' key Returns: JSON string with analyst forecasts data or error message """ try: symbol = arguments.get("symbol", "").upper().strip() if not symbol: return json.dumps({ "error": "Symbol is required", "symbol": None }) # Get yfinance data first (most reliable for analyst data) try: ticker = yf.Ticker(symbol) info = ticker.info current_price = info.get('regularMarketPrice') or info.get('currentPrice') if not current_price: # Try to get current price from history hist = ticker.history(period="1d") if not hist.empty: current_price = float(hist['Close'].iloc[-1]) except Exception as yf_error: return json.dumps({ "error": f"Error fetching data from yfinance for '{symbol}': {str(yf_error)}. Please verify the ticker symbol.", "symbol": symbol }) # Initialize components price_target = PriceTarget() recommendations = Recommendations() eps_forecasts_data = EPSForecasts() revenue_forecasts = RevenueForecasts() revision_trend = ForecastRevisionTrend() # Parse price targets from yfinance target_mean = info.get('targetMeanPrice') target_high = info.get('targetHighPrice') target_low = info.get('targetLowPrice') if target_mean: price_target.mean = round(float(target_mean), 2) if current_price and current_price > 0: upside = ((target_mean - current_price) / current_price) * 100 price_target.upside_potential = round(upside, 1) if target_high: price_target.high = round(float(target_high), 2) if target_low: price_target.low = round(float(target_low), 2) # Parse analyst recommendations from yfinance try: rec_summary = ticker.recommendations_summary if rec_summary is not None and not rec_summary.empty: # Get most recent recommendations latest_rec = rec_summary.iloc[0] recommendations.strong_buy = int(latest_rec.get('strongBuy', 0)) if pd.notna(latest_rec.get('strongBuy')) else None recommendations.buy = int(latest_rec.get('buy', 0)) if pd.notna(latest_rec.get('buy')) else None recommendations.hold = int(latest_rec.get('hold', 0)) if pd.notna(latest_rec.get('hold')) else None recommendations.sell = int(latest_rec.get('sell', 0)) if pd.notna(latest_rec.get('sell')) else None recommendations.strong_sell = int(latest_rec.get('strongSell', 0)) if pd.notna(latest_rec.get('strongSell')) else None # Calculate consensus rec_mean = info.get('recommendationMean') if rec_mean: if rec_mean <= 1.5: recommendations.consensus = "Strong Buy" elif rec_mean <= 2.5: recommendations.consensus = "Buy" elif rec_mean <= 3.5: recommendations.consensus = "Hold" elif rec_mean <= 4.5: recommendations.consensus = "Sell" else: recommendations.consensus = "Strong Sell" except Exception as rec_error: logging.warning(f"Could not fetch recommendations for {symbol}: {rec_error}") # Get Alpha Vantage data for additional EPS information api_key = os.getenv("ALPHAVANTAGE_KEY") or os.getenv("ALPHA_VANTAGE_API_KEY") alpha_earnings = {} if api_key: try: base_url = "https://www.alphavantage.co/query" earnings_params = { "function": "EARNINGS", "symbol": symbol, "apikey": api_key } earnings_response = requests.get(base_url, params=earnings_params, timeout=30) earnings_response.raise_for_status() alpha_earnings = earnings_response.json() if "Error Message" in alpha_earnings or "Note" in alpha_earnings: alpha_earnings = {} except Exception: alpha_earnings = {} # Parse EPS forecasts (combine yfinance and Alpha Vantage data) current_year_eps = EPSForecast() next_year_eps = EPSForecast() # Try to get current year EPS estimate from Alpha Vantage if alpha_earnings and "annualEarnings" in alpha_earnings: annual_earnings = alpha_earnings["annualEarnings"] if annual_earnings and len(annual_earnings) >= 2: try: latest_year = float(annual_earnings[0].get("reportedEPS", 0)) prev_year = float(annual_earnings[1].get("reportedEPS", 0)) current_year_eps.estimate = latest_year current_year_eps.previous = prev_year if prev_year > 0: surprise = ((latest_year - prev_year) / prev_year) * 100 current_year_eps.surprise = round(surprise, 1) # Calculate growth rate and next year estimate if latest_year > 0 and prev_year > 0: growth_rate = ((latest_year - prev_year) / prev_year) * 100 next_year_eps.growth_rate = round(growth_rate, 1) next_year_eps.estimate = round(latest_year * (1 + growth_rate/100), 2) except (ValueError, TypeError, IndexError): pass # Add EPS data to forecasts if current_year_eps.estimate or current_year_eps.previous or current_year_eps.surprise: eps_forecasts_data.current_year = current_year_eps if next_year_eps.estimate or next_year_eps.growth_rate: eps_forecasts_data.next_year = next_year_eps # Get revenue forecasts from yfinance info if available total_revenue = info.get('totalRevenue') revenue_growth = info.get('revenueGrowth') if total_revenue: current_rev = RevenueForecast() current_rev.estimate = int(total_revenue) if revenue_growth: current_rev.growth_rate = round(float(revenue_growth) * 100, 1) # Estimate next year revenue next_rev = RevenueForecast() next_rev.estimate = int(total_revenue * (1 + revenue_growth)) next_rev.growth_rate = round(float(revenue_growth) * 100, 1) revenue_forecasts.next_year = next_rev revenue_forecasts.current_year = current_rev # Parse upgrades/downgrades for revision trends try: upgrades_downgrades = ticker.upgrades_downgrades if upgrades_downgrades is not None and not upgrades_downgrades.empty: # Count upgrades vs downgrades in recent period (last 90 days) recent_date = datetime.now() - timedelta(days=90) recent_changes = upgrades_downgrades[upgrades_downgrades.index >= recent_date] if not recent_changes.empty: upgrade_actions = ['up', 'upgrade', 'raise', 'buy', 'outperform', 'overweight'] downgrade_actions = ['down', 'downgrade', 'lower', 'sell', 'underperform', 'underweight'] upgrades = 0 downgrades = 0 for _, row in recent_changes.iterrows(): action = str(row.get('Action', '')).lower() to_grade = str(row.get('ToGrade', '')).lower() if any(term in action or term in to_grade for term in upgrade_actions): upgrades += 1 elif any(term in action or term in to_grade for term in downgrade_actions): downgrades += 1 revision_trend.upgrades = upgrades revision_trend.downgrades = downgrades if upgrades > downgrades: revision_trend.net_trend = "Positive" elif downgrades > upgrades: revision_trend.net_trend = "Negative" else: revision_trend.net_trend = "Neutral" except Exception as trend_error: logging.warning(f"Could not fetch revision trends for {symbol}: {trend_error}") # Create the complete analyst forecasts response analyst_data = AnalystForecastsData( price_target=price_target, recommendations=recommendations, eps_forecasts=eps_forecasts_data, revenue_forecasts=revenue_forecasts, forecast_revision_trend=revision_trend ) forecasts = AnalystForecasts( symbol=symbol, analyst_forecasts=analyst_data, last_updated=datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') ) return forecasts.model_dump_json(indent=2) except Exception as e: return json.dumps({ "error": f"Error fetching analyst forecasts for '{symbol}': {str(e)}", "symbol": symbol }) def get_growth_projections(arguments: Dict[str, Any]) -> str: """ Get forward growth projections for revenue, EPS, and free cash flow using yfinance growth estimates, revenue estimates, and earnings estimates Args: arguments: Dictionary containing 'symbol' key Returns: JSON string with growth projections data or error message """ try: symbol = arguments.get("symbol", "").upper().strip() if not symbol: return json.dumps({ "error": "Symbol is required", "symbol": None }) # Get yfinance ticker try: ticker = yf.Ticker(symbol) # Get growth estimates (provides LTG - long term growth) growth_estimates = ticker.growth_estimates # Get revenue estimates (provides current year and next year estimates) revenue_estimates = ticker.revenue_estimate # Get earnings estimates (provides current year and next year EPS) earnings_estimates = ticker.earnings_estimate # Get basic info for additional data info = ticker.info except Exception as yf_error: return json.dumps({ "error": f"Error fetching data from yfinance for '{symbol}': {str(yf_error)}. Please verify the ticker symbol.", "symbol": symbol }) # Initialize growth metrics revenue_metric = GrowthMetric() eps_metric = GrowthMetric() fcf_metric = GrowthMetric() # Parse revenue projections if revenue_estimates is not None and not revenue_estimates.empty: try: # Get next year revenue estimate and growth next_year_data = revenue_estimates.loc['+1y'] current_year_data = revenue_estimates.loc['0y'] if pd.notna(next_year_data.get('avg')) and pd.notna(current_year_data.get('avg')): next_year_revenue = float(next_year_data['avg']) current_year_revenue = float(current_year_data['avg']) # Calculate next year growth rate growth_rate = ((next_year_revenue - current_year_revenue) / current_year_revenue) * 100 revenue_metric.next_year = GrowthProjectionPeriod( estimate=next_year_revenue, growth_rate=round(growth_rate, 1) ) # Use growth from info if available for long-term projections revenue_growth = info.get('revenueGrowth') if revenue_growth: annual_growth = float(revenue_growth) * 100 # Calculate 3-year and 5-year projections three_year_cagr = annual_growth * 0.9 # Slightly lower for longer term five_year_cagr = annual_growth * 0.8 # Even more conservative revenue_metric.next_3_years = GrowthProjectionMultiYear( cumulative_growth=round(((1 + three_year_cagr/100)**3 - 1) * 100, 1), cagr=round(three_year_cagr, 1) ) revenue_metric.next_5_years = GrowthProjectionMultiYear( cumulative_growth=round(((1 + five_year_cagr/100)**5 - 1) * 100, 1), cagr=round(five_year_cagr, 1) ) except Exception as rev_error: logging.warning(f"Could not parse revenue estimates for {symbol}: {rev_error}") # Parse EPS projections if earnings_estimates is not None and not earnings_estimates.empty: try: # Get next year EPS estimate and growth next_year_eps_data = earnings_estimates.loc['+1y'] current_year_eps_data = earnings_estimates.loc['0y'] if pd.notna(next_year_eps_data.get('avg')) and pd.notna(current_year_eps_data.get('avg')): next_year_eps = float(next_year_eps_data['avg']) current_year_eps = float(current_year_eps_data['avg']) # Calculate next year growth rate eps_growth_rate = ((next_year_eps - current_year_eps) / current_year_eps) * 100 eps_metric.next_year = GrowthProjectionPeriod( estimate=round(next_year_eps, 2), growth_rate=round(eps_growth_rate, 1) ) # Use earnings growth or calculate from LTG earnings_growth = info.get('earningsGrowth') ltg = None # Get LTG (Long Term Growth) from growth_estimates if growth_estimates is not None and not growth_estimates.empty: try: if 'LTG' in growth_estimates.index: ltg_data = growth_estimates.loc['LTG'] if pd.notna(ltg_data.get('stockTrend')): ltg = float(ltg_data['stockTrend']) * 100 except Exception: pass # Use available growth rate for projections base_growth = ltg or (earnings_growth * 100 if earnings_growth else eps_growth_rate) if base_growth and base_growth > 0: three_year_eps_cagr = base_growth * 0.95 # Slightly conservative five_year_eps_cagr = base_growth * 0.9 # More conservative eps_metric.next_3_years = GrowthProjectionMultiYear( cumulative_growth=round(((1 + three_year_eps_cagr/100)**3 - 1) * 100, 1), cagr=round(three_year_eps_cagr, 1) ) eps_metric.next_5_years = GrowthProjectionMultiYear( cumulative_growth=round(((1 + five_year_eps_cagr/100)**5 - 1) * 100, 1), cagr=round(five_year_eps_cagr, 1) ) except Exception as eps_error: logging.warning(f"Could not parse EPS estimates for {symbol}: {eps_error}") # Estimate Free Cash Flow projections (based on revenue growth as proxy) # FCF is typically correlated with revenue growth but more volatile if revenue_metric.next_year and revenue_metric.next_year.growth_rate: try: # Get current FCF from info if available current_fcf = info.get('freeCashflow') if current_fcf and current_fcf > 0: # Assume FCF grows at similar rate to revenue but slightly more volatile fcf_growth_rate = revenue_metric.next_year.growth_rate * 1.1 next_year_fcf = current_fcf * (1 + fcf_growth_rate/100) fcf_metric.next_year = GrowthProjectionPeriod( estimate=int(next_year_fcf), growth_rate=round(fcf_growth_rate, 1) ) # Use base revenue growth for longer projections if revenue_metric.next_3_years and revenue_metric.next_5_years: three_year_fcf_cagr = revenue_metric.next_3_years.cagr * 0.95 five_year_fcf_cagr = revenue_metric.next_5_years.cagr * 0.9 fcf_metric.next_3_years = GrowthProjectionMultiYear( cumulative_growth=round(((1 + three_year_fcf_cagr/100)**3 - 1) * 100, 1), cagr=round(three_year_fcf_cagr, 1) ) fcf_metric.next_5_years = GrowthProjectionMultiYear( cumulative_growth=round(((1 + five_year_fcf_cagr/100)**5 - 1) * 100, 1), cagr=round(five_year_fcf_cagr, 1) ) except Exception as fcf_error: logging.warning(f"Could not estimate FCF projections for {symbol}: {fcf_error}") # Create summary revenue_5y_cagr = revenue_metric.next_5_years.cagr if revenue_metric.next_5_years else None eps_5y_cagr = eps_metric.next_5_years.cagr if eps_metric.next_5_years else None fcf_5y_cagr = fcf_metric.next_5_years.cagr if fcf_metric.next_5_years else None # Determine overall growth outlook growth_rates = [rate for rate in [revenue_5y_cagr, eps_5y_cagr, fcf_5y_cagr] if rate is not None] if growth_rates: avg_growth = sum(growth_rates) / len(growth_rates) if avg_growth > 15: outlook = "Very Positive" elif avg_growth > 8: outlook = "Positive" elif avg_growth > 3: outlook = "Moderate" elif avg_growth > 0: outlook = "Conservative" else: outlook = "Challenging" else: outlook = "Data Limited" summary = GrowthSummary( expected_cagr_revenue_5y=revenue_5y_cagr, expected_cagr_eps_5y=eps_5y_cagr, expected_cagr_fcf_5y=fcf_5y_cagr, overall_growth_outlook=outlook ) # Create the complete growth projections response growth_data = GrowthProjectionsData( revenue=revenue_metric if revenue_metric.next_year else None, eps=eps_metric if eps_metric.next_year else None, free_cash_flow=fcf_metric if fcf_metric.next_year else None ) projections = GrowthProjections( symbol=symbol, growth_projections=growth_data, summary=summary, last_updated=datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') ) return projections.model_dump_json(indent=2) except Exception as e: return json.dumps({ "error": f"Error fetching growth projections for '{symbol}': {str(e)}", "symbol": symbol }) def get_valuation_insights(arguments: Dict[str, Any]) -> str: """ Get comprehensive valuation insights including current/forward multiples, sector comparison, valuation score, and historical context using yfinance and Alpha Vantage Args: arguments: Dictionary containing 'symbol' key Returns: JSON string with valuation insights data or error message """ try: symbol = arguments.get("symbol", "").upper().strip() if not symbol: return json.dumps({ "error": "Symbol is required", "symbol": None }) # Get yfinance data try: ticker = yf.Ticker(symbol) info = ticker.info except Exception as yf_error: return json.dumps({ "error": f"Error fetching data from yfinance for '{symbol}': {str(yf_error)}. Please verify the ticker symbol.", "symbol": symbol }) # Initialize components multiples = ValuationMultiples() sector_comparison = SectorComparison() valuation_score = ValuationScore() historical_context = HistoricalContext() # Parse current valuation multiples from yfinance multiples.pe_trailing = info.get('trailingPE') multiples.pe_forward = info.get('forwardPE') multiples.pb_ratio = info.get('priceToBook') multiples.ps_ratio = info.get('priceToSalesTrailing12Months') multiples.ev_ebitda = info.get('enterpriseToEbitda') # Calculate PEG ratio if we have the data pe_forward = multiples.pe_forward or multiples.pe_trailing earnings_growth = info.get('earningsGrowth') if pe_forward and earnings_growth and earnings_growth > 0: # Convert earnings growth from decimal to percentage growth_rate = earnings_growth * 100 multiples.peg_ratio = round(pe_forward / growth_rate, 2) # Get Alpha Vantage data for additional insights api_key = os.getenv("ALPHAVANTAGE_KEY") or os.getenv("ALPHA_VANTAGE_API_KEY") alpha_overview = {} if api_key: try: base_url = "https://www.alphavantage.co/query" overview_params = { "function": "OVERVIEW", "symbol": symbol, "apikey": api_key } overview_response = requests.get(base_url, params=overview_params, timeout=30) overview_response.raise_for_status() alpha_overview = overview_response.json() if "Error Message" in alpha_overview or "Note" in alpha_overview: alpha_overview = {} except Exception: alpha_overview = {} # Parse sector information and comparison sector = info.get('sector') industry = info.get('industry') # Use Alpha Vantage data to enhance multiples if available if alpha_overview: if not multiples.pe_trailing and alpha_overview.get('PERatio') != 'None': try: multiples.pe_trailing = float(alpha_overview.get('PERatio', 0)) except (ValueError, TypeError): pass if not multiples.pb_ratio and alpha_overview.get('PriceToBookRatio') != 'None': try: multiples.pb_ratio = float(alpha_overview.get('PriceToBookRatio', 0)) except (ValueError, TypeError): pass if not multiples.ev_ebitda and alpha_overview.get('EVToEBITDA') != 'None': try: multiples.ev_ebitda = float(alpha_overview.get('EVToEBITDA', 0)) except (ValueError, TypeError): pass # Sector comparison - Use industry averages as proxies # These are rough estimates for common sectors sector_multiples = { 'Technology': {'pe': 28.0, 'peg': 1.4}, 'Healthcare': {'pe': 24.0, 'peg': 1.3}, 'Consumer Cyclical': {'pe': 22.0, 'peg': 1.2}, 'Consumer Defensive': {'pe': 20.0, 'peg': 1.1}, 'Financial Services': {'pe': 12.0, 'peg': 1.0}, 'Industrials': {'pe': 18.0, 'peg': 1.2}, 'Energy': {'pe': 15.0, 'peg': 1.1}, 'Utilities': {'pe': 16.0, 'peg': 1.0}, 'Real Estate': {'pe': 14.0, 'peg': 1.1}, 'Materials': {'pe': 16.0, 'peg': 1.2}, 'Communication Services': {'pe': 25.0, 'peg': 1.3} } if sector in sector_multiples: sector_comparison.sector_pe = sector_multiples[sector]['pe'] sector_comparison.sector_peg = sector_multiples[sector]['peg'] # Calculate percentiles vs sector if multiples.pe_trailing: if multiples.pe_trailing < sector_comparison.sector_pe: sector_comparison.percentile_vs_sector_pe = 25 # Below average elif multiples.pe_trailing < sector_comparison.sector_pe * 1.1: sector_comparison.percentile_vs_sector_pe = 45 # Slightly below elif multiples.pe_trailing < sector_comparison.sector_pe * 1.3: sector_comparison.percentile_vs_sector_pe = 65 # Above average else: sector_comparison.percentile_vs_sector_pe = 85 # Well above average if multiples.peg_ratio: if multiples.peg_ratio < sector_comparison.sector_peg * 0.8: sector_comparison.percentile_vs_sector_peg = 20 # Very attractive elif multiples.peg_ratio < sector_comparison.sector_peg: sector_comparison.percentile_vs_sector_peg = 35 # Below average elif multiples.peg_ratio < sector_comparison.sector_peg * 1.2: sector_comparison.percentile_vs_sector_peg = 55 # Around average else: sector_comparison.percentile_vs_sector_peg = 75 # Above average # Calculate valuation score (0-100 scale) score_components = [] reasoning_parts = [] # PEG ratio scoring (most important) if multiples.peg_ratio: if multiples.peg_ratio < 0.8: score_components.append(90) reasoning_parts.append("PEG ratio below 0.8 suggests strong value") elif multiples.peg_ratio < 1.0: score_components.append(80) reasoning_parts.append("PEG ratio below 1 indicates good value vs growth") elif multiples.peg_ratio < 1.5: score_components.append(60) reasoning_parts.append("PEG ratio suggests fair valuation") elif multiples.peg_ratio < 2.0: score_components.append(40) reasoning_parts.append("PEG ratio indicates potential overvaluation") else: score_components.append(20) reasoning_parts.append("High PEG ratio suggests overvaluation") # PE ratio scoring if multiples.pe_forward and sector_comparison.sector_pe: pe_ratio_score = max(20, min(80, 100 - ((multiples.pe_forward / sector_comparison.sector_pe - 1) * 100))) score_components.append(pe_ratio_score) if multiples.pe_forward < sector_comparison.sector_pe: reasoning_parts.append("Forward P/E below sector median") else: reasoning_parts.append("Forward P/E above sector median") # Calculate final score if score_components: final_score = int(sum(score_components) / len(score_components)) valuation_score.score = final_score # Determine label if final_score >= 80: valuation_score.label = "Significantly Undervalued" elif final_score >= 65: valuation_score.label = "Slightly Undervalued" elif final_score >= 50: valuation_score.label = "Fairly Valued" elif final_score >= 35: valuation_score.label = "Slightly Overvalued" else: valuation_score.label = "Significantly Overvalued" # Create reasoning valuation_score.reasoning = ". ".join(reasoning_parts) + f" suggesting the stock is {valuation_score.label.lower()}." # Historical context (simplified - using 5-year averages where available) # Note: This would ideally use historical P/E data, but yfinance doesn't provide this easily # We'll use current vs trailing as a proxy if multiples.pe_forward and multiples.pe_trailing: pe_change = multiples.pe_forward - multiples.pe_trailing historical_context.current_vs_5y_avg_pe = round(pe_change, 1) # Create the complete valuation insights response insights_data = ValuationInsightsData( multiples=multiples, sector_comparison=sector_comparison, valuation_score=valuation_score, historical_context=historical_context ) insights = ValuationInsights( symbol=symbol, valuation_insights=insights_data, last_updated=datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') ) return insights.model_dump_json(indent=2) except Exception as e: return json.dumps({ "error": f"Error fetching valuation insights for '{symbol}': {str(e)}", "symbol": symbol })

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/leogue/StockMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server