world-cup-stats-mcp
by lukejbyrne
README.md
# World Cup Stats MCP
A read-only MCP server that gives compatible AI clients structured access to a
World Cup SQLite database. It supports local stdio development and
authenticated Streamable HTTP deployment from the same codebase.
```text
AI host
└── MCP client
├── stdio ── local server process
└── HTTPS ── remote server on Fly.io
└── read-only SQLite
```
## Production scope
For this project, production-ready means a private, single-tenant, read-only
service with:
- typed tools and bounded result sizes;
- read-only database access and safe tool errors;
- automated data, protocol and authenticated HTTP tests;
- a health endpoint and structured server logs;
- a pinned, non-root Docker image;
- TLS deployment on Fly.io with a pre-shared bearer token.
This is not an OAuth service for arbitrary third-party users. A public,
multi-user MCP product should replace the pre-shared token with a real OAuth
authorization server and add its own usage limits and monitoring.
## Available tools
- `list_tournaments` lists available men’s or women’s tournaments.
- `get_team_history` returns a country’s tournament records and totals.
- `get_head_to_head` finds World Cup meetings between two countries.
- `get_player_record` returns appearances, starts, goals and knockout statistics.
All tools are read-only. The server does not expose arbitrary SQL.
## Dataset
The project includes the SQLite edition of
[The Fjelstul World Cup Database](https://github.com/jfjelstul/worldcup):
- 22 men’s tournaments from 1930–2022;
- 8 women’s tournaments from 1991–2019;
- matches, teams, squads, appearances, goals, cards and standings.
Attribution, licence and the exact checksum are in
[`data/README.md`](data/README.md). The database and schema are distributed
under CC BY-SA 4.0.
## Local setup
Requirements:
- Python 3.12 or newer;
- [uv](https://docs.astral.sh/uv/);
- Node.js if using MCP Inspector.
Install the locked dependencies:
```bash
cd "/Users/lukebyrne/Documents/builds/world-cup-stats-mcp"
uv sync --no-editable
```
Run all tests:
```bash
uv run --no-editable pytest
```
Run an ordinary Python query before introducing MCP:
```bash
uv run --no-editable python scripts/demo_query.py
```
## Test the local stdio server
Launch MCP Inspector:
```bash
npx -y @modelcontextprotocol/inspector \
uv \
--directory "/Users/lukebyrne/Documents/builds/world-cup-stats-mcp" \
run \
--no-editable \
world-cup-stats-mcp
```
In Inspector, open **Tools**, list the tools and call
`get_player_record` with `Kylian Mbappe`.
The ready-to-use stdio configuration is in [`mcp.json`](mcp.json).
## Test Streamable HTTP locally
Create a temporary token and start the HTTP transport:
```bash
export MCP_API_TOKEN="$(openssl rand -hex 32)"
export MCP_TRANSPORT="streamable-http"
export MCP_PUBLIC_BASE_URL="http://127.0.0.1:8000"
export MCP_ALLOWED_HOSTS="127.0.0.1:8000,localhost:8000"
uv run --no-editable world-cup-stats-mcp
```
The endpoints are:
```text
Health: http://127.0.0.1:8000/health
MCP: http://127.0.0.1:8000/mcp
```
In Inspector, select **Streamable HTTP**, enter the MCP URL and paste the token
into **Bearer Token**. An omitted or incorrect token receives HTTP 401.
The repeatable command-line smoke test is:
```bash
MCP_SERVER_URL="http://127.0.0.1:8000/mcp" \
MCP_API_TOKEN="$MCP_API_TOKEN" \
uv run --no-editable python scripts/remote_smoke_test.py
```
## Build the container
The Docker image contains the immutable database snapshot and runs as a
non-root user:
```bash
docker build -t world-cup-stats-mcp .
docker run --rm \
-p 8000:8000 \
-e MCP_API_TOKEN="$MCP_API_TOKEN" \
-e MCP_PUBLIC_BASE_URL="http://127.0.0.1:8000" \
-e MCP_ALLOWED_HOSTS="127.0.0.1:8000,localhost:8000" \
world-cup-stats-mcp
```
Fly can build the same Dockerfile remotely, so a local Docker daemon is not
required for deployment.
## Deploy to Fly.io
Install and authenticate Fly’s CLI:
```bash
brew install flyctl
fly auth login
```
The repository contains a checked-in `fly.toml`. Set the bearer token as a Fly
secret, then deploy:
```bash
fly secrets set MCP_API_TOKEN="$MCP_API_TOKEN"
fly deploy
```
Check the deployment:
```bash
fly status
fly logs
curl --fail "https://world-cup-mcp-luke.fly.dev/health"
```
Run the authenticated MCP smoke test:
```bash
MCP_SERVER_URL="https://world-cup-mcp-luke.fly.dev/mcp" \
MCP_API_TOKEN="$MCP_API_TOKEN" \
uv run --no-editable python scripts/remote_smoke_test.py
```
## Recording prompts
Start with:
> Compare Lionel Messi and Kylian Mbappé’s World Cup records through 2022.
> Include appearances, starts, total goals, knockout goals, final appearances
> and final goals. Use only the connected World Cup tools.
Then demonstrate a multi-tool question:
> Compare Argentina and France at the men’s World Cup since 1990. Include each
> team’s tournament record and their head-to-head matches, then give me an
> evidence-based verdict. Use only the connected World Cup tools.
The second prompt should call `get_team_history` twice and
`get_head_to_head` once.
## Data conventions
- Match wins and losses follow the source database’s convention. A knockout
match decided on penalties counts as a win for the shootout winner.
- Shootout wins and losses are also returned separately.
- Own goals are excluded from personal goal totals.
- Germany and West Germany remain separate source teams.
- SQLite is opened with `mode=ro` and `PRAGMA query_only`.
- Set `WORLDCUP_DB_PATH` only when moving the database file.
TDQS
B3.3/5.0
Scored across 4 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: listing tournaments, team history, head-to-head matchups, and player records. No overlap or ambiguity.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern (list_tournaments, get_team_history, get_head_to_head, get_player_record), using 'get' for data retrieval and 'list' for enumeration.
Tool Count4/5
With 4 tools, the server is slightly under-scoped for a World Cup stats domain, but each tool covers a meaningful query. The count is reasonable and not problematic.
Completeness3/5
The tool surface covers team history, head-to-head, and player records, but lacks individual match details or tournament standings, which are notable gaps for a stats MCP.
Maintenance
ActivitySlowing
ResponsivenessNo issues