Skip to main content
Glama
tiansern92-collab

worldcup-mcp

README.md
# worldcup-mcp

A local **MCP server (stdio)** exposing live **2026 FIFA World Cup** group standings and a
Round-of-32 bracket, so a Cowork HTML artifact can call it on page load and render current data.

- TypeScript (strict), Node ≥ 20, `@modelcontextprotocol/sdk` over stdio.
- Provider abstraction (`StandingsProvider`) + 60s cache + `stale` last-good fallback.
- Pure-logic layer (third-place ranking, best-third allocation, confidence) is fully unit-tested.

> **Status:** the server is complete and its logic/handshake tests pass offline. The **live data
> gate is unmet until you add a free API token** — see [`DATA_SOURCE.md`](./DATA_SOURCE.md).
> Without a token the tools return an all-undecided bracket marked `stale` (no faked standings).

---

## 1. Install

```bash
cd worldcup-mcp
npm install
cp .env.example .env      # then edit .env (see below)
```

## 2. Environment

| Var | Purpose |
| --- | --- |
| `PROVIDER` | `football-data` (default, recommended) or `thesportsdb` |
| `FOOTBALL_DATA_TOKEN` | free token from <https://www.football-data.org/client/register> |
| `THESPORTSDB_KEY` | `3` (free, **capped at ~5 rows**) or a Premium key |

Get the free football-data token (instant email), put it in `.env`:

```
PROVIDER=football-data
FOOTBALL_DATA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
```

## 3. Scripts

```bash
npm run dev        # run the server from source (tsx) over stdio
npm run build      # tsc -> dist/
npm start          # node dist/server.js
npm test           # vitest — pure-logic unit tests (no network)
npm run verify     # LIVE smoke test: one real fetch, prints 12 tables + 16 R32 slots, asserts
npm run handshake  # builds, launches server over stdio, lists+calls both tools, validates shapes
```

## 4. Tools & output contracts

### `get_group_standings`
Returns an object keyed by group letter `A`–`L`; each value is an array ordered rank 1→4 of
`{ name, code, played, won, drawn, lost, goalsFor, goalsAgainst, goalDifference, points, rank,
confidence }`. `confidence` ∈ `confirmed | likely | open` (see classifier below). The object also
carries `stale` and `fetchedAt` alongside the letter keys.

### `get_round_of_32`
Returns `{ matches: [...16], stale, fetchedAt }`. Each match:
`{ match, posA, posB, slotA, slotB }`, where a slot is either
`{ posCode, decided:true, team:{name,code,confidence} }` or
`{ posCode, decided:false, eligibleGroups:[...] }`.

**Bracket positions are fixed** (from the official draw); only the teams change. Position map is in
[`src/logic/bracket.ts`](./src/logic/bracket.ts).

**Resolution rules**
- *Rank (1x/2x) slot:* `decided:true` with the current holder of that exact rank once the group has
  begun, carrying its `confidence`; `decided:false` with `eligibleGroups:[group]` before any game.
- *Best-third slot:* stays `decided:false` (with the 5-group pool) until the group stage is
  **complete**; then the top-8 thirds are ranked by **points → goal difference → goals scored** and
  allocated to slots honoring each slot's `eligibleGroups`.

### `get_knockout_results`
Returns `{ rounds, stale, fetchedAt }` where `rounds` is `{ r16:[…8], qf:[…4], sf:[…2],
third:[…1], final:[…1] }`. Each match: `{ match, posA, posB, slotA, slotB, status, score,
winnerCode }`:
- `slotA`/`slotB` mirror the `get_round_of_32` shape: `{ posCode, decided:true,
  team:{name,code,confidence} }` once the feeding match is finished, else `{ posCode,
  decided:false, feeder }` (a label like `"R32 M12"` / `"R16 #89"`).
- `status` ∈ `scheduled | live | finished`; `score` is `{ a, b, duration?, penalties? }` or `null`;
  `winnerCode` is set **only** when `status === "finished"`.
- **Bracket wiring is ours** (the official 2026 map: R32 ties M1..M16 feed forward as consecutive
  pairs → R16 → QF → SF → final); **results are overlaid from the live source** by matching teams.
  Match numbers use the official 89–104. See [`src/logic/knockout.ts`](./src/logic/knockout.ts).
- Real winners propagate: a slot fills as soon as its feeding match is finished, otherwise it stays
  a placeholder. If the provider can't supply knockout data, every node is a placeholder + `stale`.

### `confidence` classifier
`confirmed` = mathematically locked at the exact rank (every other team is clearly above or clearly
below given remaining fixtures; reachable-point ties count as not-yet-separated, so it never
over-claims). `likely` = currently in a qualifying slot (rank 1–2) but not locked. `open` = otherwise.
Once every team in a group has played all 3 games, all four are `confirmed`.

## 5. Cowork / Claude Desktop integration

Register this server under the slug **`worldcup`** so the tools resolve to the names the artifact
expects:

- `mcp__worldcup__get_group_standings`
- `mcp__worldcup__get_round_of_32`
- `mcp__worldcup__get_knockout_results`

These are already the defaults in [`artifact/wc2026_bracket.html`](./artifact/wc2026_bracket.html)
(`TOOL_STANDINGS` / `TOOL_RO32` / `TOOL_KO`). If you register under a different key, update those
constants. The artifact renders the live R32 + a projected bracket, and overlays **real** knockout
results from `get_knockout_results` once matches are played (green "result" boxes with scores;
muted boxes remain projections).

MCP config block (Claude Desktop `claude_desktop_config.json` → `mcpServers`, or the Cowork MCP
config). Use the **built** entry for stability:

```json
{
  "mcpServers": {
    "worldcup": {
      "command": "node",
      "args": ["C:\\Users\\U\\OneDrive\\worldcup-mcp\\dist\\server.js"],
      "env": {
        "PROVIDER": "football-data",
        "FOOTBALL_DATA_TOKEN": "your-free-token"
      }
    }
  }
}
```

(Run `npm run build` first so `dist/server.js` exists. For a no-build dev setup, use
`"command": "npx", "args": ["tsx", "C:\\Users\\U\\OneDrive\\worldcup-mcp\\src\\server.ts"]`.)

**Freshness for the artifact's "Live / stale" banner:** football-data.org refreshes tables within
**~1–5 minutes** of a match finishing (it is not a per-second live feed). Expect "Live" right after
a refresh and "stale" only if the provider call fails (then last-good is served).

## 6. Manual FIFA cross-check

To eyeball that live numbers are real and current, compare one group against the official source:

```bash
# Group A table straight from the provider (needs your token):
curl -s -H "X-Auth-Token: $FOOTBALL_DATA_TOKEN" \
  "https://api.football-data.org/v4/competitions/WC/standings" \
  | npx --yes node-jq '.standings[] | select(.group=="GROUP_A") | .table[] | {pos:.position, team:.team.name, pld:.playedGames, pts:.points, gd:.goalDifference}'
```

Then open the official FIFA standings page for the 2026 World Cup
(<https://www.fifa.com/fifaplus/en/tournaments/mens/worldcup/canadamexicousa2026>) and confirm the
played/points/GD for Group A match. `npm run verify` performs the structural half of this check
automatically (groups, team counts, codes vs. the official draw).

## 7. Known limitations

- **Best-third allocation method.** FIFA publishes a fixed lookup table keyed by *which* 8 of 12
  groups produce a qualifying third (C(12,8)=495 rows). Rather than embed that table verbatim, this
  server solves the same constraint it encodes — a deterministic eligibility-honoring bipartite
  matching (see [`src/logic/thirdPlace.ts`](./src/logic/thirdPlace.ts)). Every group always lands in
  an **eligible** slot; for some of the 495 combinations the exact group→slot pairing may differ
  from FIFA's printed table while staying eligibility-correct. Swap in the literal table here if you
  need byte-for-byte parity with FIFA's published bracket.
- **Free data sources are credential-gated or capped** — see `DATA_SOURCE.md`. Nothing is hardcoded;
  absent a credential the bracket is all-undecided + `stale`.

## 8. Layout

```
src/
  draw.ts            official group draw + name→code mapping (validation only)
  types.ts           shared types
  cache.ts           60s TTL + last-good stale fallback
  service.ts         provider selection, enrich(), the two tool entry points
  server.ts          MCP stdio server (registers the two tools)
  verify.ts          live smoke test
  env.ts             local .env loader
  providers/
    types.ts         StandingsProvider interface (the swap seam)
    footballData.ts  primary provider
    theSportsDb.ts   secondary provider (capped on free tier)
  logic/
    confidence.ts    confidence classifier
    thirdPlace.ts    third-place ranking + best-third allocation
    bracket.ts       fixed R32 position map + resolution
test/                vitest unit tests (confidence, thirdPlace, bracket)
scripts/handshake.ts MCP stdio handshake test
```

TDQS

A3.9/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one for group standings and one for the round of 32 bracket. There is no overlap, so an agent can easily differentiate them.

Naming Consistency5/5

Both tools follow the consistent 'get_<noun>' naming pattern (get_group_standings, get_round_of_32), providing a predictable and uniform interface.

Tool Count3/5

With only 2 tools, the server feels under-scoped for a comprehensive World Cup data service. While the tools are relevant, additional tools for match details or team info would be expected.

Completeness2/5

The server covers only group standings and the round of 32 bracket, missing critical features like match results, team information, player stats, and later knockout stages. Significant gaps exist for a full World Cup data server.