whoop-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@whoop-mcphow’s my recovery and sleep looking today?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
whoop-mcp
A remote MCP server for WHOOP, running on Cloudflare Workers. Any MCP client (Claude, ChatGPT, your own agent) connects over Streamable HTTP, signs in with WHOOP through OAuth 2.1, and gets read-only tools over the user's recovery, sleep, strain and workouts. It can also receive WHOOP webhooks and forward them, signed, to your own endpoints.
OAuth 2.1 authorization server for MCP clients (PKCE S256, Dynamic Client Registration, Client ID Metadata Documents, RFC 8414 / RFC 9728 metadata, RFC 7009 revocation), built on
@cloudflare/workers-oauth-provider.Read-only data tools (
readOnlyHint: true) against the WHOOP API v2.WHOOP tokens never reach the MCP client: they live in a per-user Durable Object.
Signed webhooks: WHOOP v2 events are verified and re-signed before forwarding.
Tools
Tool | WHOOP endpoint | Notes |
|
| user id, name, email |
|
| height, weight, max heart rate |
|
|
|
|
| same pagination |
|
| same pagination |
|
| same pagination; daily strain |
| cycle + recovery + sleep | latest of each, in one call |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| not read-only ( |
Every data tool is annotated readOnlyHint: true. Ids are validated before any request is made.
The user-facing strings (tool descriptions, consent page) are currently in Spanish.
Related MCP server: WHOOP MCP Server
Architecture
MCP client ──OAuth 2.1 + PKCE──▶ Worker (authorization server + /mcp) ──OAuth 2.0──▶ WHOOP
│
├── KV OAUTH_KV clients, grants, token hashes, encrypted props
└── DO WhoopTokenVault one object per WHOOP user: WHOOP access/refresh token
WHOOP ──POST /webhooks/whoop──▶ Worker ──signed POST──▶ FORWARD_WEBHOOK_URLSsrc/index.ts: theOAuthProvider(authorization server + protected resource/mcp), the MCP handler (createMcpHandlerfromagents), the RFC 7009 revocation hook, and the webhook and disconnect routes.src/auth-handler.ts:/authorizeshows a per-client consent page (confused-deputy protection), then redirects to WHOOP;/callbackexchanges the WHOOP code, stores the WHOOP tokens in the user's vault and issues the MCP grant. The grant only carries the WHOOPuser_id, encrypted.src/token-vault.ts: theWhoopTokenVaultDurable Object. WHOOP rotates refresh tokens on every use, so refreshes are serialized per user and persisted before the new access token is returned.src/mcp-server.ts: the tools.src/webhooks.ts: webhook verification and forwarding.src/grants.ts: listing and revoking a user's MCP grants.src/icon.ts: the server icon, served atGET /icon.svgand announced inserverInfo.icons(MCP spec 2025-11) as${PUBLIC_BASE_URL}/icon.svg. It is a generic heart-and-pulse glyph, not the WHOOP logo: this project is not affiliated with or endorsed by WHOOP, Inc.
One WHOOP user may have several MCP grants (one per client); they all share that user's vault.
Disconnecting and revocation
The WHOOP token is revoked at WHOOP (DELETE /v2/user/access) and deleted from the vault when:
The MCP client revokes its grant (RFC 7009 on
/token, the advertisedrevocation_endpoint, with the refresh token) and it was the user's last grant. If another MCP client is still connected for the same WHOOP user, WHOOP stays connected. Revoking only an access token ends that token, not the connection. A made-up token in the provider's format disconnects nobody: the hook only acts when a grant that existed before the request is gone after it.The user asks to disconnect, through the
disconnect_whooptool ({ "confirm": true }) orPOST /mcp/disconnectwith the user's MCP bearer token. Both revoke WHOOP access, delete the vault and revoke all of the user's MCP grants (without WHOOP access none of them would work). The response is{ "disconnected": true, "whoop_access_revoked": <bool>, "mcp_grants_revoked": <n> }.All of the user's grants disappear without a revoke call (grants expire after 30 days without use). The vault sets a Durable Object alarm; when it finds no live grant for the user it disconnects as above, otherwise it re-arms itself after the longest-lived grant.
Local deletion always happens, even if WHOOP's revoke call fails (users can also revoke the app from
their WHOOP account). The reverse direction is handled too: if WHOOP rejects the refresh token (the
user revoked access at WHOOP), the next MCP token refresh answers invalid_grant and the grant is
deleted, so the client re-authorizes instead of retrying forever.
Webhooks
Register the URL at WHOOP
In the WHOOP developer dashboard (developer.whoop.com), open your app.
Under Webhooks, add
https://<your-worker-host>/webhooks/whoopand choose v2 in the Model Version dropdown. Save.WHOOP signs each delivery with your app's client secret, which this Worker already has (
WHOOP_CLIENT_SECRET). There is nothing else to configure for verification.
WHOOP sends recovery.updated, recovery.deleted, sleep.updated, sleep.deleted,
workout.updated and workout.deleted for users who authorized your app. In v2 the id is a UUID;
for recovery events it is the id of the associated sleep.
What the Worker does
Verifies
X-WHOOP-Signature=base64(HMAC-SHA256(X-WHOOP-Signature-Timestamp + raw_body, WHOOP_CLIENT_SECRET))with a constant-time check, and rejects timestamps (milliseconds) more than 5 minutes away. Invalid or stale signatures get401, malformed events400.Answers
204immediately. Forwarding runs inctx.waitUntil, so a slow destination never makes WHOOP retry.De-duplicates by
trace_id(KV key with a 1 h TTL, best effort), so WHOOP retries of the same event are forwarded once.POSTs the event to every URL in
FORWARD_WEBHOOK_URLS(comma-separated,httpsonly, up to 10), with up to 3 attempts per destination (retrying 5xx, 408, 429 and network errors).
With no FORWARD_WEBHOOK_URLS it only verifies and answers 204. With URLs but no
FORWARD_WEBHOOK_SECRET (32+ characters) it refuses to forward unsigned and logs an error.
Forwarded request format
POST <your url>
Content-Type: application/json
X-Whoop-MCP-Signature-Timestamp: 1790000000000
X-Whoop-MCP-Signature: <base64 HMAC-SHA256(timestamp + raw_body, FORWARD_WEBHOOK_SECRET)>
X-Whoop-MCP-Event: recovery.updated
X-Whoop-MCP-Trace-Id: d3709ee7-104e-4f70-a928-2932964b017b{
"version": 1,
"source": "whoop",
"user_id": 10129,
"type": "recovery.updated",
"id": "ecfc6a15-4661-442f-a9a4-f160dd7afae8",
"trace_id": "d3709ee7-104e-4f70-a928-2932964b017b",
"received_at": "2026-09-28T07:12:03.120Z",
"recovery": { "cycle_id": 93845, "sleep_id": "ecfc6a15-4661-442f-a9a4-f160dd7afae8", "score": { "recovery_score": 67 } }
}user_id is the WHOOP user id (the same value get_profile returns). recovery is present only
for recovery.updated and only when the Worker holds a token for that user (it resolves
sleep → cycle_id → GET /v2/cycle/{cycleId}/recovery); otherwise you get the bare event and can
fetch the data yourself. *.deleted events never carry data.
Verifying on the receiving side (any runtime with Web Crypto):
async function verifyWhoopMcpWebhook(request: Request, secret: string): Promise<unknown | null> {
const timestamp = request.headers.get("X-Whoop-MCP-Signature-Timestamp") ?? "";
const signature = request.headers.get("X-Whoop-MCP-Signature") ?? "";
const raw = await request.text(); // verify the raw body, before parsing
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return null;
const enc = new TextEncoder();
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
let sig: Uint8Array;
try {
sig = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
} catch {
return null;
}
const ok = await crypto.subtle.verify("HMAC", key, sig, enc.encode(timestamp + raw)); // constant time
return ok ? JSON.parse(raw) : null; // then de-duplicate on trace_id
}Answer 2xx quickly. 5xx, 408, 429 and network errors are retried (3 attempts in total over a few seconds); other 4xx answers are not.
Deploy your own in 5 steps
You need a Cloudflare account and a WHOOP developer app (developer.whoop.com).
Clone and install
git clone https://github.com/vicens-aniol/whoop-mcp.git && cd whoop-mcp npm install && npx wrangler loginSet your URL. In
wrangler.jsonc, changePUBLIC_BASE_URLto the URL your Worker will have (e.g.https://whoop-mcp.<your-subdomain>.workers.dev, or a custom domain). OAuth tokens are bound to${PUBLIC_BASE_URL}/mcp.Configure the WHOOP app: redirect URI
${PUBLIC_BASE_URL}/callback, scopesoffline read:profile read:body_measurement read:recovery read:cycles read:sleep read:workout, and optionally the webhook URL${PUBLIC_BASE_URL}/webhooks/whoop(model v2).Deploy. The KV namespace is created automatically on the first deploy.
npx wrangler deployAdd the secrets (values are prompted or piped, never typed on the command line):
npx wrangler secret put WHOOP_CLIENT_ID npx wrangler secret put WHOOP_CLIENT_SECRET openssl rand -hex 32 | npx wrangler secret put CONSENT_SECRET # optional, webhook forwarding: openssl rand -hex 32 | npx wrangler secret put FORWARD_WEBHOOK_SECRET npx wrangler secret put FORWARD_WEBHOOK_URLS # e.g. https://example.com/hooks/whoop
Check GET ${PUBLIC_BASE_URL}/ ("whoop_configured": true), then add ${PUBLIC_BASE_URL}/mcp as a
remote MCP server in your client. Hand FORWARD_WEBHOOK_SECRET to the receiver through a secret
manager, never in plain text.
Keeping your real ids out of git
wrangler.jsonc is a template without account or namespace ids. To pin your own values without
committing them, copy it to wrangler.local.jsonc (gitignored), add account_id, the KV id and
your PUBLIC_BASE_URL, and deploy with:
npm run deploy:local # wrangler deploy -c wrangler.local.jsonc
npx wrangler secret put NAME -c wrangler.local.jsoncIf you move to another URL later, update PUBLIC_BASE_URL and the WHOOP redirect URI, and reconnect
clients (existing tokens are bound to the old resource).
Security
WHOOP access and refresh tokens are stored only in the user's Durable Object and never exposed over HTTP or to MCP clients. MCP grants carry only the WHOOP user id, encrypted by the provider. MCP tokens, codes and client secrets are stored in KV only as hashes.
Consent is asked per client before redirecting to WHOOP. The consent page cannot be framed, and its handle and the upstream
stateare single use and bound to the browser with__Host-cookies. All client-supplied metadata is HTML-escaped.WHOOP token refreshes are serialized per user, because WHOOP refresh tokens are single use.
Webhooks: constant-time HMAC verification, 5 minute replay window, 64 KB body limit, forwarding only to
httpsURLs without credentials, and never unsigned.All secrets are Worker secrets.
.dev.vars,.env*andwrangler.local.jsoncare gitignored;.dev.vars.examplelists the variable names with no values.Found a vulnerability? Please report it privately through a GitHub security advisory rather than a public issue.
Development
cp .dev.vars.example .dev.vars # fill in for `npm run dev`
npm test # vitest inside workerd; WHOOP is mocked, no request leaves the machine
npm run type-check
npm run cf-typegen # after changing wrangler.jsoncLicense
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Multi-tenant hosted MCP server for Oura Ring — 21 read-only tools, OAuth per user.
OAuth MCP for Google, Meta, X, LinkedIn, Reddit, TikTok, GSC, GA4, WordPress and GHL.
Your OpenWork org's skills, plugins, workflows, and connections through one OAuth MCP URL.
Related MCP Servers
- AlicenseBqualityFmaintenanceEnables access to WHOOP fitness and health data through all WHOOP v2 API endpoints. Supports OAuth 2.0 authentication and provides comprehensive access to user profiles, physiological cycles, recovery metrics, sleep analysis, and workout data.16196 npm17MIT
- AlicenseAqualityDmaintenanceExposes WHOOP recovery, sleep, strain, and workout metrics to MCP-compatible AI assistants using OAuth 2.0 authentication, enabling daily wellbeing snapshots, trend analysis, and workload recommendations.6196 npmMIT
- AlicenseAqualityDmaintenanceMCP server providing read access to WHOOP biometric data including recovery, sleep, strain, and workouts.161MIT
- AlicenseNot gradedqualityBmaintenanceEnables OAuth-authenticated access to Garmin account data through MCP, with a read-only web dashboard for monitoring account connections and activity.MIT