Install
$ agentstack add mcp-ashwanthramkl-whoop-mcp Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged2 finding(s); flagged for manual review. · v0.8.3 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Destructive filesystem operation.
- high Pipes remote content directly into a shell (remote code execution).
About
WHOOP MCP Server
[](https://github.com/AshwanthramKL/whoop-mcp/actions/workflows/ci.yml) [](https://pypi.org/project/whoop-mcp/) [](https://www.python.org/downloads/) [](./LICENSE) [](https://modelcontextprotocol.io) [](./tests)
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.5 — see [CHANGELOG.md](./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.x minor 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/AGENTINSTALLPROMPT.md](./docs/AGENTINSTALLPROMPT.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](./AGENTS.md) and > [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) — they're the > load-bearing conventions and one-page system map.
Table of contents
- [What this is](#what-this-is)
- [Install (one line via uvx)](#install-one-line-via-uvx)
- [Install with your agent](#install-with-your-agent)
- [Install from source](#install-from-source-for-development)
- [Analysis skill: whoop-insights](#analysis-skill-whoop-insights)
- [Quick start](#quick-start)
- [Tool catalog](#tool-catalog)
- [MCP resources](#mcp-resources)
- [Data model](#data-model)
- [Sync model](#sync-model)
- [Event feed](#event-feed)
- [Exports](#exports)
- [What's not supported](#whats-not-supported)
- [Updating](#updating)
- [Operations](#operations)
- [Security and privacy](#security-and-privacy)
- [Development](#development)
- [For agents building on this repo](#for-agents-building-on-this-repo)
- [Versioning](#versioning)
What this is
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.
Install (one line via uvx)
# 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="" \
WHOOP_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).
Install with your agent
Paste the prompt in [docs/AGENTINSTALLPROMPT.md](./docs/AGENTINSTALLPROMPT.md) into any MCP-aware agent (Claude Code, Claude Desktop, Cursor, Windsurf, Zed, Aider). The agent will run the install for you.
Install from source (for development)
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).
Quick start
Once the server is registered, try these prompts:
- Sync the cache. "Sync my WHOOP data from the last 30 days."
Claude calls sync_whoop() and reports per-resource counts.
- Inspect a day. "Give me yesterday's WHOOP daily summary."
Claude calls get_whoop_daily_summary(date="YYYY-MM-DD") and shows a joined cycle + recovery + primary sleep + workouts record.
- Export. "Export all my cached workouts to
~/whoop-workouts.csv."
Claude calls export_whoop(kind="workouts", format="csv", path="...").
Analysis skill: whoop-insights
Bundled with the repo: [skills/whoop-insights](./skills/whoop-insights/SKILL.md).
Install (one command):
whoop-mcp-install-skills # writes to ~/.claude/skills/
That fetches the latest skill from this repo's main branch and drops it where Claude Code (and Cursor / Windsurf / Zed) auto-discovers user-scope skills. Restart your MCP client session and ask:
> "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.
For repo-native developers iterating on the skill itself: whoop-mcp-install-skills --source ./skills installs from your local working tree instead of GitHub.
Tool catalog
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.
MCP resources
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.
Data model
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
}
}
Sync model
- Cache-first reads. Every list/get tool reads from SQLite by default.
An empty window transparently triggers a targeted sync and re-reads.
fresh=Truebypasses the cache, hits the WHOOP API, write-throughs
the result into the cache, and returns the live response.
- Incremental sync uses
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).
- Idempotent. Re-running
sync_whoopwith no new upstream data is a
no-op. Combined with snapshot hash dedupe (below), this means the event feed stays quiet.
- Snapshot hash dedupe.
profileandbody_measurementare 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.
Event feed
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: `updatedat > since AND updatedat "}
`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](#mcp-resources)
table above.
## Exports
`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.
## What's not supported
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`](https://github.com/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. |
## Updating
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 wh
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [AshwanthramKL](https://github.com/AshwanthramKL)
- **Source:** [AshwanthramKL/whoop-mcp](https://github.com/AshwanthramKL/whoop-mcp)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.8.3 Imported from the upstream source.