"""Financial Modeling Prep (FMP) MCP Server. This MCP server provides company fundamentals, financial statements, and key metrics using the Financial Modeling Prep API. Free tier: 250 calls/day, 500MB/30 days """ import logging from datetime import datetime from typing import Dict, List, Optional, Any from decimal import Decimal import httpx import yfinance as yf from fastmcp import FastMCP from pydantic import BaseModel, Field from tenacity import ( retry, stop_after_attempt, wait_exponential, retry_if_exception_type, ) from backend.config import settings logger = logging.getLogger(__name__) # Initialize MCP server mcp = FastMCP("financial-modeling-prep") # API Configuration BASE_URL = "https://financialmodelingprep.com/api/v3" API_KEY = settings.fmp_api_key class CompanyProfileRequest(BaseModel): """Request for company profile.""" ticker: str class CompanyProfile(BaseModel): """Company profile and fundamental data.""" ticker: str company_name: Optional[str] = None sector: Optional[str] = None industry: Optional[str] = None website: Optional[str] = None description: Optional[str] = None ceo: Optional[str] = None employees: Optional[int] = None market_cap: Optional[Decimal] = None beta: Optional[Decimal] = None price: Optional[Decimal] = None volume_avg: Optional[int] = None exchange: Optional[str] = None ipo_date: Optional[str] = None country: Optional[str] = None class FinancialStatementsRequest(BaseModel): """Request for financial statements.""" ticker: str period: str = Field(default="annual", description="annual or quarter") limit: int = Field(default=5, ge=1, le=120) class IncomeStatement(BaseModel): """Income statement data.""" date: str revenue: Optional[Decimal] = None cost_of_revenue: Optional[Decimal] = None gross_profit: Optional[Decimal] = None operating_expenses: Optional[Decimal] = None operating_income: Optional[Decimal] = None ebitda: Optional[Decimal] = None net_income: Optional[Decimal] = None eps: Optional[Decimal] = None eps_diluted: Optional[Decimal] = None class BalanceSheet(BaseModel): """Balance sheet data.""" date: str total_assets: Optional[Decimal] = None total_current_assets: Optional[Decimal] = None cash_and_cash_equivalents: Optional[Decimal] = None total_liabilities: Optional[Decimal] = None total_current_liabilities: Optional[Decimal] = None total_debt: Optional[Decimal] = None total_stockholders_equity: Optional[Decimal] = None class CashFlowStatement(BaseModel): """Cash flow statement data.""" date: str operating_cash_flow: Optional[Decimal] = None capital_expenditure: Optional[Decimal] = None free_cash_flow: Optional[Decimal] = None net_cash_from_financing: Optional[Decimal] = None net_cash_from_investing: Optional[Decimal] = None net_change_in_cash: Optional[Decimal] = None class FinancialRatiosRequest(BaseModel): """Request for financial ratios.""" ticker: str ttm: bool = Field(default=True, description="Use trailing twelve months") class FinancialRatios(BaseModel): """Key financial ratios.""" ticker: str date: Optional[str] = None # Profitability net_profit_margin: Optional[Decimal] = None roe: Optional[Decimal] = None roa: Optional[Decimal] = None roic: Optional[Decimal] = None # Liquidity current_ratio: Optional[Decimal] = None quick_ratio: Optional[Decimal] = None cash_ratio: Optional[Decimal] = None # Efficiency asset_turnover: Optional[Decimal] = None inventory_turnover: Optional[Decimal] = None # Leverage debt_to_equity: Optional[Decimal] = None debt_to_assets: Optional[Decimal] = None interest_coverage: Optional[Decimal] = None class KeyMetricsRequest(BaseModel): """Request for key metrics.""" ticker: str ttm: bool = Field(default=True) class KeyMetrics(BaseModel): """Key company metrics.""" ticker: str date: Optional[str] = None market_cap: Optional[Decimal] = None pe_ratio: Optional[Decimal] = None price_to_book: Optional[Decimal] = None price_to_sales: Optional[Decimal] = None enterprise_value: Optional[Decimal] = None ev_to_ebitda: Optional[Decimal] = None revenue_per_share: Optional[Decimal] = None earnings_per_share: Optional[Decimal] = None book_value_per_share: Optional[Decimal] = None operating_cash_flow_per_share: Optional[Decimal] = None free_cash_flow_per_share: Optional[Decimal] = None async def _make_request(endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Make HTTP request to FMP API. Args: endpoint: API endpoint path params: Query parameters Returns: JSON response data Raises: httpx.HTTPError: On HTTP errors """ if params is None: params = {} params["apikey"] = API_KEY url = f"{BASE_URL}/{endpoint}" async with httpx.AsyncClient() as client: response = await client.get(url, params=params, timeout=30.0) response.raise_for_status() return response.json() @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type((TimeoutError, ConnectionError, httpx.HTTPStatusError)), ) @mcp.tool() async def get_company_profile(request: CompanyProfileRequest) -> CompanyProfile: """Get company profile using yfinance (free, no API key required). Replaces deprecated FMP v3/profile endpoint with yfinance as fallback. Args: request: Company profile request Returns: Company profile with name, sector, industry, market cap, description, etc. Example: >>> await get_company_profile(CompanyProfileRequest(ticker="AAPL")) """ logger.info(f"Fetching company profile for {request.ticker} using yfinance") try: # Create ticker object stock = yf.Ticker(request.ticker) info = stock.info # Map yfinance data to CompanyProfile structure profile = CompanyProfile( ticker=request.ticker, company_name=info.get("longName") or info.get("shortName"), sector=info.get("sector"), industry=info.get("industry"), website=info.get("website"), description=info.get("longBusinessSummary"), ceo=info.get("companyOfficers", [{}])[0].get("name") if info.get("companyOfficers") else None, employees=info.get("fullTimeEmployees"), market_cap=Decimal(str(info["marketCap"])) if info.get("marketCap") else None, beta=Decimal(str(info["beta"])) if info.get("beta") else None, price=Decimal(str(info.get("currentPrice") or info.get("regularMarketPrice", 0))) if info.get("currentPrice") or info.get("regularMarketPrice") else None, volume_avg=info.get("averageVolume"), exchange=info.get("exchange"), ipo_date=info.get("ipoDate"), country=info.get("country"), ) logger.info(f"Successfully fetched profile for {request.ticker} using yfinance: {profile.company_name}") return profile except Exception as e: logger.error(f"Error fetching company profile for {request.ticker} using yfinance: {e}") # Return minimal profile on error return CompanyProfile(ticker=request.ticker) @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type((TimeoutError, ConnectionError, httpx.HTTPStatusError)), ) @mcp.tool() async def get_income_statement(request: FinancialStatementsRequest) -> List[IncomeStatement]: """Get income statement data. Args: request: Financial statements request Returns: List of income statements Example: >>> await get_income_statement(FinancialStatementsRequest(ticker="AAPL", period="annual", limit=5)) """ logger.info(f"Fetching income statement for {request.ticker}") try: data = await _make_request( f"income-statement/{request.ticker}", params={"period": request.period, "limit": request.limit} ) statements = [] for item in data: stmt = IncomeStatement( date=item.get("date", ""), revenue=Decimal(str(item["revenue"])) if item.get("revenue") else None, cost_of_revenue=Decimal(str(item["costOfRevenue"])) if item.get("costOfRevenue") else None, gross_profit=Decimal(str(item["grossProfit"])) if item.get("grossProfit") else None, operating_expenses=Decimal(str(item["operatingExpenses"])) if item.get("operatingExpenses") else None, operating_income=Decimal(str(item["operatingIncome"])) if item.get("operatingIncome") else None, ebitda=Decimal(str(item["ebitda"])) if item.get("ebitda") else None, net_income=Decimal(str(item["netIncome"])) if item.get("netIncome") else None, eps=Decimal(str(item["eps"])) if item.get("eps") else None, eps_diluted=Decimal(str(item["epsdiluted"])) if item.get("epsdiluted") else None, ) statements.append(stmt) logger.info(f"Fetched {len(statements)} income statements for {request.ticker}") return statements except Exception as e: logger.error(f"Error fetching income statement for {request.ticker}: {e}") return [] @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type((TimeoutError, ConnectionError, httpx.HTTPStatusError)), ) @mcp.tool() async def get_balance_sheet(request: FinancialStatementsRequest) -> List[BalanceSheet]: """Get balance sheet data. Args: request: Financial statements request Returns: List of balance sheets Example: >>> await get_balance_sheet(FinancialStatementsRequest(ticker="AAPL", period="annual")) """ logger.info(f"Fetching balance sheet for {request.ticker}") try: data = await _make_request( f"balance-sheet-statement/{request.ticker}", params={"period": request.period, "limit": request.limit} ) sheets = [] for item in data: sheet = BalanceSheet( date=item.get("date", ""), total_assets=Decimal(str(item["totalAssets"])) if item.get("totalAssets") else None, total_current_assets=Decimal(str(item["totalCurrentAssets"])) if item.get("totalCurrentAssets") else None, cash_and_cash_equivalents=Decimal(str(item["cashAndCashEquivalents"])) if item.get("cashAndCashEquivalents") else None, total_liabilities=Decimal(str(item["totalLiabilities"])) if item.get("totalLiabilities") else None, total_current_liabilities=Decimal(str(item["totalCurrentLiabilities"])) if item.get("totalCurrentLiabilities") else None, total_debt=Decimal(str(item["totalDebt"])) if item.get("totalDebt") else None, total_stockholders_equity=Decimal(str(item["totalStockholdersEquity"])) if item.get("totalStockholdersEquity") else None, ) sheets.append(sheet) logger.info(f"Fetched {len(sheets)} balance sheets for {request.ticker}") return sheets except Exception as e: logger.error(f"Error fetching balance sheet for {request.ticker}: {e}") return [] @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type((TimeoutError, ConnectionError, httpx.HTTPStatusError)), ) @mcp.tool() async def get_cash_flow_statement(request: FinancialStatementsRequest) -> List[CashFlowStatement]: """Get cash flow statement data. Args: request: Financial statements request Returns: List of cash flow statements Example: >>> await get_cash_flow_statement(FinancialStatementsRequest(ticker="AAPL")) """ logger.info(f"Fetching cash flow statement for {request.ticker}") try: data = await _make_request( f"cash-flow-statement/{request.ticker}", params={"period": request.period, "limit": request.limit} ) statements = [] for item in data: stmt = CashFlowStatement( date=item.get("date", ""), operating_cash_flow=Decimal(str(item["operatingCashFlow"])) if item.get("operatingCashFlow") else None, capital_expenditure=Decimal(str(item["capitalExpenditure"])) if item.get("capitalExpenditure") else None, free_cash_flow=Decimal(str(item["freeCashFlow"])) if item.get("freeCashFlow") else None, net_cash_from_financing=Decimal(str(item["netCashUsedProvidedByFinancingActivities"])) if item.get("netCashUsedProvidedByFinancingActivities") else None, net_cash_from_investing=Decimal(str(item["netCashUsedForInvestingActivites"])) if item.get("netCashUsedForInvestingActivites") else None, net_change_in_cash=Decimal(str(item["netChangeInCash"])) if item.get("netChangeInCash") else None, ) statements.append(stmt) logger.info(f"Fetched {len(statements)} cash flow statements for {request.ticker}") return statements except Exception as e: logger.error(f"Error fetching cash flow statement for {request.ticker}: {e}") return [] @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type((TimeoutError, ConnectionError, httpx.HTTPStatusError)), ) @mcp.tool() async def get_financial_ratios(request: FinancialRatiosRequest) -> FinancialRatios: """Get key financial ratios. Args: request: Financial ratios request Returns: Financial ratios Example: >>> await get_financial_ratios(FinancialRatiosRequest(ticker="AAPL", ttm=True)) """ logger.info(f"Fetching financial ratios for {request.ticker}") try: endpoint = f"ratios-ttm/{request.ticker}" if request.ttm else f"ratios/{request.ticker}" data = await _make_request(endpoint) if not data or len(data) == 0: logger.warning(f"No ratios data found for {request.ticker}") return FinancialRatios(ticker=request.ticker) item = data[0] if isinstance(data, list) else data ratios = FinancialRatios( ticker=request.ticker, date=item.get("date"), net_profit_margin=Decimal(str(item["netProfitMargin"])) if item.get("netProfitMargin") else None, roe=Decimal(str(item["returnOnEquity"])) if item.get("returnOnEquity") else None, roa=Decimal(str(item["returnOnAssets"])) if item.get("returnOnAssets") else None, roic=Decimal(str(item["returnOnCapitalEmployed"])) if item.get("returnOnCapitalEmployed") else None, current_ratio=Decimal(str(item["currentRatio"])) if item.get("currentRatio") else None, quick_ratio=Decimal(str(item["quickRatio"])) if item.get("quickRatio") else None, cash_ratio=Decimal(str(item["cashRatio"])) if item.get("cashRatio") else None, asset_turnover=Decimal(str(item["assetTurnover"])) if item.get("assetTurnover") else None, inventory_turnover=Decimal(str(item["inventoryTurnover"])) if item.get("inventoryTurnover") else None, debt_to_equity=Decimal(str(item["debtEquityRatio"])) if item.get("debtEquityRatio") else None, debt_to_assets=Decimal(str(item["debtRatio"])) if item.get("debtRatio") else None, interest_coverage=Decimal(str(item["interestCoverage"])) if item.get("interestCoverage") else None, ) logger.info(f"Successfully fetched ratios for {request.ticker}") return ratios except Exception as e: logger.error(f"Error fetching financial ratios for {request.ticker}: {e}") return FinancialRatios(ticker=request.ticker) @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type((TimeoutError, ConnectionError, httpx.HTTPStatusError)), ) @mcp.tool() async def get_key_metrics(request: KeyMetricsRequest) -> KeyMetrics: """Get key company metrics. Args: request: Key metrics request Returns: Key metrics Example: >>> await get_key_metrics(KeyMetricsRequest(ticker="AAPL")) """ logger.info(f"Fetching key metrics for {request.ticker}") try: endpoint = f"key-metrics-ttm/{request.ticker}" if request.ttm else f"key-metrics/{request.ticker}" data = await _make_request(endpoint) if not data or len(data) == 0: logger.warning(f"No key metrics found for {request.ticker}") return KeyMetrics(ticker=request.ticker) item = data[0] if isinstance(data, list) else data metrics = KeyMetrics( ticker=request.ticker, date=item.get("date"), market_cap=Decimal(str(item["marketCap"])) if item.get("marketCap") else None, pe_ratio=Decimal(str(item["peRatio"])) if item.get("peRatio") else None, price_to_book=Decimal(str(item["pbRatio"])) if item.get("pbRatio") else None, price_to_sales=Decimal(str(item["priceToSalesRatio"])) if item.get("priceToSalesRatio") else None, enterprise_value=Decimal(str(item["enterpriseValue"])) if item.get("enterpriseValue") else None, ev_to_ebitda=Decimal(str(item["evToEbitda"])) if item.get("evToEbitda") else None, revenue_per_share=Decimal(str(item["revenuePerShare"])) if item.get("revenuePerShare") else None, earnings_per_share=Decimal(str(item["netIncomePerShare"])) if item.get("netIncomePerShare") else None, book_value_per_share=Decimal(str(item["bookValuePerShare"])) if item.get("bookValuePerShare") else None, operating_cash_flow_per_share=Decimal(str(item["operatingCashFlowPerShare"])) if item.get("operatingCashFlowPerShare") else None, free_cash_flow_per_share=Decimal(str(item["freeCashFlowPerShare"])) if item.get("freeCashFlowPerShare") else None, ) logger.info(f"Successfully fetched key metrics for {request.ticker}") return metrics except Exception as e: logger.error(f"Error fetching key metrics for {request.ticker}: {e}") return KeyMetrics(ticker=request.ticker) # Export the MCP server if __name__ == "__main__": mcp.run()