A Model Context Protocol server that exposes the 2025-26 NBA daily leaders dataset (~28k player-game rows, all 30 teams) to MCP-compatible AI clients — Claude Desktop, Cursor, MCP Inspector, and any custom MCP host.
Built with the official Python MCP SDK (mcp[cli] / FastMCP), asyncpg for a
pooled async Postgres connection, and Docker Compose for one-command setup.
docker compose up --buildThat brings up:
- Postgres 16 on
localhost:5432— schema + ~28k rows loaded automatically on first start - MCP server on
http://localhost:8000/mcp(Streamable HTTP transport)
npx @modelcontextprotocol/inspectorOpen the URL it prints, point it at http://localhost:8000/mcp, and call any
tool interactively.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json
(or the equivalent path on your OS):
{
"mcpServers": {
"nba": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/mcp-nba-server", "run", "python", "-m", "src.server"],
"env": {
"DATABASE_URL": "postgresql://mcp:mcp_dev_password@localhost:5432/nba",
"MCP_TRANSPORT": "stdio"
}
}
}
}Restart Claude Desktop. Try asking:
- "Who's been the best scorer in the last two weeks?"
- "Show me Luka Dončić's last 10 games."
- "What were the top 5 single-game performances by Game Score this season?"
- "Compare Shai Gilgeous-Alexander and Jokić head-to-head."
| Tool | Purpose |
|---|---|
top_scorers(start_date, end_date, limit) |
PPG leaders in a window (3+ games min) |
player_game_log(player, limit) |
Recent games for a player |
search_players(query) |
Fuzzy player-name lookup |
player_season_averages(player) |
Full per-game averages for a player |
top_performances(metric, limit, min_minutes) |
Best single-game performances by pts, trb, ast, stl, blk, game_score, or plus_minus |
team_leaderboard(team) |
Wins/losses and team PPG |
head_to_head(player_a, player_b) |
Side-by-side season averages |
schema://player_games— column-by-column schema documentationdataset://summary— row count, date coverage, top-line names
Claude Desktop ──(MCP / stdio or HTTP)──> FastMCP server ──(asyncpg)──> PostgreSQL
│ pool, lifespan-managed │ seeded from CSV
└─ tools, resources, prompts └─ via COPY + transform
- No
execute_sqltool. Every tool wraps a specific intent with parameterized queries — this is the difference between "exposing a database to an AI" and "vending it safely." - Whitelisted dynamic SQL. The one place a column name is interpolated
(
top_performances) validatesmetricagainst an explicit set first. - Pooled connections.
asyncpg.create_poolopens once in the FastMCP lifespan hook and is shared across tool calls. - Two transports, one binary.
MCP_TRANSPORT=stdiofor Claude Desktop,streamable-httpfor the deployed Docker service. - Staging-then-transform load. The CSV ships with awkward column names
(
FG%,+/-,MPasMM:SS). A staging table mirrors the CSV exactly, then a singleINSERT … SELECTcleans it into the query-friendly final table.
mcp-nba-server/
├── docker-compose.yml
├── Dockerfile
├── pyproject.toml
├── README.md
├── init-db/
│ ├── 01_schema.sql
│ ├── 02_load.sql
│ └── nba_dailyleaders_full.csv # ~2.5MB, 27,818 rows
└── src/
├── __init__.py
└── server.py
- OAuth 2.1 in front of the HTTP transport via
fastmcp.auth - Structured logging (
structlog) and a Prometheus/metricsendpoint - GitHub Actions CI: ruff + mypy + pytest against an ephemeral Postgres service
- Deploy to Fly.io / Railway and put the live URL in this README