BrianIsaac's picture
feat: implement 7 production enhancements for portfolio analysis platform
9f411df
"""FRED (Federal Reserve Economic Data) MCP Server.
This MCP server provides macroeconomic data from the Federal Reserve Economic Data API.
Free tier: Unlimited requests with API key.
"""
import logging
from datetime import datetime
from typing import Dict, List, Optional
from decimal import Decimal
import httpx
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("fred")
# API Configuration
BASE_URL = "https://api.stlouisfed.org/fred"
API_KEY = settings.fred_api_key
class SeriesRequest(BaseModel):
"""Request for economic data series."""
series_id: str = Field(..., description="FRED series ID (e.g., GDP, UNRATE)")
observation_start: Optional[str] = Field(None, description="Start date (YYYY-MM-DD)")
observation_end: Optional[str] = Field(None, description="End date (YYYY-MM-DD)")
class SeriesObservation(BaseModel):
"""Single observation from a FRED series."""
date: str
value: Optional[Decimal] = None
class EconomicSeries(BaseModel):
"""Economic data series."""
series_id: str
title: Optional[str] = None
units: Optional[str] = None
frequency: Optional[str] = None
observations: List[SeriesObservation] = Field(default_factory=list)
async def _make_request(endpoint: str, params: Dict[str, str]) -> Dict:
"""Make HTTP request to FRED API."""
params["api_key"] = API_KEY
params["file_type"] = "json"
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, Exception)),
)
@mcp.tool()
async def get_economic_series(request: SeriesRequest) -> EconomicSeries:
"""Get economic data series from FRED.
Args:
request: Series request
Returns:
Economic series with observations
Example:
>>> await get_economic_series(SeriesRequest(series_id="GDP"))
Common Series IDs:
- GDP: Gross Domestic Product
- UNRATE: Unemployment Rate
- CPIAUCSL: Consumer Price Index
- DFF: Federal Funds Rate
- T10Y2Y: 10-Year minus 2-Year Treasury Yield
"""
logger.info(f"Fetching FRED series {request.series_id}")
try:
# Get series info
series_params = {"series_id": request.series_id}
series_data = await _make_request("series", series_params)
series_info = series_data.get("seriess", [{}])[0] if series_data.get("seriess") else {}
# Get observations
obs_params = {"series_id": request.series_id}
if request.observation_start:
obs_params["observation_start"] = request.observation_start
if request.observation_end:
obs_params["observation_end"] = request.observation_end
obs_data = await _make_request("series/observations", obs_params)
observations = []
for obs in obs_data.get("observations", []):
value = None
if obs.get("value") and obs["value"] != ".":
try:
value = Decimal(obs["value"])
except:
pass
observations.append(
SeriesObservation(date=obs.get("date", ""), value=value)
)
series = EconomicSeries(
series_id=request.series_id,
title=series_info.get("title"),
units=series_info.get("units"),
frequency=series_info.get("frequency"),
observations=observations,
)
logger.info(f"Fetched {len(observations)} observations for {request.series_id}")
return series
except Exception as e:
logger.error(f"Error fetching FRED series {request.series_id}: {e}")
return EconomicSeries(series_id=request.series_id)
# Export the MCP server
if __name__ == "__main__":
mcp.run()