Sync WHOOP recovery, sleep, strain, and workouts into Postgres and query them from any MCP client.
Runs on your machine from its package; the steps are below.
At most: creates or changes data: records, files, settings.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx whoop-postgres-mcp
MCP client config
{
"mcpServers": {
"whoop-mcp": {
"command": "uvx",
"args": [
"whoop-postgres-mcp"
]
}
}
}
An MCP server that syncs your WHOOP data into Postgres and lets AI agents query it.
WHOOP's public API is rate-limited, cursor-paginated, and returns one record at a time. That is fine for a sync job and terrible for an agent that wants to answer "how does my HRV this week compare to my baseline?". So this server splits the job in two:
Your data lands in a database you own, in a schema you can read with any SQL client, and nothing leaves your machine except calls to WHOOP itself.
Python 3.12+ · MIT · stdio MCP server + CLI · Postgres storage
Two write-side tools talk to the WHOOP API; everything else reads Postgres only.
| Tool | Args | What it does |
|---|---|---|
whoop_auth_url | none | Returns the OAuth authorization URL to open in a browser. |
whoop_connect | code | Exchanges the OAuth code for tokens, stores them, returns the connected user. |
whoop_sync | days=7, full=false | Pulls data from WHOOP into Postgres. Returns per-collection counts and errors. |
whoop_status | none | Per-collection sync health: last sync, watermark, row count, last error. |
whoop_overview | days=7 | Latest recovery and vitals vs. 30-day baseline, last night's sleep, recent workouts and strain. |
whoop_recovery | days=7, limit=50 | Recent recovery scores, resting HR, HRV, SpO2, skin temperature. |
whoop_sleep | days=7, limit=50 | Recent sleeps and naps with stage breakdown, need, performance, efficiency. |
whoop_workouts | days=14, limit=50 | Recent workouts with strain, HR, energy, distance, and zone minutes. |
whoop_cycles | days=7, limit=50 | Recent physiological cycles (WHOOP days) with day strain and HR. |
whoop_baseline | none | 30-day mean and standard deviation per vital. |
days is capped at 365 and limit at 200. All tools return JSON text.
uv tool install whoop-postgres-mcp # or: pipx install whoop-postgres-mcp
Or run it without installing:
uvx whoop-postgres-mcp --help
You also need a Postgres database (any recent version; 14+ is fine) and a WHOOP developer app. docs/SETUP.md walks through both.
export WHOOP_CLIENT_ID=...
export WHOOP_CLIENT_SECRET=...
export WHOOP_REDIRECT_URI=http://localhost:8765/callback
export WHOOP_DB_URL=postgresql://whoop:whoop@localhost:5432/whoop
whoop-mcp init-db # create the `whoop` schema (idempotent)
whoop-mcp auth-url # print the URL to visit; approve access in a browser
whoop-mcp connect <code> # paste the `code` from the redirect URL
whoop-mcp sync --all # first backfill; later runs: whoop-mcp sync --days 7
whoop-mcp # serve MCP over stdio
The same flow is available as MCP tools (whoop_auth_url, whoop_connect,
whoop_sync) so an agent can drive the whole setup.
All configuration is by environment variable. The server validates every variable at startup and exits with a message naming each missing one.
| Variable | Required | Meaning |
|---|---|---|
WHOOP_CLIENT_ID | yes | Client ID of your WHOOP developer app. |
WHOOP_CLIENT_SECRET | yes | Client secret of your WHOOP developer app. |
WHOOP_REDIRECT_URI | yes | Redirect URI, exactly as registered on the app. Any URL works; the server does not listen on it. You copy the code from the address bar. |
WHOOP_DB_URL | yes | Postgres DSN, e.g. postgresql://user:pass@host:5432/db. Tokens and data live here. |
Claude Code:
claude mcp add whoop -e WHOOP_CLIENT_ID=... -e WHOOP_CLIENT_SECRET=... \
-e WHOOP_REDIRECT_URI=http://localhost:8765/callback \
-e WHOOP_DB_URL=postgresql://whoop:whoop@localhost:5432/whoop \
-- uvx whoop-postgres-mcp
Claude Desktop (claude_desktop_config.json) or any client that takes a
stdio command:
{
"mcpServers": {
"whoop": {
"command": "uvx",
"args": ["whoop-postgres-mcp"],
"env": {
"WHOOP_CLIENT_ID": "...",
"WHOOP_CLIENT_SECRET": "...",
"WHOOP_REDIRECT_URI": "http://localhost:8765/callback",
"WHOOP_DB_URL": "postgresql://whoop:whoop@localhost:5432/whoop"
}
}
}
}
WHOOP's collection endpoints filter on when a record occurred, not when it
was last modified, and records change after creation (a sleep is created as
PENDING_SCORE and scored later). The sync therefore:
days days, which catches late re-scores;newest_updated_at in
whoop_sync_state). If the last sync is older than days, the window is
stretched back to the watermark minus a two-day lookback so a gap never
leaves a hole;id, or cycle_id for recovery),
so overlapping windows are harmless and re-running is safe;whoop_sync_state and carries on with the
other collections rather than aborting the run.full=true (or whoop-mcp sync --all) drops the lower bound and walks the
account's entire history. Body measurements are a single current record and are
refreshed on every sync.
Token refresh is proactive (five minutes before expiry) and serialized through
a Postgres advisory lock, so the MCP server and a cron-driven whoop-mcp sync
can share one token row without racing.
The full data model is documented in docs/SCHEMA.md.
whoop.whoop_tokens. Anyone who can read that table can call the WHOOP API
as you until the refresh token is revoked. Restrict database grants
accordingly and treat WHOOP_DB_URL as a secret.whoop_connect and whoop_sync
talk to WHOOP. Everything else is a SQL query against your database.read:* scopes plus offline for
refresh tokens. It cannot modify anything in your WHOOP account.whoop_tokens and remove the app from
your WHOOP account settings.git clone https://github.com/cunicopia-dev/whoop-mcp
cd whoop-mcp
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
ruff check .
mypy src
pytest # pure-logic tests
WHOOP_TEST_DB_URL=postgresql://whoop:whoop@localhost:5432/whoop pytest # + live DB tests
The live tests apply the schema and truncate every table in the target database before each test. Point them at a scratch database.
A throwaway Postgres for local testing:
docker run -d --rm --name whoop-pg -e POSTGRES_USER=whoop -e POSTGRES_PASSWORD=whoop \
-e POSTGRES_DB=whoop -p 5432:5432 postgres:16-alpine
src/whoop_mcp/
config.py environment variables, validated at startup
auth.py OAuth2 flow, token persistence, locked refresh
client.py httpx client: pagination, backoff, Retry-After
schema.sql the Postgres DDL (applied by `whoop-mcp init-db`)
db.py connection + schema helpers
sync.py incremental sync with per-collection watermarks
queries.py read-side SQL behind the MCP tools
server.py MCP server over stdio + CLI entry point
docs/
SETUP.md WHOOP app registration, OAuth walkthrough, DB init, first sync
SCHEMA.md column-by-column data model
tests/ config, schema, client, auth, sync, and stdio protocol tests
MIT. See LICENSE.