euroleague-open-data
An MCP server that lets an LLM query a DuckDB warehouse of EuroLeague and EuroCup basketball data in natural language, offering tools for player and team stats, shot charts, boxscores, and fantasy draft analysis without contacting upstream APIs.
README
euroleague-open-data
An open EuroLeague / EuroCup basketball data warehouse, and an MCP server that lets an LLM query it in natural language.
Unofficial. Not affiliated with, endorsed by, or approved by Euroleague Basketball. Data originates from Euroleague Basketball and is retrieved from publicly accessible endpoints. For research and educational use. See DISCLAIMER.md.
Why this exists
The upstream EuroLeague API is undocumented, unversioned, and rate-limited at roughly 10 requests per minute by Cloudflare. That makes it unusable for interactive analysis: three questions in a row from one user would black out everyone else for five minutes.
So this project inverts the problem. A slow, polite, resumable crawler pulls the data into a local DuckDB warehouse once. Everything else — the MCP server, the Parquet exports — reads that snapshot.
The MCP server never contacts upstream. It holds no HTTP client. No amount of traffic to this project can generate load on Euroleague Basketball's infrastructure.
Status
Four seasons loaded: EuroLeague E2024 and E2025, EuroCup U2024 and U2025. Plus the announced 2026-27 EuroLeague squads, so the draft tools work before a game has been played.
| Component | State |
|---|---|
| Throttled crawler with permanent cache | working |
| DuckDB warehouse, 8 base tables | working |
| Validation suite, 8 reconciliation checks | working |
| Derived analytics (TS%, eFG%, usage, Four Factors, shot zones) | working |
| MCP server, stdio transport, 15 tools + 3 resources | working |
| HTTP transport + landing page, Docker image build-tested | working |
| Public deployment on Render (free instance, sleeps when idle) | live |
| Full backfill (52 seasons, 12 122 games) | not started, ~50h of crawling |
| Dataset publishing to GitHub Releases / HuggingFace | not started |
Quick start
Requires uv and Python 3.12+.
git clone https://github.com/McNews11/euroleague-open-data && cd euroleague-open-data
uv sync --extra etl
--extra etl pulls in the crawler's dependencies. Plain uv sync installs only what is
needed to serve an existing warehouse, which is what the deployed image does — it keeps
polars, pyarrow and httpx out of the container and halves its size.
Build the warehouse. The crawl is deliberately slow — about two hours for one season — and it is safe to interrupt and rerun, because every response is cached permanently.
uv run euroleague-etl --season E2025
Already have the cache and only changed the schema? Skip the network entirely:
uv run euroleague-etl --season E2025 --skip-crawl
Connect it to Claude
Claude Code
claude mcp add euroleague --env EUROLEAGUE_DB=$PWD/data/euroleague.duckdb -- $PWD/.venv/bin/python -m euroleague_open_data.mcp_server
Remote, for sharing with other people
A hosted deployment serves the same tools over HTTPS, so anyone can connect by URL with nothing installed — and it is the only way to use this from ChatGPT, which cannot run local MCP servers.
claude mcp add --transport http euroleague https://euroleague-open-data.onrender.com/mcp
It runs on a free instance that sleeps after 15 minutes idle, so the first request after a
quiet spell takes about a minute. Everything after it is fast. To host your own, see
docs/DEPLOY.md — the landing page at / fills in whatever hostname it
is served from, so a fork needs no edit.
Note that ChatGPT custom connectors require a paid plan (Plus, Pro, Business, Enterprise or Edu) with Developer mode enabled. Claude Code and Claude Desktop work on any plan.
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"euroleague": {
"command": "/absolute/path/to/euroleague-open-data/.venv/bin/python",
"args": ["-m", "euroleague_open_data.mcp_server"],
"env": {
"EUROLEAGUE_DB": "/absolute/path/to/euroleague-open-data/data/euroleague.duckdb"
}
}
}
}
Things to ask it
Who had the best true shooting percentage in EuroLeague last season, minimum 20 games?
Compare Vezenkov and Nwora on efficiency and usage.
Which team had the best defensive rating, and which Four Factor drove it?
Show me Micic's shot chart by zone.
Which games are unreliable for lineup analysis?
Who changed clubs this summer, and which of them scored well last season?
That last one matters. The warehouse tracks its own completeness, so the model can say "this game has no shot data" instead of inventing a number.
Fantasy drafting
Built for BasketNews Fantasy draft mode: private leagues of 3–12 managers, 13-player rosters, unique squads, snake or reverse-snake order.
Fantasy points are recomputed exactly from boxscores, not estimated. Every term in the modern scoring system maps onto a stored field, so a player's score here is the score the game would award.
Ranking is by value over replacement, not by average. In a draft each manager gets a unique roster, so what decides a pick is how much better a player is than the next one available at the same position — and that depends on league size. An 8-team league and a 12-team league produce different boards from the same data.
uv run python -m euroleague_open_data.fantasy --teams 8 --scoring classic
Two things about scoring are worth knowing before you trust a late-round pick:
- The "classic" formula is not published for draft mode, so it was reverse-checked against the live site. Vezenkov, E2025 round 1: PIR 27 in a win scores 29.7 there, and 27 × 1.1 = 29.7 here. The PIR base and the win multiplier are confirmed. The loss multiplier is still inferred — and measured to be immaterial: rebuilding the board with 1.0 instead of 0.9 gives rank correlation 0.9996, an unchanged top 7, and an average move of 1.4 places.
- It matters less than it looks. Rank correlation between classic and modern is 0.99 and seven of the top eight are the same players. The disagreement shows up in the middle: one player moves 94th to 61st, which in an 8×13 draft is a different round.
Tools
| Tool | Purpose |
|---|---|
search_players |
fuzzy name → canonical person_code |
search_teams |
fuzzy name → canonical team_code, handles sponsor renames |
get_player_stats |
season or career, totals / per-game / per-36, plus TS%, eFG%, usage |
get_team_stats |
ratings and Four Factors, team and opponent |
get_game_boxscore |
full game detail with completeness flags |
get_shot_chart |
zone aggregates, optionally raw x/y coordinates |
run_sql |
read-only DuckDB SELECT — the escape hatch for unanticipated questions |
get_draft_board |
draft ranking by value over replacement, sized to your league, adjusted for minute pressure, with EuroCup and NBA arrivals converted and labelled |
plan_snake_draft |
your picks in snake order, and who should survive until each |
compare_draft_candidates |
head to head for a specific pick decision |
get_player_fantasy_log |
game-by-game fantasy points, for form and role changes |
get_coach_rotation |
how deep a coach's rotation runs — the ceiling on minutes |
get_role_outlook |
minutes and production a club vacated, by position |
get_transfers |
announced 2026-27 squads: who moved, who stayed, who is unsigned |
get_squad_outlook |
a club's coming season: minutes freed, minutes claimed, who has no history |
Resources: euroleague://schema, euroleague://coverage, euroleague://data-quality.
run_sql runs on a read-only connection, permits a single SELECT/WITH, caps rows, and
cancels after 15 seconds.
Correcting what the data cannot know
The API lists who is on a roster. It never says why someone is absent, so a player who
signed in the NBA looks identical to one still negotiating — and the first should not
appear on a draft board at all. data/overrides.csv is where a
human records the difference:
player,status,note
"DIALLO, ALPHA",left_league,signed in the NBA after Monaco left the EuroLeague
uv run euroleague-overrides
Rows that match no player, or match two, raise rather than being skipped — a correction that silently fails to apply is worse than none, because it looks like it worked.
Data quality
Validation runs as part of every ETL run and writes
docs/data-quality-report.json, which is committed so
regressions show up in git log.
Three findings worth knowing about, all documented in
docs/api-notes.md:
- Shot coordinates and play-by-play begin at the 2007 season. Earlier seasons have boxscores only. This is a property of the source, not of this project.
- Period buckets and event sequence numbers disagree in roughly 40% of games.
NUMBEROFPLAYis unique and reliable; the per-quarter arrays upstream returns are not. Affected games are flaggedlineup_safe = false. - Player identifiers differ across endpoints. Boxscores use
TGB, the live feed usesPTGB. Normalisation is source-aware, and there is a regression test for it.
Coverage
Measured across all 52 seasons on 2026-08-10 (docs/coverage.json):
| Segment | Seasons | Games | Boxscore | PBP + shots |
|---|---|---|---|---|
| EuroLeague | E2000–E2006 | 1 563 | yes | no |
| EuroCup | U2002–U2006 | 896 | yes | no |
| Both | 2007–2025 | 9 059 | yes | yes |
| Total | 52 | 12 122 |
Development
uv run pytest
uv run ruff check src tests
uv run mypy src
Licence and contact
Code is MIT — see LICENSE. The licence covers the code only. It grants no rights in the underlying match data, which belongs to Euroleague Basketball and its data partners. Commercial use of the data may require a licence from them.
Takedown: if you represent a rights holder and want this changed or removed, open a
GitHub issue titled TAKEDOWN. We will respond within 7 days and will take published
datasets down on request while any disagreement is discussed. No formal legal process is
needed to get our attention.
Prior art
giasemidis/euroleague_api— Python wrapper. The shot-coordinate endpoint used here was reverse-engineered there first.FlavioLeccese92/euroleaguer— R wrapper, useful for cross-checking endpoint coverage.bsamot10/EuroleagueDataETL— existing ETL patterns for this data.vtzimpl/euroleague-api-mcp— an earlier MCP server that proxies the API directly. Given the rate limit measured here, proxying is the thing this project deliberately avoids.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。