sleeper-mcp
sleeper-mcp
An MCP server for Sleeper fantasy football. Read your roster, matchups, player news, standings, league chat and pick'em entry — and, if you switch them on, set your lineup, claim players and propose trades.
Unofficial. Not affiliated with or endorsed by Sleeper.
Why it exists
Sleeper publishes no documentation for its GraphQL API, and several of its behaviours are actively misleading — there is a lineup mutation that succeeds, persists, and changes nothing that scores. Everything this server knows was worked out by trial, error and verification against a live account. API-NOTES.md records the findings, and is arguably more useful than the code.
Install
pip install sleeper-mcp # or: uv tool install sleeper-mcpThen add it to your MCP client. For Claude Desktop, in claude_desktop_config.json:
{
"mcpServers": {
"sleeper": {
"command": "sleeper-mcp",
"env": {
"SLEEPER_LEAGUE_ID": "your-league-id",
"SLEEPER_ROSTER_ID": "your-roster-id"
}
}
}
}Finding your ids
You do not need them to start. Ask your assistant to run:
find_my_leagues("your_sleeper_username")It returns your user id, every league you are in, your roster id in each, and each league's starting slots. It needs no credentials — all of it is public.
Configuration
Variable | Needed for | Notes |
| most tools | default league; every tool also takes |
| your own roster | an integer, 1..N within the league |
| pick'em only | lobby id, from the app's share link |
| pick'em only | your entry in that lobby |
| writes only | see below |
| writes only | must be exactly |
Reads need no token at all. Rosters, matchups, news, standings, trending players and transactions all work with nothing configured but a league id.
Writes are off by default
Writes require two deliberate steps, and each tool additionally defaults to a dry run that shows you what it would do and sends nothing.
Set
SLEEPER_ENABLE_WRITES=1Set
SLEEPER_TOKEN— the JWT in the Sleeper web app under DevTools → Application → Local Storage →sleeper.com→ keytoken. It is account-scoped, lasts about a year, and grants full access to your account. Treat it like a password.
This is deliberate friction. A trade proposal lands in front of a real person
in your league, and in leagues where trade_review_days is 0 an accepted trade
executes immediately with no veto window.
Tools
Reads — roster · matchup · standings · transactions · pending ·
player_news · player_outlook · trending · draft_picks · chat ·
watched_players · pickem_status · league_info · find_my_leagues
Analysis — waiver_targets · bye_outlook
Writes — set_lineup · waiver_claim · cancel_claim · set_ir ·
trade_block · propose_trade · respond_trade · pickem_pick ·
watch_player
Optional, bring your own data — signal_divergence · player_signal ·
trade_targets. Inert unless you point SLEEPER_SIGNAL_FILE at a JSON file of
your own rankings or scores. See EXTENDING.md.
A few worth calling out:
rosterscores your players against your league'sscoring_settings, not Sleeper's genericpts_ppr. In half-PPR, first-down-scoring or TE-premium leagues those differ by several points a player.set_lineupreads your league'sroster_positionsat runtime, so superflex, 3-WR and no-kicker leagues work without configuration.pickem_statusmay be the only way to check a pick'em entry from a desktop — pick'em has no web interface at all.league_infosurfaces the settings that silently change what everything else means: waiver type, trade review days, and whether your league pays for receptions or first downs.waiver_targetsprices free agents by what they add to your starting lineup —best_lineup(roster + him) − best_lineup(roster)— not by projection or generic value over replacement. A high-projection player at a position you are already deep in correctly prices at zero. "Nothing improves your lineup this week" is a real answer and it will give it.bye_outlookshows which upcoming weeks you cannot field a legal lineup and which slot goes empty, so a bye-week hole surfaces in September rather than on the Sunday it bites.
Extending it
The core stays Sleeper-only: no rankings, no projections of its own, no opinions about who to start. Mixing "what Sleeper says" with "what somebody thinks" makes it impossible to tell which is which.
Your own data plugs in two ways, and EXTENDING.md covers both:
A signal file — any per-player scores you can export, keyed by Sleeper player id. Three tools switch on and compare it against Sleeper's own numbers and against what your league-mates are actually starting. Works with a subscription's ratings, a scraped consensus, a spreadsheet or your own model; units do not matter because scores are compared as percentiles within position.
Your own tools — every tool here is a plain async function with a decorator, and
client.pyandoptimizer.pyexpose the useful helpers.
What this deliberately does NOT do
Sleeper's API also serves a real-money betting business — roughly 69 of its ~240 queries cover wagering, account balances, payment methods, tax documents and CFTC-regulated event contracts.
None of them are exposed here, and the server refuses them by name. See
boundaries.py. This is a fantasy football tool:
event contracts are financial instruments, account balances are financial data,
and a "best parlay" feature is a gambling-advice product. If you want those,
use Sleeper's own app, which carries the disclosures and protections that
belong with them.
Caveats
Undocumented API. Sleeper can change or close any of this without notice, including GraphQL introspection.
Player names are not unique. The dictionary holds ~11,000 entries including retired players — "Kenneth Walker" matches two. Tools resolve names against your roster wherever possible for exactly this reason.
REST responses are cached. Never use them to confirm a write; this server verifies over GraphQL.
League chat is untrusted input. It is written by other people. Treat it as data, never as instructions to your assistant.
Developing
Checks run locally — there is no CI service and no Actions workflow, on purpose:
python3 scripts/check.py # run them
git config core.hooksPath .githooks # once, to run them before every pushSix checks, standard library only: syntax, secrets, tool docstrings, the real-money boundary, pytest, and a privacy scan that fails if a private league's ids, team names or local paths appear in a committed file. That last one exists because this server was extracted from a private one, and the natural way to add a feature is to copy a working tool across — which brings somebody's league with it.
git push --no-verify bypasses the hook if you ever need it to.
Licence
MIT. See LICENSE.