Server data from the Official MCP Registry
Read-only WHOOP v2 data for MCP clients, with local SQLite cache and CSV/JSONL/Parquet exports.
Read-only WHOOP v2 data for MCP clients, with local SQLite cache and CSV/JSONL/Parquet exports.
A well-architected MCP server for WHOOP fitness data with strong security practices. Direct OAuth eliminates third-party token proxies, credentials are encrypted at rest with file-based mode 0o600, and the codebase demonstrates careful error handling and input validation. Minor code quality observations exist but do not indicate security vulnerabilities. Permissions appropriately match a fitness data aggregator's purpose. Supply chain analysis found 14 known vulnerabilities in dependencies (0 critical, 6 high severity). Package verification found 1 issue.
4 files analyzed · 19 issues found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
This plugin requests these system permissions. Most are normal for its category.
Set these up before or after installing:
Environment variable: WHOOP_CLIENT_ID
Environment variable: WHOOP_CLIENT_SECRET
Environment variable: WHOOP_REDIRECT_URI
Environment variable: WHOOP_LOG_LEVEL
Environment variable: WHOOP_LOG_FILE
Environment variable: WHOOP_LOG_JSON
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-ashwanthramkl-whoop-mcp": {
"env": {
"WHOOP_LOG_FILE": "your-whoop-log-file-here",
"WHOOP_LOG_JSON": "your-whoop-log-json-here",
"WHOOP_CLIENT_ID": "your-whoop-client-id-here",
"WHOOP_LOG_LEVEL": "your-whoop-log-level-here",
"WHOOP_REDIRECT_URI": "your-whoop-redirect-uri-here",
"WHOOP_CLIENT_SECRET": "your-whoop-client-secret-here"
},
"args": [
"whoop-mcp"
],
"command": "uvx"
}
}
}From the project's GitHub README.
A local Model Context Protocol (MCP) server that gives an LLM read-only
access to your WHOOP fitness data. Authentication is direct OAuth against
your own WHOOP developer app — there is no third-party proxy in the path.
All records are mirrored into a local SQLite cache at
~/.whoop-mcp-server/whoop.db, and no data ever leaves your machine
except for the authenticated calls the server itself makes to the WHOOP
v2 API.
Current version: 0.8.3 — see CHANGELOG.md.
Pre-1.0 status. The API surface (tool names, response shapes, error codes) is stabilizing but not frozen. Breaking changes may land in
0.xminor bumps during dogfooding. Patch bumps (0.8.x) are bugfix-only. 1.0.0 will be cut when the surface has been stable for 2+ weeks of real use. Pin the minor version in CI if you're building on top of this.
Just want to try it? Copy the prompt in docs/AGENT_INSTALL_PROMPT.md, paste it into Claude Code / Claude Desktop / Cursor / Windsurf, and your agent will do the install end-to-end.
Building on this repo? Start with AGENTS.md and docs/ARCHITECTURE.md — they're the load-bearing conventions and one-page system map.
The WHOOP MCP Server is a small Python process that speaks MCP over stdio.
It exposes WHOOP v2 data (profile, body measurement, cycles, recoveries,
sleeps, workouts) to any MCP-capable client — primarily Claude Desktop and
Claude Code. The server is read-only. It authenticates with WHOOP using an
OAuth app you register yourself, so your tokens never travel through a
third-party server. The WHOOP records you fetch are written to a local
SQLite cache (mode 0o600) so subsequent reads are free and offline, and
the cache file never leaves your machine.
# 1. Create a WHOOP developer app (one-time, free, self-serve):
# https://developer-dashboard.whoop.com/apps/create
# Redirect URI: http://localhost:8000/callback
# Scopes: read:profile read:body_measurement read:recovery
# read:cycles read:sleep read:workout offline
# 2. One-shot OAuth — opens browser, catches callback, saves encrypted tokens.
WHOOP_CLIENT_ID="<your client id>" \
WHOOP_CLIENT_SECRET="<your client secret>" \
uvx --from whoop-mcp whoop-mcp-oauth
# 3. Register with Claude Code. The --env flags are required so the
# server can refresh tokens when the 1-hour access token expires.
claude mcp add whoop --scope user \
--env WHOOP_CLIENT_ID="$WHOOP_CLIENT_ID" \
--env WHOOP_CLIENT_SECRET="$WHOOP_CLIENT_SECRET" \
-- uvx --from whoop-mcp whoop-mcp
No clone, no venv, no absolute paths. Don't have uv? Install it once:
curl -LsSf https://astral.sh/uv/install.sh | sh (or brew install uv).
Paste the prompt in docs/AGENT_INSTALL_PROMPT.md into any MCP-aware agent (Claude Code, Claude Desktop, Cursor, Windsurf, Zed, Aider). The agent will run the install for you.
git clone https://github.com/AshwanthramKL/whoop-mcp.git
cd whoop-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
.venv/bin/pytest -q # 183 tests, ~6s, all respx-mocked
Then register with -- /abs/path/to/whoop-mcp/.venv/bin/python /abs/path/to/whoop-mcp/src/whoop_mcp_server.py.
For Claude Desktop, add the equivalent entry to
~/Library/Application Support/Claude/claude_desktop_config.json (macOS),
pointing command at the venv Python and args at src/whoop_mcp_server.py.
Run ./scripts/fresh_install_check.sh if you want a clean-env smoke
of the install path end-to-end (minus the browser OAuth flow).
Once the server is registered, try these prompts:
sync_whoop() and reports per-resource counts.get_whoop_daily_summary(date="YYYY-MM-DD") and shows a
joined cycle + recovery + primary sleep + workouts record.~/whoop-workouts.csv."
Claude calls export_whoop(kind="workouts", format="csv", path="...").Bundled with the repo: skills/whoop-insights. Ask your agent:
"How am I doing? Run the whoop-insights skill." "Generate my weekly WHOOP report." "Am I overtraining? Check the last 30 days."
The skill pulls 30 days from the cache, computes personal baselines (HRV, recovery, sleep, strain), flags anomalies with evidence, runs two correlations (sleep→next-day-recovery, strain→next-day-recovery), and optionally generates a self-contained HTML dashboard with Chart.js visualizations. Every claim cites a specific date and a specific number — the skill is instructed never to fabricate.
To use it outside this repo, copy skills/whoop-insights/ into your
agent's skills directory (e.g. ~/.claude/skills/ for Claude Code).
17 tools. All list tools accept start / end as ISO-8601 and auto-paginate.
Every read tool takes fresh: bool = False — pass True to bypass the
cache and hit the WHOOP API, write-through to cache, and return the live
response. Errors are returned as a structured {"error": {...}} envelope;
tools never raise.
| Tool | What it does | Key parameters |
|---|---|---|
get_whoop_auth_status | Report OAuth token status. Call first if other tools return AUTH_FAILED. | — |
get_whoop_profile | Authenticated user's WHOOP profile (name, email). | fresh |
get_whoop_body_measurement | Latest body measurements: height, weight, max HR. | fresh |
list_whoop_cycles | Physiological cycles in a time window. | start, end, limit, fresh |
get_whoop_cycle | One cycle by integer ID. | cycle_id, fresh |
get_whoop_cycle_sleep | Sleep record tied to a given cycle. | cycle_id, fresh |
get_whoop_cycle_recovery | Recovery record tied to a given cycle. | cycle_id, fresh |
list_whoop_recoveries | Recoveries (HRV / RHR / recovery score) in a window. | start, end, limit, fresh |
list_whoop_sleeps | Sleep activities incl. naps in a window. | start, end, limit, fresh |
get_whoop_sleep | One sleep activity by UUID. | sleep_id, fresh |
list_whoop_workouts | Workouts with zone durations (seconds). | start, end, limit, fresh |
get_whoop_workout | One workout by UUID. | workout_id, fresh |
get_whoop_daily_summary | Joined cycle + recovery + primary sleep + workouts for a UTC date. | date |
sync_whoop | Refresh cache from WHOOP API. Idempotent, incremental by default. | full, since, resources |
get_whoop_events | Chronological "what's new" feed across cached resources. | since, until, resources, limit |
export_whoop | Dump cached records to CSV / JSONL / Parquet. | kind, format, path, start, end, overwrite |
health_check | Composite status (auth, API, cache, schema). Never raises. | live |
Error codes: AUTH_FAILED, RATE_LIMITED, NOT_FOUND, UPSTREAM_ERROR,
VALIDATION_ERROR, CACHE_ERROR, CACHE_EMPTY, FILE_EXISTS,
EXPORT_ERROR, SYNC_ERROR.
The cache is also exposed as read-only MCP resources, so the client can browse date slices without invoking a tool.
| URI | Content |
|---|---|
whoop://db/cycles/{start}/{end} | Cached cycles in [start, end) (dates YYYY-MM-DD). |
whoop://db/recoveries/{start}/{end} | Cached recoveries. |
whoop://db/sleeps/{start}/{end} | Cached sleeps (including naps). |
whoop://db/workouts/{start}/{end} | Cached workouts. |
whoop://db/profile | Latest profile snapshot. |
whoop://db/body_measurement | Latest body_measurement snapshot. |
whoop://db/sync_runs/{limit} | Most recent sync audit rows. |
whoop://db/events/{since} | Event feed since since, until = now. |
whoop://db/events/{since}/{until} | Event feed for explicit window. |
All resources return application/json. Bad inputs return the same
{"error": {"code","message"}} envelope used by tools.
Responses are flattened: the WHOOP score wrapper is lifted, milliseconds
become seconds (*_seconds, 1 decimal), kilojoules become calories
(calories, rounded int), heart-rate keys are renamed to avg_hr_bpm /
max_hr_bpm, and sleep stages are named deep_sleep_seconds,
rem_sleep_seconds, light_sleep_seconds, awake_seconds,
in_bed_seconds. Raw user_id, v1_id, and per-record created_at are
dropped.
score_state convention. WHOOP scores are not always computed. When
score_state != "SCORED" (e.g. PENDING_SCORE, UNSCORABLE) every
derived score field is null and the top-level score_state is preserved
so callers know why.
One flattened record per resource:
// Cycle
{
"id": 123456,
"start": "2026-04-20T04:00:00.000Z",
"end": "2026-04-21T04:00:00.000Z",
"timezone_offset": "+00:00",
"score_state": "SCORED",
"strain": 12.4,
"avg_hr_bpm": 62,
"max_hr_bpm": 168,
"calories": 2810
}
// Recovery
{
"cycle_id": 123456,
"sleep_id": "bb68db7b-...",
"score_state": "SCORED",
"recovery_score": 74,
"resting_heart_rate_bpm": 48,
"hrv_rmssd_ms": 87.3,
"spo2_pct": 97.4,
"skin_temp_c": 33.1,
"user_calibrating": false
}
// Sleep
{
"id": "bb68db7b-...",
"cycle_id": 123456,
"start": "2026-04-20T03:10:00.000Z",
"end": "2026-04-20T10:42:00.000Z",
"nap": false,
"score_state": "SCORED",
"sleep_performance_pct": 88.0,
"in_bed_seconds": 27120.0,
"light_sleep_seconds": 12540.0,
"rem_sleep_seconds": 5280.0,
"deep_sleep_seconds": 6840.0,
"awake_seconds": 1080.0
}
// Workout
{
"id": "a91f...",
"start": "2026-04-20T17:00:00.000Z",
"end": "2026-04-20T17:48:00.000Z",
"sport_name": "Running",
"score_state": "SCORED",
"strain": 9.3,
"avg_hr_bpm": 142,
"max_hr_bpm": 176,
"calories": 511,
"distance_meter": 8030.0,
"zone_durations_seconds": {
"zone_zero": 0.0, "zone_one": 120.0, "zone_two": 900.0,
"zone_three": 1440.0, "zone_four": 420.0, "zone_five": 0.0
}
}
fresh=True bypasses the cache, hits the WHOOP API, write-throughs
the result into the cache, and returns the live response.MAX(updated_at) per resource as the cursor.
A fresh DB falls back to the last 90 days. sync_whoop(full=True) pulls
from 2010-01-01 for every resource (do this once on a brand-new cache).sync_whoop with no new upstream data is a
no-op. Combined with snapshot hash dedupe (below), this means the event
feed stays quiet.profile and body_measurement are singleton
snapshots. Before writing, the server compares SHA-256 of the canonical
raw payload against the stored blob. Byte-identical payloads do not
advance updated_at, so no spurious events are generated.get_whoop_events(since, until=None, resources=None, limit=500) is a
chronological feed across all cached resources — pure cache read, no API
calls. The window is exclusive on both ends:
updated_at > since AND updated_at < until. The strict since bound
means you can feed a returned cursor back in as the next since without
re-seeing a row.
Each event wraps a flat record:
{"resource": "sleeps",
"id": "bb68db7b-...",
"updated_at": "2026-04-20T14:12:33.123Z",
"record": { "...flat sleep..." }}
Response:
{"status": "success", "count": 17,
"since": "...", "until": "...",
"events": [...],
"next_cursor": null | "<opaque-base64>"}
next_cursor is an opaque base64 encoding of
updated_at|resource|id for the last returned event. Pass it back as
since to continue. The full triple is used as a tie-broken lower bound,
so events sharing an updated_at are never skipped at a pagination
boundary. Plain ISO-8601 strings as since still work (M5 compat).
Also exposed as MCP resources — see the MCP resources table above.
export_whoop(kind, format, path, start=None, end=None, overwrite=False)
writes flat cached records to disk. Pure data layer — never hits the API.
Run sync_whoop() first.
kind: cycles | recoveries | sleeps | workouts | all.
"all" writes one file per resource (cycles.*, recoveries.*, …)
into the given directory.format: csv (RFC 4180, alphabetically sorted header, nested
values JSON-encoded), jsonl (one record per line, sorted keys), or
parquet (pyarrow, snappy).overwrite: default False. If the destination has content,
returns FILE_EXISTS. True replaces silently.An empty window still yields a file (header-only CSV / empty JSONL / empty Parquet) so downstream tooling sees a consistent artifact.
Spelled out so nobody wastes an issue.
| Ask | Why not |
|---|---|
claude.ai (the web app) | We ship as a stdio MCP (local subprocess). claude.ai wants a remote HTTPS MCP. A hosted deployment would also break our single-user / local-only threat model. |
| Mobile Claude apps | Same stdio constraint. |
| Writing back to WHOOP (logging activities, updating weight, etc.) | Read-only by design. Adding writes would require reverse-engineered endpoints outside WHOOP's stable v2 API — too brittle and ToS-risky. See jd1207/whoop-mcp if you need that. |
| Multi-user / shared server | One user, one WHOOP account, one machine. The cache + encrypted tokens live under ~/.whoop-mcp-server/; there's no tenancy model. |
| More than 10 users per WHOOP dev app | WHOOP's dev-app cap is 10 users until app-level approval. Each user should create their own dev app — it's free and self-serve. |
| Push notifications / webhooks | Event feed is poll-driven from the cache. For proactive alerts, run sync_whoop + get_whoop_events on a cron from whatever scheduler you already have. |
| Python < 3.10 | The code uses X | Y union syntax. pip install will refuse on older Pythons. |
| Windows-specific install paths | Code works, but OAuth callback (localhost:8000) and path handling haven't been dogfooded on Windows. Ubuntu + macOS are the currently-tested surface. |
MCP servers don't auto-update. When you installed is when you pinned. How to pick up a new release depends on how you installed:
| Install path | How to update |
|---|---|
uvx --from whoop-mcp whoop-mcp | uvx --refresh --from whoop-mcp whoop-mcp, or uv cache clean whoop-mcp and re-invoke |
uvx --from whoop-mcp@latest whoop-mcp | Update happens on next cache miss (no manual step) |
pipx install whoop-mcp | pipx upgrade whoop-mcp |
pip install whoop-mcp | pip install --upgrade whoop-mcp |
| Git clone + venv | git pull && pip install -r requirements.txt |
Run health_check() afterwards — its pypi_update_available component
will confirm the version your MCP client is now running and whether a
newer one is published.
Breaking-change policy: while the project is 0.x, minor bumps
(0.8.x → 0.9.0) may include breaking changes — always called out in
the ### Changed section of CHANGELOG.md. Patch
bumps (0.8.3 → 0.8.3) are bugfix-only. Once we cut 1.0.0,
breaking changes require a major bump. Pin to a known-good 0.x.y if
you can't absorb churn.
~/.whoop-mcp-server/logs/whoop-mcp.log (~1 MB per file, 5 backups).
Env overrides: WHOOP_LOG_LEVEL (default INFO), WHOOP_LOG_FILE
(path; empty string disables the file handler), WHOOP_LOG_JSON
(default true).health_check(live=True) returns a dict with five component
checks (auth, api_reachable, cache_readable, cache_writable,
schema_version) plus an overall verdict healthy | degraded | unhealthy. live=False skips the network probe.whoop-mcp-oauth (after pip install) or
.venv/bin/python src/setup_direct_oauth.py (from source).Retry-After on 429s and
retries with exponential backoff. After the retry budget, the call
surfaces as RATE_LIMITED — tools never raise.Everything runs locally. OAuth tokens are encrypted at rest in
~/.whoop-mcp-server/tokens.json with a key at
~/.whoop-mcp-server/.encryption_key. The SQLite cache file is created
with mode 0o600. No third-party services — the only outbound network
calls are directly to api.prod.whoop.com. Logs do not include
tokens, refresh tokens, the client secret, or raw WHOOP response bodies.
See PRIVACY.md for the complete inventory of what the
server reads, writes, sends, and logs; and how to delete everything
(rm -rf ~/.whoop-mcp-server).
# Run the full test suite (183 tests).
.venv/bin/pytest -q
# Re-record fixtures (live calls, requires creds in env).
.venv/bin/python tests/record_fixtures.py
# Fresh-install smoke test (clones current HEAD into a tempdir and
# verifies the install path end-to-end, minus the browser OAuth flow).
bash scripts/fresh_install_check.sh
Adding a new tool. Add the implementation under src/whoop_mcp_server.py
with an @mcp.tool() decorator, a Pydantic model in src/whoop_models.py
if the response has a new shape, a cache table in src/whoop_store.py if
the resource is persisted, and tests in tests/. Keep the error envelope
(_error_payload / _map_error) — tools never raise.
Release flow. Bump src/__version__.py (single source of truth) →
add a new top section to CHANGELOG.md in
Keep-a-Changelog format → verify SERVER_VERSION in
whoop_mcp_server.py picks up the new value (the test_version_import
test prevents drift) → update version in pyproject.toml → tag
vX.Y.Z on main.
Start here:
Human contributors: CONTRIBUTING.md has the short
version. The test suite runs in ~6s (.venv/bin/pytest -q); every HTTP
call is respx-mocked so you can iterate offline.
Current version: 0.8.3 (see src/__version__.py). Semantic
versioning. Full history: CHANGELOG.md.
Forked from RomanEvstigneev/whoop-mcp-server
(v0.1.x). Substantially rewritten starting at v0.2.0 to use direct WHOOP
OAuth (no third-party proxy), the WHOOP v2 API, Pydantic-flattened
responses, a local SQLite cache with incremental sync, cache-first
reads, an event feed, exports (CSV/JSONL/Parquet), a health_check
tool, structured JSON logging, and 180+ tests (188 as of 0.8.3). Licensed MIT — see
LICENSE for both copyright lines.
mcp-name: io.github.AshwanthramKL/whoop-mcp
Be the first to review this server!
by Modelcontextprotocol · Developer Tools
Read, search, and manipulate Git repositories programmatically
by Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
by mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.