Spaces:
Running
on
Zero
Running
on
Zero
| """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() | |
| 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) | |
| 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 [] | |
| 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 [] | |
| 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 [] | |
| 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) | |
| 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() | |