Skip to main content
Glama

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


Related MCP server: WHOOP MCP

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.

Available Tools

10 tools
whoop_auth_urlA

Start connecting a WHOOP account: returns the OAuth authorization URL for the user to open in a browser. After approving, the browser lands on the redirect URI with a code parameter to pass to whoop_connect.

Inputs: no named parameters.

whoop_baselineA

30-day mean and standard deviation per vital: recovery score, resting HR, HRV, SpO2, skin temperature, sleep hours, sleep performance, day strain. Reads the local database only; run whoop_sync first to refresh.

Inputs: no named parameters.

whoop_connectA

Finish connecting: exchange the OAuth code from the redirect URL for tokens, store them in Postgres, and return the connected user's summary.

Inputs: code (required).

whoop_cyclesA

Recent physiological cycles (WHOOP days): day strain, energy, average and max heart rate, newest first. Reads the local database only; run whoop_sync first to refresh.

Inputs: days, limit.

whoop_overviewA

One-call snapshot: latest recovery and vitals compared with the 30-day baseline (delta and z-score), last night's sleep, and recent workouts and daily strain. Reads the local database only; run whoop_sync first to refresh.

Inputs: days.

whoop_recoveryA

Recent recovery records: score (0-100), resting HR, HRV, SpO2, skin temperature, newest first. Reads the local database only; run whoop_sync first to refresh.

Inputs: days, limit.

whoop_sleepA

Recent sleeps including naps: stage durations, sleep need, respiratory rate, performance and efficiency, newest first. Reads the local database only; run whoop_sync first to refresh.

Inputs: days, limit.

whoop_statusA

Sync health: connected user, and per collection the last sync time, watermark, row count, and last error. Reads the local database only; run whoop_sync first to refresh.

Inputs: no named parameters.

whoop_syncA

Pull data from the WHOOP API into Postgres. Incremental by default (last days days plus anything since the last sync); set full=true to backfill the account's entire history. Returns per-collection counts and errors.

Inputs: days, full.

whoop_workoutsA

Recent workouts: sport, duration, strain, heart rate, energy, distance, and minutes per heart-rate zone, newest first. Reads the local database only; run whoop_sync first to refresh.

Inputs: days, limit.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 10 tools

    Disambiguation4/5

    Each tool targets a distinct data type (recovery, sleep, workouts, cycles) or action (auth, connect, sync, status), so boundaries are mostly clear. whoop_overview aggregates recovery/sleep/workouts/strain and thus overlaps with the individual read tools and whoop_baseline, but its 'one-call snapshot' description mostly disambiguates it.

    Naming Consistency5/5

    Every tool uses the same whoop_<noun> snake_case prefix with no mixing of conventions. The pattern is predictable and readable throughout.

    Tool Count5/5

    Ten tools is a well-scoped set: two auth tools, one sync, one status, one baseline, and six read tools. Each earns its place with no obvious redundancy.

    Completeness4/5

    The surface covers the full lifecycle: OAuth connect, sync, sync health, and reads across recovery, sleep, workouts, cycles, baseline, and overview. Minor gaps like token revocation/disconnect or delete operations are absent but not blocking for the read-oriented purpose.

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables AI assistants to query a user's WHOOP recovery, sleep, HRV, strain, workouts and body measurements through 16 read-only tools, including today's status, personal baselines, sleep debt, trends and period comparisons. Runs locally, requires no data exports, and supports aggregate mode that limits responses to high-level summaries.
      17 npm
      -
    • A
      license
      Not graded
      quality
      A
      maintenance
      A read-only MCP server that gives AI assistants like Claude, Codex, and GitHub Copilot access to your WHOOP recovery, sleep, HRV, strain, and workout data. It enables natural-language queries about daily readiness, personal baselines, trends across periods, sleep debt, and HYROX or triathlon performance, all without manually exporting data.
      17 npm
      MIT