Skip to main content
Glama
README.md
# findata-mcp

A unified financial data library with an **MCP server** for **code-writing agents**.

When an agent queries the MCP (e.g. `"equity daily prices"`), it receives:
- The **findata wrapper function** signature
- Full parameter and return-type documentation
- A ready-to-paste code example calling our API

The MCP never fetches live data — it is a documentation server so agents
can write correct calls to the findata library.

---

## Project structure

```
data-mcp/
├── findata/                         Data library
│   ├── equity_prices.py             get_equity_prices()           yfinance wrapper
│   ├── sp500_composition.py         get_sp500_composition()       fja05680/sp500 (local git clone)
│   ├── fama_french.py               get_fama_french_factors()     Ken French Data Library
│   ├── fred.py                      get_fred_series()             FRED macroeconomic series
│   ├── cboe_volatility.py           get_cboe_volatility_indices() VIX / VVIX
│   ├── coingecko.py                 get_coingecko_ohlcv()         CoinGecko public API
│   ├── file_reader.py               get_file_data()               CSV / Parquet / Excel
│   └── bloomberg.py                 get_bloomberg_ticks()         blpapi — tick-by-tick
│                                    get_bloomberg_bars()          blpapi — intraday OHLCV
│                                    get_bloomberg_data()          blpapi — historical / reference
├── findata_mcp/
│   └── server.py                    Tool registry + MCP handlers
├── Dockerfile
├── docker-compose.yml
├── .github/workflows/docker.yml     GHCR build + push on every push to main
├── pyproject.toml
└── README.md
```

---

## Installation

The recommended way to run findata-mcp is via Docker. The image is published to GHCR on every push to `main` and includes Codex CLI baked in.

### Prerequisites

- Docker
- Codex authenticated on your host machine

### 1. Authenticate Codex (one-time)

```bash
codex auth login    # opens browser → saves to ~/.codex/auth.json
```

### 2. Pull and run

```bash
curl -O https://raw.githubusercontent.com/lakshya-aga/data-mcp/main/docker-compose.yml
docker compose up -d
```

`docker-compose.yml` mounts `~/.codex` read-only so the container inherits your Codex session with no interactive prompts. Named volumes keep generated files and data across restarts.

### 3. Verify

```bash
docker logs data-mcp-findata-mcp-1
# should show: findata-mcp starting on :8000
```

---

## Connecting to the server

### Claude Desktop

Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "findata": {
      "url": "http://localhost:8000/sse"
    }
  }
}
```

### Python (raw MCP client)

```python
import asyncio
from mcp.client.sse import sse_client
from mcp.client.session import ClientSession

async def main():
    async with sse_client("http://localhost:8000/sse") as (r, w):
        async with ClientSession(r, w) as s:
            await s.initialize()
            res = await s.call_tool("search_tools", {"query": "equity daily prices", "top_k": 3})
            print(res.content[0].text)

asyncio.run(main())
```

### OpenAI Agents SDK

```python
from agents.mcp import MCPServerSse
mcp = MCPServerSse(url="http://localhost:8000/sse")
```

---

## MCP tools

| Tool | Description |
|------|-------------|
| `search_tools` | Natural-language query → matching function docs + code examples |
| `get_tool_doc` | Full reference for one function by exact name |
| `list_all_tools` | All wrapper functions with summaries and tags |
| `request_data_source` | Ask Codex to implement and register a new data wrapper |

### search_tools

```python
res = await s.call_tool("search_tools", {"query": "fama french factors", "top_k": 2})
```

### get_tool_doc

```python
res = await s.call_tool("get_tool_doc", {"tool_name": "get_equity_prices"})
```

### request_data_source

```python
res = await s.call_tool("request_data_source", {
    "description": "get World Bank GDP per capita using the wbdata library"
})
```

Codex writes `findata/<module>.py`, updates `server.py`, and hot-reloads the new function into the live registry — no restart needed.

---

## Environment variables

| Variable | Description |
|----------|-------------|
| `OPENAI_API_KEY` | Codex auth — skips OAuth if set (alternative to host auth mount) |
| `FRED_API_KEY` | Required for `get_fred_series`. Free at [fred.stlouisfed.org](https://fred.stlouisfed.org/docs/api/api_key.html) |
| `CODEX_CLI_PATH` | Override Codex binary path (defaults to `codex` on PATH) |
| `BLOOMBERG_HOST` | Bloomberg API host for the `get_bloomberg_*` functions (default `localhost`) |
| `BLOOMBERG_PORT` | Bloomberg API port (default `8194`) |
| `BLOOMBERG_TIMEOUT_MS` | Per-event wait before a Bloomberg request is abandoned (default `30000`) |
| `BLOOMBERG_AUTH` | `setAuthenticationOptions` string for Server API / B-PIPE, e.g. `AuthenticationType=OS_LOGON`. Leave unset for a desktop Terminal |

---

## findata quick reference

### `get_equity_prices`
```python
from findata.equity_prices import get_equity_prices

df = get_equity_prices(
    tickers=["AAPL", "MSFT"],
    start_date="2024-01-01",
    end_date="2024-12-31",
    fields=["Close"],
    frequency="1d",         # 1d 5d 1wk 1mo 3mo
)
```

### `get_fama_french_factors`
```python
from findata.fama_french import get_fama_french_factors

df = get_fama_french_factors(factor_model="5", start_date="2010-01-01", end_date="2020-12-31")
# columns: Mkt-RF, SMB, HML, RMW, CMA, RF
```

### `get_fred_series`
```python
from findata.fred import get_fred_series

df = get_fred_series(["CPIAUCSL", "UNRATE"], start_date="2015-01-01", end_date="2024-12-31")
```

### `get_coingecko_ohlcv`
```python
from findata.coingecko import get_coingecko_ohlcv

df = get_coingecko_ohlcv("bitcoin", vs_currency="usd", days=90)
# columns: open, high, low, close, volume
```

### `get_cboe_volatility_indices`
```python
from findata.cboe_volatility import get_cboe_volatility_indices

df = get_cboe_volatility_indices(symbols=["^VIX", "^VVIX"], start_date="2020-01-01", end_date="2024-12-31")
```

### `get_sp500_composition`
```python
from findata.sp500_composition import get_sp500_composition

members = get_sp500_composition("2024-12-31")   # list[str], ~503 tickers
```

### `get_bloomberg_ticks` — tick-level data for any security

Requires a Bloomberg Terminal, SAPI or B-PIPE entitlement and the SDK:

```bash
pip install blpapi --index-url https://bcms.bloomberg.com/pip/simple/
```

```python
from findata.bloomberg import get_bloomberg_ticks

# Every trade print in one US cash session, in New York time
ticks = get_bloomberg_ticks(
    "AAPL US Equity",
    "2024-06-03 09:30:00",
    "2024-06-03 16:00:00",
    tz="America/New_York",
)
# index = time (tz-aware)
# columns: security, type, value, size, conditionCodes, exchangeCode

# Trades AND top-of-book quotes, one hour per sub-request
book = get_bloomberg_ticks(
    "ESZ5 Index",
    "2024-06-03 13:30:00",
    "2024-06-03 20:00:00",
    event_types=["TRADE", "BID", "ASK"],
    chunk="1h",
)

# VWAP straight from the prints
trades = ticks[ticks["type"] == "TRADE"]
vwap = (trades["value"] * trades["size"]).sum() / trades["size"].sum()
```

The security string is passed to Bloomberg verbatim, so anything the
Terminal quotes works — `"AAPL US Equity"`, `"ESZ5 Index"`,
`"EURUSD Curncy"`, `"TY1 Comdty"`, `"US912810TW33 Govt"`.

Long windows are sliced into `chunk`-sized sub-requests (default one day)
and concatenated, because Bloomberg truncates an oversized intraday
response silently rather than erroring. Bloomberg's own limits still
apply: intraday tick history reaches back roughly 140 days, and daily
volume caps are tied to your entitlement.

Reuse one session when looping over a universe — it avoids a connection
handshake per name:

```python
from findata.bloomberg import BloombergSession, get_bloomberg_ticks

with BloombergSession() as bbg:
    frames = {
        sym: get_bloomberg_ticks(sym, start, end, session=bbg)
        for sym in universe
    }
```

Aggregate ticks into bars locally, at any pandas offset:

```python
from findata.bloomberg import ticks_to_bars

bars = ticks_to_bars(ticks, rule="5min")
# columns: open, high, low, close, ticks, volume, vwap
```

### `get_bloomberg_bars`
```python
from findata.bloomberg import get_bloomberg_bars

# 5-minute bars, aggregated server-side (much lighter than raw ticks)
bars = get_bloomberg_bars(
    "AAPL US Equity",
    "2024-06-03 09:30:00",
    "2024-06-03 16:00:00",
    interval=5,                 # minutes, 1-1440
    tz="America/New_York",
)
# columns: security, open, high, low, close, volume, numEvents, value
```

### `get_bloomberg_data`
```python
from findata.bloomberg import get_bloomberg_data

# Historical daily series -> MultiIndex columns (field, security)
df = get_bloomberg_data(
    tickers=["AAPL US Equity", "MSFT US Equity"],
    fields=["PX_LAST", "VOLUME"],
    start_date="2024-01-01",
    end_date="2024-12-31",
)
close = df["PX_LAST"]

# Reference data -> rows=securities, cols=fields
ref = get_bloomberg_data(
    tickers=["AAPL US Equity"],
    fields=["CUR_MKT_CAP", "GICS_SECTOR_NAME"],
    request_type="ReferenceDataRequest",
    overrides={"BEST_FPERIOD_OVERRIDE": "1BF"},
)
```

### `get_file_data`
```python
from findata.file_reader import get_file_data

df = get_file_data("data/prices.parquet", tickers=["AAPL"], start_date="2023-01-01", end_date="2023-12-31")
```

---

## Tests

```bash
pytest tests/ -v
```