Skip to main content

whoop-mcp

Python License: MIT storage: 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:

  • 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

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

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.

Quickstart

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.

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:

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"
      }
    }
  }
}

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.

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

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

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

whoop_postgres_mcp-0.1.0.tar.gz (101.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

whoop_postgres_mcp-0.1.0-py3-none-any.whl (27.6 kB view details)

Uploaded Python 3

File details

Details for the file whoop_postgres_mcp-0.1.0.tar.gz.

File metadata

  • Download URL: whoop_postgres_mcp-0.1.0.tar.gz
  • Upload date:
  • Size: 101.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for whoop_postgres_mcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 31c986b034276c29d6c44f3a5bbadbf96c08670ea8bf824ed3568f6c9b5e72af
MD5 cb8b7a8d808c0f59f0ab01eff2c63044
BLAKE2b-256 a931a1624fa27b1229699f8d333cc4a6d8719d78ac30f0bbca19ea991f7601e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for whoop_postgres_mcp-0.1.0.tar.gz:

Publisher: publish.yml on cunicopia-dev/whoop-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whoop_postgres_mcp-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for whoop_postgres_mcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e09bb4bf149829efa137f2446762bd0561f65e937e67a3d43b628410b7f07c3f
MD5 4bb9d4c9fffbc29c0581f61cc1c2ef5e
BLAKE2b-256 7ab2d198d9bd5ed2c77b2febf6ed1a82c54c6e4d81fef84436a35b45757f9f57

See more details on using hashes here.

Provenance

The following attestation bundles were made for whoop_postgres_mcp-0.1.0-py3-none-any.whl:

Publisher: publish.yml on cunicopia-dev/whoop-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page