sleeper-mcp
by pixelwix
README.md
# sleeper-mcp
Connects Claude to a Sleeper fantasy football account two ways:
1. **MCP tools** — ask Claude directly ("who should I start this week") and
it pulls live Sleeper data via the tools below.
2. **Daily report dashboard** — a site with matchup summary, a full recommended lineup (every starting slot,
not just close calls), win probability, waiver targets, and trade
suggestions, regenerated every morning (and again Sunday 11am). Also
has a **live** matchup card (your lineup vs. theirs, live score, injury
badges — refreshed straight from Sleeper, not the daily report) and a
live standings card.
3. **Gameday alerts** — a background watcher pushes a phone notification
(via ntfy.sh) the moment one of your starters' injury status changes.
4. **Private owner settings** — the existing owner profile is the only account
that can sign in and open the health/configuration panel.
Sleeper's API is public and read-only, so this only ever *reads* league data;
it can't submit waiver claims, propose trades, or set your lineup for you.
Those actions still happen in the Sleeper app — this is for the research and
reasoning that inform them.
## Configured leagues
Two league slots (keys `butt-punt`/`no-fun` — rename in `src/config.js` if you
like) set entirely via env vars: `SLEEPER_USERNAME`, `SLEEPER_USER_ID`,
`SLEEPER_LEAGUE_BUTT_PUNT`, `SLEEPER_LEAGUE_NO_FUN`. See `.env.example`. This
The dashboard is intentionally single-owner and reads only these configured leagues.
## Tools
- `list_leagues` — both leagues with current standings
- `get_my_roster` — your starters/bench/taxi/IR + record
- `get_all_rosters` — every team's roster, for scouting trade targets
- `get_matchup` — your starters vs. your opponent's for a given week (defaults to current)
- `get_waiver_wire` — available free agents, ranked, filterable by position
- `get_trending_players` — sitewide add/drop trends (not league-specific)
- `get_transactions` — recent waiver/trade activity in a league
- `get_player_intelligence` — cached, multi-source player news, stats, outlook,
confidence, source conflicts, freshness, and direct source links
## Setup — local (stdio, this Mac only)
```
npm install
```
Register as a user-scoped MCP server (adjust the path to wherever you cloned this):
```
claude mcp add --scope user sleeper -- node /path/to/sleeper/src/index.js
```
Just ask Claude things like "who should I start this week in the Butt Punt
League" or "any good waiver pickups at RB" — no need to invoke tools by name.
The MCP server is model-neutral. Ready-to-edit client examples live in
`examples/mcp/` for Claude and Gemini. ChatGPT/OpenAI clients use the hosted
Streamable HTTP endpoint; see `examples/mcp/chatgpt.md`. The hosted endpoint
requires a Bearer token. If the ChatGPT custom-connector UI requires OAuth,
place an OAuth-capable gateway in front of `/mcp` rather than removing auth.
## Setup — hosted (HTTP, behind Traefik)
`src/http.js` runs the same tools over the MCP Streamable HTTP transport
instead of stdio, so other devices (e.g. Claude Desktop on your phone) can
reach it too. `docker-compose.yml` has a Traefik label setup assuming
Docker-label discovery on a `proxy` network with a `myresolver` cert
resolver — adjust to your own reverse-proxy setup, or drop the `labels:`
block entirely and just publish a port if you don't use Traefik. Set
`SLEEPER_DOMAIN` in `.env` to your own hostname.
Requires `SLEEPER_MCP_TOKEN` in `.env` (the server refuses to start without
it) — every request to `/mcp` must send `Authorization: Bearer <token>`.
`.env` is gitignored; generate a token with:
```
node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"
```
To deploy/update, sync everything except local state to wherever Docker runs and rebuild:
```
rsync -av --delete --exclude .git --exclude node_modules --exclude data \
./ your-docker-host:/path/to/sleeper/
ssh your-docker-host "cd /path/to/sleeper && docker compose up -d --build"
```
To add it as a remote MCP server elsewhere:
```
claude mcp add --transport http sleeper https://your-domain/mcp \
--header "Authorization: Bearer <token>"
```
## Daily report
`automation/generate-report.mjs` runs Claude Code headless (`claude -p`)
once a day with a tightly scoped tool surface — only the read-only sleeper
MCP tools plus WebSearch (for real injury/matchup context), no Bash/Write/
Edit — and `--json-schema` to force valid structured output matching
`automation/report-schema.json`. This runs under the Claude Pro plan, not
the pay-per-token API, so there's no separate billing (a report for both
leagues costs roughly $1–1.50 in notional usage against Pro's limits, not
an actual charge).
On the Docker host, `automation/run-daily-onhost.sh` prefers the
subscription-backed Antigravity CLI when it is installed and authenticated.
It runs from an isolated workspace, uses only the read-only Sleeper MCP server
plus web research, and returns JSON constrained by
`automation/report-schema.json`. If Antigravity fails or returns an incomplete
report, the runner tries the Gemini Interactions API when `GEMINI_API_KEY` is
set, then Claude. The existing report is preserved if every provider fails.
`AGY_MODEL` and `GEMINI_MODEL` both default to `gemini-3.6-flash`.
Scheduled on the **docker host** via cron (`automation/setup-host-cron.sh`):
daily at 8:00 AM local time, plus a second Sunday 11:00 AM run to catch
late-week inactive/injury news before the early games lock. Pregame kickoff
refreshes and the injury watcher run on the same host every 10 minutes. Logs
land in `automation/logs/` on the host.
The Mac-side `run-daily.sh` + `deploy-report.sh` rsync path is legacy — only
useful if you intentionally generate reports locally and push them up. Normal
operation writes straight into the container's bind-mounted `data/` on the
host, so a new report does not need a redeploy or rsync. To run it by hand on
the host:
```
ssh docker-host "cd /mnt/docker/sleeper && ./automation/run-daily-onhost.sh"
```
To install or refresh host cron after pulling changes:
```
ssh docker-host "cd /mnt/docker/sleeper && ./automation/setup-host-cron.sh"
```
## Cross-checked matchup data
Sleeper has no projections, no matchup difficulty, and no Vegas lines — the
daily report, injury advisories, and player popups all lean on outside
sources to fill that in, combining two kinds:
- **Real computed numbers**, fetched and handed to the model as ground
truth rather than left to search results:
- `src/defenseMatchups.js` — defense-vs-position matchup difficulty
(PPR points allowed per game, trailing 6 weeks) computed from
[nflverse](https://github.com/nflverse/nflverse-data)'s free, public,
play-by-play-derived weekly player stats. Cached to
`data/defense-matchups.json` for ~20h (the source only meaningfully
updates weekly).
- `src/vegasLines.js` — spread + over/under from ESPN's public odds API,
converted to implied team point totals (scoring environment / game
script), per game for the upcoming week.
- **Web research, explicitly cross-checked across outlets** — the daily
report prompt (`automation/prompt.md`), injury advisor
(`automation/injury-advisor-prompt.md`), and player-info batches
(`automation/player-info-batch-prompt.md`) all instruct Claude to check
more than one source (ESPN, FantasyPros, Rotoworld/RotoWire, Yahoo,
NFL.com, beat reporters) for projections/rankings/injury news, and to
flag it when sources disagree rather than silently picking one. Player-info
cards preserve 2-5 direct source URLs, an `as_of` timestamp, and an explicit
confidence rating so you can inspect the evidence yourself.
Both data sources are free and unauthenticated — no API keys, no scraping
of subscription content. Yahoo's fantasy data isn't pulled directly since
their public API is OAuth-per-user and scoped to league data, not
league-agnostic projections/consensus; it's covered the same way ESPN/
FantasyPros/Rotoworld are, via web research. The automation never bypasses
paywalls or copies subscription-only content.
## Live data (no LLM, no daily-report lag)
`/api/matchup?league=<key>` and `/api/standings?league=<key>` hit Sleeper
directly on every request via `getMatchupView()`/`getStandingsView()` in
`src/sleeperApi.js` — same functions the MCP tools use. The dashboard's
Live Matchup and Standings cards poll these every 90s while the tab is
visible. Unauthenticated, same trust level as `/api/report` and the
dashboard itself — only `/mcp` (tool execution) requires the bearer token.
## Installable PWA and push notifications
The dashboard is an installable Progressive Web App with a manifest, offline
app shell, install icons, and a service worker. Use the **Install** control in
supported desktop/Android browsers. On iPhone/iPad, open the site in Safari,
choose **Share → Add to Home Screen**, then open the installed app.
Tap **Enable alerts** from the dashboard to create a Web Push subscription.
Installed devices receive the same injury/status and urgent lineup-lock alerts
as ntfy, plus a notification when a kickoff-aware pregame report finishes.
iOS Web Push requires the Home Screen-installed app. Subscriptions are stored
in `data/push-subscriptions.json` with restricted file permissions; VAPID keys
stay in the host `.env` and are never sent to the browser except for the public
application key.
The dashboard header uses a bell icon for device notification status and a
gear icon for the authenticated system control panel. The control panel keeps
the dashboard's existing visual language and provides three views:
- **Configuration** — an allowlisted set of Sleeper, model, alert, and domain
settings. Secret fields are write-only and never returned by the API.
- **System Health** — live Sleeper latency, MCP uptime, report age, watcher and
pregame-scheduler state, provider availability, and push-device count.
- **Notifications** — enable or disable Web Push on the current device.
Only `SETTINGS_ADMIN_USER_ID` may access these APIs. Saves are validated,
backed up under `data/env-backups/`, written to the host `.env`, and applied by
a locked host job that recreates the service within one minute.
## Gameday alerts
`automation/watch-starters.mjs` polls your current starters' `injury_status`
in both leagues and diffs against `data/starter-status.json`. On a real
change (to/from Questionable/Doubtful/Out/IR/PUP/Suspended), it pushes a
notification via [ntfy.sh](https://ntfy.sh) to the topic in `.env`'s
`NTFY_TOPIC` — subscribe to that topic in the ntfy app (iOS/Android) or at
`https://ntfy.sh/<topic>` to receive them. Treat the topic name like a
shared secret — ntfy.sh topics are unauthenticated by default, so anyone
who knows it can read or publish to it.
Two additional checks run alongside the basic status diff:
- **Waiver/bench advisory** — for the more serious statuses (Doubtful/Out/
IR/Suspended, not the very common/often-inconsequential Questionable),
`automation/injuryAdvisor.mjs` runs the same locked-down headless-Claude-
Code pattern as the daily report (only `get_waiver_wire`/`get_my_roster` +
WebSearch, no Bash/Write/Edit) to confirm the news, check the waiver wire
for a same-position replacement, and check the bench for a better play —
folded into the same notification.
- **Gametime lock check** — `src/nflSchedule.js` pulls real kickoff times
from ESPN's public scoreboard API (Sleeper has no schedule endpoint of its
own). If a starter is marked `Out` and their game kicks off within 4
hours, it sends an urgent separate "bench them now" alert with the same
waiver/bench advisory attached. Deduplicated per player per game via
`data/gametime-alerts.json` so it fires once, not every 10 minutes.
General lineup optimization (is a healthy bench player just a better play
this week) is handled by the daily report plus kickoff-aware pregame refreshes,
not this watcher. `automation/pregame-refresh.mjs` reads actual regular-season
kickoff times and triggers a full report/player-card refresh when one of your
active roster players is 55-95 minutes from kickoff. It does not assume a weekday, so
Thursday night, Monday night, Saturday, international, holiday, and rescheduled
games are covered. Completed kickoff windows are deduplicated and a host lock
prevents overlapping report jobs.
Scheduled on the **docker host** via the same cron block as the daily report
(`automation/setup-host-cron.sh`), every 10 minutes, year-round — Sleeper's
API is free and this is a cheap read, so there's no real cost to running it
in the off-season too. The first run after a reset just seeds a baseline
without alerting (avoids a flood of "changes" for every player's pre-existing
status). Run by hand on the host:
```
ssh docker-host "cd /mnt/docker/sleeper && ./automation/with-host-env.sh node --env-file=.env automation/watch-starters.mjs"
```
## Player detail popup
Clicking any player name on the dashboard (lineup, waiver wire, trades, live
matchup) opens a popup with a news/stats/outlook summary. This is a pure
cache lookup at request time — `GET /api/player-info?name=<name>` in
`src/http.js` reads `data/player-info-cache.json` and returns a 404 if the
player isn't in it. No LLM call happens in the container, since the
container has no Claude Code CLI or credentials.
The cache itself is built on the docker host alongside the daily report:
`automation/generate-player-info.mjs` collects your complete rosters, both
weekly opponents' starters and benches, plus the current report's waiver
targets. It batches them (6 at a time) through Gemini/Antigravity with Claude
as fallback and web research enabled,
and writes the results keyed by a normalized player name. Entries older than
7 days that no longer correspond to a rostered/targeted player are dropped
on each run. `automation/run-daily-onhost.sh` runs it right after the report
generator; a failed batch is non-fatal and just leaves that player's prior
cache entry in place.
Only covers players the daily pipeline actually cares about (your rosters,
current opponents, and that day's waiver targets) — not the whole league,
since generating profiles for every free agent isn't worth the cost.
## Private owner access
- `src/db.js` keeps the existing owner credential and signed sessions in
`data/app.db`; public account creation is disabled.
- `/settings.html` is owner sign-in/profile only.
- The gear on the main dashboard opens configuration and system health only
when the signed-in user matches `SETTINGS_ADMIN_USER_ID`.
- Sleeper identity and league IDs come from `.env`; there is no per-user
onboarding, league storage, or report generation path.
## Known limitation
Sleeper's public player list is occasionally stale for deep veterans (a
handful of long-retired players are still marked as active on a team). The
waiver wire tool filters on Sleeper's own fantasy relevance ranking to push
most of that noise out, but it isn't a second, independently-verified data
source, so treat any surprising deep-bench name with suspicion.
## Player data cache
`data/players_cache.json` caches Sleeper's full player list (~2000 players)
for 24 hours, per Sleeper's API guidance not to poll that endpoint more
often than that. Gitignored — regenerates automatically on first use each day.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing