Skip to main content
Glama
fferegrino

ligamx-mcp

by fferegrino
README.md
# Liga MX API

Query Liga MX form, head-to-head history, and tournament standings from [football-data.co.uk](https://www.football-data.co.uk/) match data.

Data is rebuilt daily into a local SQLite database (`data/ligamx.db`). Form and H2H are answered from the `matches` table; standings are materialised at rebuild time.

## Setup

Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).

```bash
uv sync
DATA_URL=https://www.football-data.co.uk/new/MEX.csv ./scripts/rebuild_db.sh
```

Or rebuild from an existing CSV without re-downloading:

```bash
uv run python -m ligamx.etl
```

## Run the server

One process serves both the REST API and MCP (streamable HTTP):

```bash
uv run ligamx-serve
# or
uv run uvicorn ligamx.api:app --reload
```

- REST docs: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)
- MCP endpoint: `http://127.0.0.1:8000/mcp`

## Deploy on Fly.io

The image rebuilds `data/ligamx.db` from `DATA_URL` on every start (fits free machines that stop when idle).

Set the CSV URL as a Fly secret (required by `scripts/rebuild_db.sh`):

```bash
fly secrets set DATA_URL=https://www.football-data.co.uk/new/MEX.csv
```

```bash
# once: install flyctl, log in, then from the repo root:
fly launch        # accept the existing fly.toml; pick a unique app name if needed
fly deploy
```

Useful checks:

```bash
fly status
fly logs
fly open /docs
```

App URL will look like `https://<app-name>.fly.dev` — MCP at `/mcp`, REST docs at `/docs`.

After deploy, optional daily refresh without idle sleep:

```bash
fly machine run --schedule daily ...   # or just rely on rebuild-on-boot when traffic wakes the machine
```

On the free allowance, `auto_stop_machines` means the first request after idle pays a cold start (download CSV + ETL + boot).

### MCP (HTTP)

Point an MCP client at the streamable HTTP URL (no stdio):

```json
{
  "mcpServers": {
    "ligamx": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}
```

Exact client config keys vary; the transport is **streamable HTTP** at `/mcp`.

### MCP tools

| Tool | Description |
|------|-------------|
| `list_teams` | All known team slugs |
| `team_form` | Last `n` matches for a team (default 5, max 30) |
| `head_to_head` | Historical H2H between two slugs |
| `current_standings` | Full current tournament table |
| `team_position` | One team’s row in the current table |

REST and MCP share [`ligamx/service.py`](ligamx/service.py) payloads.

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/health` | Liveness check |
| `GET` | `/teams/{slug}/form?n=5` | Last `n` matches for a team (default 5, max 30), newest first |
| `GET` | `/h2h/{slug_a}/{slug_b}` | Historical head-to-head + summary |
| `GET` | `/standings/current` | Full table for the current tournament |
| `GET` | `/teams/{slug}/position` | One team’s row in the current table |
| `*` | `/mcp` | MCP streamable HTTP endpoint |

Unknown team slugs return `404`.

### Examples

```bash
curl 'http://127.0.0.1:8000/teams/america/form?n=10'
curl 'http://127.0.0.1:8000/h2h/america/guadalajara'
curl 'http://127.0.0.1:8000/standings/current'
curl 'http://127.0.0.1:8000/teams/tigres/position'
```

## Pipeline

```text
MEX.csv  →  ligamx.etl  →  ligamx.db  →  FastAPI (REST + MCP /mcp)
                 │
                 ├─ matches (raw names + slugs)
                 └─ standings (per season + tournament)
```

1. `scripts/rebuild_db.sh` downloads `DATA_URL` (required env var) into `data/MEX.csv` and runs the ETL.
2. ETL loads matches via the CSV reader, normalises team names to slugs, writes a temp SQLite file, then atomically replaces `data/ligamx.db`.
3. Standings are aggregated with 3/1/0 points and ranked by **points → goal difference → goals for**.

### Tournaments

The CSV only has a `Season` string (e.g. `2025/2026`). Short tournaments are derived from the match date:

- **Apertura** — July–December
- **Clausura** — January–June

“Current” standings use the season and tournament of the latest match in the database.

## Team slugs

Raw CSV labels are kept on each match; API paths use stable slugs from `ligamx/teams.py`:

| Slug | CSV label |
|------|-----------|
| `america` | Club America |
| `atlante` | Atlante |
| `atlas` | Atlas |
| `atletico-san-luis` | Atl. San Luis |
| `chiapas` | Chiapas |
| `cruz-azul` | Cruz Azul |
| `dorados` | Dorados de Sinaloa |
| `guadalajara` | Guadalajara Chivas |
| `juarez` | Juarez |
| `leon` | Club Leon |
| `leones-negros` | Leones Negros |
| `lobos-buap` | Lobos BUAP |
| `mazatlan` | Mazatlan FC |
| `monterrey` | Monterrey |
| `morelia` | Monarcas |
| `necaxa` | Necaxa |
| `pachuca` | Pachuca |
| `puebla` | Puebla |
| `pumas` | UNAM Pumas |
| `queretaro` | Queretaro |
| `santos-laguna` | Santos Laguna |
| `tigres` | Tigres UANL |
| `tijuana` | Club Tijuana |
| `toluca` | Toluca |
| `veracruz` | Veracruz |

## Package layout

| Path | Role |
|------|------|
| `ligamx/reader.py` | Parse `MEX.csv` into `Match` rows |
| `ligamx/teams.py` | Raw name → slug map |
| `ligamx/db.py` | Schema + SQLite connection |
| `ligamx/etl.py` | Daily rebuild + standings |
| `ligamx/queries.py` | Form / H2H / standings helpers |
| `ligamx/service.py` | Shared JSON payloads for API + MCP |
| `ligamx/api.py` | FastAPI REST + MCP HTTP mount |
| `ligamx/mcp_server.py` | MCP tool definitions |
| `scripts/rebuild_db.sh` | Download + ETL entrypoint |
| `scripts/start.sh` | Container entrypoint (rebuild + serve) |
| `Dockerfile` / `fly.toml` | Fly.io deploy |
| `data/notes.txt` | football-data.co.uk column key |

## Library usage

```python
from ligamx.queries import get_form, get_h2h, get_standings, get_position

get_form("america", n=5)
get_h2h("america", "guadalajara")
get_standings()           # current tournament
get_position("tigres")
```