# whoop-mcp ![Python](https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![storage: Postgres](https://img.shields.io/badge/storage-Postgres-4169E1?logo=postgresql&logoColor=white) ![MCP](https://img.shields.io/badge/MCP-ready-FF6F00) **An [MCP](https://modelcontextprotocol.io) 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: * a **sync** path that pulls cycles, recovery, sleep, workouts, and body measurements from the WHOOP API into a documented Postgres schema, keeping the raw JSON of every record alongside the typed columns; * a **query** path of MCP tools that read Postgres only. They are fast, never hit WHOOP's rate limits, and keep working offline once data is synced. 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 --- ## Contents - [Tools](#tools) - [Install](#install) - [Quickstart](#quickstart) - [Configuration](#configuration) - [Register with an MCP client](#register-with-an-mcp-client) - [How sync works](#how-sync-works) - [Security model](#security-model) - [Development](#development) - [Project layout](#project-layout) - [License](#license) --- ## Tools 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. ## Install ```bash uv tool install whoop-postgres-mcp # or: pipx install whoop-postgres-mcp ``` Or run it without installing: ```bash 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](docs/SETUP.md) walks through both. ## Quickstart ```bash 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 # 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. ## Configuration 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. | ## Register with an MCP client Claude Code: ```bash 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: ```json { "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" } } } } ``` ## How sync works 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: 1. always re-fetches the last `days` days, which catches late re-scores; 2. tracks a per-collection watermark (`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; 3. upserts every record on its natural key (`id`, or `cycle_id` for recovery), so overlapping windows are harmless and re-running is safe; 4. records per-collection errors in `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](docs/SCHEMA.md). ## Security model * **Database access is token access.** OAuth tokens are stored in plaintext in `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. * **Read tools never reach the network.** Only `whoop_connect` and `whoop_sync` talk to WHOOP. Everything else is a SQL query against your database. * **Nothing is written outside Postgres.** No files, no caches, no telemetry. * **Scopes are read-only.** The app requests `read:*` scopes plus `offline` for refresh tokens. It cannot modify anything in your WHOOP account. * To revoke access, delete the row from `whoop_tokens` and remove the app from your WHOOP account settings. ## Development ```bash 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: ```bash 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 ``` ## Project layout ``` 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 ``` ## License MIT. See [LICENSE](LICENSE).