@souravpn/whoop-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # whoop-mcp
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that gives Claude access to your WHOOP biometric data — recovery, sleep, strain, and workouts.
4
+
5
+ Ask Claude things like:
6
+
7
+ - _"How's my recovery today?"_
8
+ - _"How did I sleep last night?"_
9
+ - _"How has my HRV trended this week?"_
10
+ - _"What was my strain from yesterday's workout?"_
11
+
12
+ ---
13
+
14
+ ## Privacy
15
+
16
+ This app accesses your WHOOP data locally on your device. No data is sent to any third-party server.
17
+
18
+ ---
19
+
20
+ ## Tools
21
+
22
+ | Tool | What it returns |
23
+ | --------------------- | --------------------------------------------------------------------- |
24
+ | `get_recovery` | Recovery score, HRV, resting HR, SpO2 |
25
+ | `get_sleep` | Sleep duration, stages (light/deep/REM), efficiency, respiratory rate |
26
+ | `get_strain` | Day strain score, avg/max HR, calories |
27
+ | `get_latest_workout` | Most recent workout — sport, duration, strain, HR zones |
28
+ | `get_recovery_trend` | Recovery scores over N days (default 7) |
29
+ | `get_sleep_trend` | Sleep data over N days (default 7) |
30
+ | `get_workout_history` | Recent workout history (default 5) |
31
+ | `get_profile` | Profile + body measurements |
32
+
33
+ ---
34
+
35
+ ## Setup
36
+
37
+ ### 1. Create a WHOOP developer app
38
+
39
+ 1. Go to [developer-dashboard.whoop.com](https://developer-dashboard.whoop.com)
40
+ 2. Sign in with your WHOOP account
41
+ 3. Create a new App:
42
+ - Name: `whoop-mcp` (or anything)
43
+ - Redirect URI: `http://localhost:8080/callback`
44
+ - Scopes: select all read scopes + `offline`
45
+ 4. Copy your **Client ID** and **Client Secret**
46
+
47
+ ### 2. Install
48
+
49
+ ```bash
50
+ npm install -g whoop-mcp
51
+ ```
52
+
53
+ ### 3. Run one-time auth setup
54
+
55
+ This opens your browser, you log into WHOOP, and your tokens are saved to `~/.whoop-mcp-tokens.json`:
56
+
57
+ ```bash
58
+ WHOOP_CLIENT_ID=your_id WHOOP_CLIENT_SECRET=your_secret whoop-mcp-auth-setup
59
+ ```
60
+
61
+ You only need to do this once. The server will auto-refresh tokens after that.
62
+
63
+ ### 4. Add to Claude Desktop
64
+
65
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "whoop": {
71
+ "command": "whoop-mcp",
72
+ "env": {
73
+ "WHOOP_CLIENT_ID": "your_client_id",
74
+ "WHOOP_CLIENT_SECRET": "your_client_secret"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Restart Claude Desktop. You should see a green "running" badge in **Settings → Developer**.
82
+
83
+ ### 5. Test it
84
+
85
+ Open a new chat and ask:
86
+
87
+ ```
88
+ How's my recovery today?
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Example queries
94
+
95
+ **Daily check-in:**
96
+
97
+ ```
98
+ What's my recovery, sleep, and strain for today?
99
+ ```
100
+
101
+ **Trend analysis:**
102
+
103
+ ```
104
+ How has my HRV trended over the past 7 days?
105
+ ```
106
+
107
+ **Workout correlation:**
108
+
109
+ ```
110
+ Look at my workouts this week and my recovery scores
111
+ the day after each one. Is there a pattern?
112
+ ```
113
+
114
+ **Full briefing:**
115
+
116
+ ```
117
+ Give me a complete health briefing — recovery, last
118
+ night's sleep breakdown, and any workouts from yesterday
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Pairing with Oura
124
+
125
+ If you also use Oura Ring, you can run both MCP servers together and ask Claude to cross-reference:
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "whoop": {
131
+ "command": "/path/to/whoop-mcp",
132
+ "env": { "WHOOP_ACCESS_TOKEN": "your_whoop_token" }
133
+ },
134
+ "oura": {
135
+ "command": "/path/to/oura-mcp",
136
+ "env": { "OURA_ACCESS_TOKEN": "your_oura_token" }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ Then ask:
143
+
144
+ ```
145
+ Compare my WHOOP and Oura HRV readings for this week.
146
+ Do they agree? Which is trending higher?
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ git clone https://github.com/yourusername/whoop-mcp
155
+ cd whoop-mcp
156
+ npm install
157
+ npm run build
158
+
159
+ # Test locally
160
+ WHOOP_ACCESS_TOKEN=your_token node dist/index.js
161
+ ```
162
+
163
+ ### Project structure
164
+
165
+ ```
166
+ whoop-mcp/
167
+ ├── src/
168
+ │ ├── index.ts # MCP server + tool definitions
169
+ │ └── whoop.ts # WHOOP API client + formatters
170
+ ├── package.json
171
+ ├── tsconfig.json
172
+ └── README.md
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Contributing
178
+
179
+ PRs welcome. Some ideas for extension:
180
+
181
+ - Heart rate time series data
182
+ - Sleep stage timeline (light/deep/REM per hour)
183
+ - Strain goal recommendations
184
+ - Weekly summary tool
185
+
186
+ ---
187
+
188
+ ## License
189
+
190
+ MIT
191
+
192
+ ---
193
+
194
+ ## Acknowledgements
195
+
196
+ Built with the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) and the [WHOOP Developer API](https://developer.whoop.com).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // auth-setup.ts — one-time OAuth2 setup for WHOOP
4
+ //
5
+ // Run this ONCE to get your initial access + refresh tokens.
6
+ // After that, the MCP server handles refreshing automatically.
7
+ //
8
+ // USAGE:
9
+ // node dist/auth-setup.js
10
+ //
11
+ // WHAT IT DOES:
12
+ // 1. Reads WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET from env
13
+ // 2. Starts a local HTTP server on port 8080
14
+ // 3. Prints an authorization URL — open it in your browser
15
+ // 4. You log into WHOOP and authorize the app
16
+ // 5. WHOOP redirects back to localhost:8080 with an auth code
17
+ // 6. The script exchanges the code for tokens
18
+ // 7. Saves tokens to ~/.whoop-mcp-tokens.json
19
+ // 8. Done — you can now run the MCP server
20
+ //
21
+ // GETTING CLIENT_ID AND CLIENT_SECRET:
22
+ // 1. Go to https://developer-dashboard.whoop.com
23
+ // 2. Sign in with your WHOOP account
24
+ // 3. Create a new App:
25
+ // - Name: "whoop-mcp" (or anything)
26
+ // - Redirect URI: http://localhost:8080/callback
27
+ // - Scopes: select all read scopes + offline
28
+ // 4. Copy the Client ID and Client Secret
29
+ // 5. Run: WHOOP_CLIENT_ID=xxx WHOOP_CLIENT_SECRET=yyy node dist/auth-setup.js
30
+ // =============================================================================
31
+ import { createServer } from 'http';
32
+ import { saveTokens } from './auth.js';
33
+ const clientId = process.env.WHOOP_CLIENT_ID;
34
+ const clientSecret = process.env.WHOOP_CLIENT_SECRET;
35
+ if (!clientId || !clientSecret) {
36
+ console.error('Missing required env variables.');
37
+ console.error('Usage: WHOOP_CLIENT_ID=xxx WHOOP_CLIENT_SECRET=yyy node dist/auth-setup.js');
38
+ process.exit(1);
39
+ }
40
+ const REDIRECT_URI = 'http://localhost:8080/callback';
41
+ const AUTH_URL = 'https://api.prod.whoop.com/oauth/oauth2/auth';
42
+ const TOKEN_URL = 'https://api.prod.whoop.com/oauth/oauth2/token';
43
+ const SCOPES = 'offline read:recovery read:cycles read:workout read:sleep read:profile read:body_measurement';
44
+ // Generate a random state string for CSRF protection
45
+ const state = Math.random().toString(36).substring(2, 18);
46
+ // Build the authorization URL
47
+ const authUrl = new URL(AUTH_URL);
48
+ authUrl.searchParams.set('response_type', 'code');
49
+ authUrl.searchParams.set('client_id', clientId);
50
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
51
+ authUrl.searchParams.set('scope', SCOPES);
52
+ authUrl.searchParams.set('state', state);
53
+ console.log('\n=== WHOOP MCP — One-time Auth Setup ===\n');
54
+ console.log('1. Open this URL in your browser:\n');
55
+ console.log(` ${authUrl.toString()}\n`);
56
+ console.log('2. Log in to WHOOP and authorize the app');
57
+ console.log('3. You\'ll be redirected back here automatically\n');
58
+ console.log('Waiting for authorization...\n');
59
+ // Start local server to receive the redirect
60
+ const server = createServer(async (req, res) => {
61
+ if (!req.url?.startsWith('/callback')) {
62
+ res.writeHead(404);
63
+ res.end('Not found');
64
+ return;
65
+ }
66
+ const url = new URL(req.url, 'http://localhost:8080');
67
+ const code = url.searchParams.get('code');
68
+ const returnedState = url.searchParams.get('state');
69
+ const error = url.searchParams.get('error');
70
+ if (error) {
71
+ res.writeHead(400, { 'Content-Type': 'text/html' });
72
+ res.end(`<h1>Authorization failed</h1><p>${error}</p>`);
73
+ console.error(`Authorization failed: ${error}`);
74
+ server.close();
75
+ process.exit(1);
76
+ }
77
+ if (returnedState !== state) {
78
+ res.writeHead(400, { 'Content-Type': 'text/html' });
79
+ res.end('<h1>State mismatch — possible CSRF attack</h1>');
80
+ console.error('State mismatch');
81
+ server.close();
82
+ process.exit(1);
83
+ }
84
+ if (!code) {
85
+ res.writeHead(400, { 'Content-Type': 'text/html' });
86
+ res.end('<h1>No authorization code received</h1>');
87
+ server.close();
88
+ process.exit(1);
89
+ }
90
+ // Exchange code for tokens
91
+ console.log('Authorization code received — exchanging for tokens...');
92
+ try {
93
+ const body = new URLSearchParams({
94
+ grant_type: 'authorization_code',
95
+ code,
96
+ client_id: clientId,
97
+ client_secret: clientSecret,
98
+ redirect_uri: REDIRECT_URI,
99
+ });
100
+ const tokenResponse = await fetch(TOKEN_URL, {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
103
+ body: body.toString(),
104
+ });
105
+ if (!tokenResponse.ok) {
106
+ const text = await tokenResponse.text();
107
+ throw new Error(`Token exchange failed (${tokenResponse.status}): ${text}`);
108
+ }
109
+ const data = await tokenResponse.json();
110
+ saveTokens({
111
+ access_token: data.access_token,
112
+ refresh_token: data.refresh_token,
113
+ expires_at: Date.now() + data.expires_in * 1000,
114
+ });
115
+ res.writeHead(200, { 'Content-Type': 'text/html' });
116
+ res.end(`
117
+ <h1>✅ Authorization successful!</h1>
118
+ <p>Your WHOOP tokens have been saved to <code>~/.whoop-mcp-tokens.json</code></p>
119
+ <p>You can close this tab and start using the MCP server.</p>
120
+ `);
121
+ console.log('\n✅ Success! Tokens saved to ~/.whoop-mcp-tokens.json');
122
+ console.log('\nAdd this to your claude_desktop_config.json:\n');
123
+ console.log(JSON.stringify({
124
+ mcpServers: {
125
+ whoop: {
126
+ command: 'whoop-mcp', // or full path
127
+ env: {
128
+ WHOOP_CLIENT_ID: clientId,
129
+ WHOOP_CLIENT_SECRET: clientSecret,
130
+ },
131
+ },
132
+ },
133
+ }, null, 2));
134
+ console.log('\nThe server will auto-refresh tokens using the saved refresh token.\n');
135
+ }
136
+ catch (err) {
137
+ res.writeHead(500, { 'Content-Type': 'text/html' });
138
+ res.end(`<h1>Error</h1><p>${err}</p>`);
139
+ console.error('Token exchange failed:', err);
140
+ }
141
+ server.close();
142
+ process.exit(0);
143
+ });
144
+ server.listen(8080, () => {
145
+ // Try to auto-open the browser on macOS
146
+ import('child_process').then(({ exec }) => {
147
+ exec(`open "${authUrl.toString()}"`);
148
+ }).catch(() => {
149
+ // Non-macOS — user opens manually
150
+ });
151
+ });
152
+ //# sourceMappingURL=auth-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-setup.js","sourceRoot":"","sources":["../src/auth-setup.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface TokenStore {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_at: number;
5
+ }
6
+ export interface OAuthConfig {
7
+ clientId: string;
8
+ clientSecret: string;
9
+ }
10
+ export declare function loadTokens(): TokenStore;
11
+ export declare function saveTokens(tokens: TokenStore): void;
12
+ export declare function isExpired(tokens: TokenStore): boolean;
13
+ export declare function refreshTokens(tokens: TokenStore, config: OAuthConfig): Promise<TokenStore>;
14
+ export declare class TokenManager {
15
+ private tokens;
16
+ private config;
17
+ constructor(config: OAuthConfig);
18
+ getAccessToken(): Promise<string>;
19
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,98 @@
1
+ // =============================================================================
2
+ // auth.ts — WHOOP OAuth2 token manager
3
+ //
4
+ // WHOOP uses OAuth2 authorization code flow. Access tokens expire every ~hour.
5
+ // This module:
6
+ // 1. Loads tokens from a local JSON file (~/.whoop-mcp-tokens.json)
7
+ // 2. Checks expiry before every API call
8
+ // 3. Refreshes automatically using the refresh token
9
+ // 4. Saves updated tokens back to disk
10
+ //
11
+ // INITIAL SETUP (one-time):
12
+ // You need to do the first OAuth flow manually to get your initial tokens.
13
+ // Run: node dist/auth-setup.js
14
+ // This starts a local server, opens your browser, and saves your tokens.
15
+ //
16
+ // TOKEN FILE (~/.whoop-mcp-tokens.json):
17
+ // {
18
+ // "access_token": "...",
19
+ // "refresh_token": "...",
20
+ // "expires_at": 1234567890000 ← ms since epoch
21
+ // }
22
+ // =============================================================================
23
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
24
+ import { homedir } from 'os';
25
+ import { join } from 'path';
26
+ const TOKEN_FILE = join(homedir(), '.whoop-mcp-tokens.json');
27
+ const TOKEN_URL = 'https://api.prod.whoop.com/oauth/oauth2/token';
28
+ // Buffer: refresh 5 minutes before actual expiry
29
+ const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
30
+ // ---- Token file I/O ---------------------------------------------------------
31
+ export function loadTokens() {
32
+ if (!existsSync(TOKEN_FILE)) {
33
+ throw new Error(`No WHOOP tokens found at ${TOKEN_FILE}.\n` +
34
+ `Run: node dist/auth-setup.js\n` +
35
+ `This will open your browser to authorize the app and save your tokens.`);
36
+ }
37
+ try {
38
+ const raw = readFileSync(TOKEN_FILE, 'utf-8');
39
+ return JSON.parse(raw);
40
+ }
41
+ catch {
42
+ throw new Error(`Failed to parse token file at ${TOKEN_FILE}. Try running auth-setup again.`);
43
+ }
44
+ }
45
+ export function saveTokens(tokens) {
46
+ writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 }); // owner read/write only
47
+ process.stderr.write(`[whoop-mcp] Tokens saved to ${TOKEN_FILE}\n`);
48
+ }
49
+ // ---- Token refresh ----------------------------------------------------------
50
+ export function isExpired(tokens) {
51
+ return Date.now() >= tokens.expires_at - EXPIRY_BUFFER_MS;
52
+ }
53
+ export async function refreshTokens(tokens, config) {
54
+ process.stderr.write('[whoop-mcp] Access token expired — refreshing...\n');
55
+ const body = new URLSearchParams({
56
+ grant_type: 'refresh_token',
57
+ refresh_token: tokens.refresh_token,
58
+ client_id: config.clientId,
59
+ client_secret: config.clientSecret,
60
+ scope: 'offline read:recovery read:cycles read:workout read:sleep read:profile read:body_measurement',
61
+ });
62
+ const response = await fetch(TOKEN_URL, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
65
+ body: body.toString(),
66
+ });
67
+ if (!response.ok) {
68
+ const text = await response.text();
69
+ throw new Error(`Failed to refresh WHOOP token (${response.status}): ${text}\n` +
70
+ `Your refresh token may have expired. Run: node dist/auth-setup.js`);
71
+ }
72
+ const data = await response.json();
73
+ const newTokens = {
74
+ access_token: data.access_token,
75
+ refresh_token: data.refresh_token ?? tokens.refresh_token, // WHOOP may or may not rotate refresh token
76
+ expires_at: Date.now() + data.expires_in * 1000,
77
+ };
78
+ saveTokens(newTokens);
79
+ process.stderr.write('[whoop-mcp] Token refreshed successfully\n');
80
+ return newTokens;
81
+ }
82
+ // ---- Token manager class ----------------------------------------------------
83
+ // Used by WhoopClient to get a always-valid access token
84
+ export class TokenManager {
85
+ tokens;
86
+ config;
87
+ constructor(config) {
88
+ this.config = config;
89
+ this.tokens = loadTokens();
90
+ }
91
+ async getAccessToken() {
92
+ if (isExpired(this.tokens)) {
93
+ this.tokens = await refreshTokens(this.tokens, this.config);
94
+ }
95
+ return this.tokens.access_token;
96
+ }
97
+ }
98
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,+EAA+E;AAC/E,eAAe;AACf,sEAAsE;AACtE,2CAA2C;AAC3C,uDAAuD;AACvD,yCAAyC;AACzC,EAAE;AACF,4BAA4B;AAC5B,6EAA6E;AAC7E,iCAAiC;AACjC,2EAA2E;AAC3E,EAAE;AACF,yCAAyC;AACzC,MAAM;AACN,6BAA6B;AAC7B,8BAA8B;AAC9B,qDAAqD;AACrD,MAAM;AACN,gFAAgF;AAEhF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,wBAAwB,CAAC,CAAC;AAC7D,MAAM,SAAS,GAAG,+CAA+C,CAAC;AAElE,iDAAiD;AACjD,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAavC,gFAAgF;AAEhF,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,4BAA4B,UAAU,KAAK;YAC3C,gCAAgC;YAChC,wEAAwE,CACzE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAe,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,iCAAiC,UAAU,iCAAiC,CAAC,CAAC;IAChG,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,wBAAwB;IACrG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,UAAU,IAAI,CAAC,CAAC;AACtE,CAAC;AAED,gFAAgF;AAEhF,MAAM,UAAU,SAAS,CAAC,MAAkB;IAC1C,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,UAAU,GAAG,gBAAgB,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAkB,EAClB,MAAmB;IAEnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;IAE3E,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;QAC/B,UAAU,EAAE,eAAe;QAC3B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;QAClC,KAAK,EAAE,8FAA8F;KACtG,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACtC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;KACtB,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,kCAAkC,QAAQ,CAAC,MAAM,MAAM,IAAI,IAAI;YAC/D,mEAAmE,CACpE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAI/B,CAAC;IAEF,MAAM,SAAS,GAAe;QAC5B,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,MAAM,CAAC,aAAa,EAAE,4CAA4C;QACvG,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI;KAChD,CAAC;IAEF,UAAU,CAAC,SAAS,CAAC,CAAC;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;IACnE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,gFAAgF;AAChF,yDAAyD;AAEzD,MAAM,OAAO,YAAY;IACf,MAAM,CAAa;IACnB,MAAM,CAAc;IAE5B,YAAY,MAAmB;QAC7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,UAAU,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,IAAI,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC;IAClC,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // index.ts — whoop-mcp server
4
+ //
5
+ // Exposes WHOOP data as MCP tools that Claude can call.
6
+ //
7
+ // TOOLS:
8
+ // get_recovery — today's recovery score, HRV, resting HR
9
+ // get_sleep — last night's sleep breakdown
10
+ // get_strain — today's day strain and calories
11
+ // get_latest_workout — most recent workout
12
+ // get_recovery_trend — recovery scores over N days
13
+ // get_sleep_trend — sleep data over N days
14
+ // get_workout_history — recent workouts
15
+ // get_profile — user profile and body measurements
16
+ //
17
+ // AUTH:
18
+ // Set WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET env variables.
19
+ // Run: node dist/auth-setup.js (one-time setup — opens browser, saves tokens)
20
+ // Tokens are auto-refreshed and stored in ~/.whoop-mcp-tokens.json
21
+ //
22
+ // USAGE IN claude_desktop_config.json:
23
+ // {
24
+ // "mcpServers": {
25
+ // "whoop": {
26
+ // "command": "/path/to/whoop-mcp",
27
+ // "env": { "WHOOP_CLIENT_ID": "your_id", "WHOOP_CLIENT_SECRET": "your_secret" }
28
+ // }
29
+ // }
30
+ // }
31
+ // =============================================================================
32
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
33
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
34
+ import { z } from 'zod';
35
+ import { WhoopClient, formatRecovery, formatSleep, formatWorkout, formatCycle, } from './whoop.js';
36
+ // ---- Validate env -----------------------------------------------------------
37
+ const clientId = process.env.WHOOP_CLIENT_ID;
38
+ const clientSecret = process.env.WHOOP_CLIENT_SECRET;
39
+ if (!clientId || !clientSecret) {
40
+ process.stderr.write('[whoop-mcp] Error: WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET are required.\n' +
41
+ '[whoop-mcp] Run: node dist/auth-setup.js to complete one-time setup.\n');
42
+ process.exit(1);
43
+ }
44
+ const client = new WhoopClient({ clientId, clientSecret });
45
+ // ---- MCP Server -------------------------------------------------------------
46
+ const server = new McpServer({
47
+ name: 'whoop',
48
+ version: '1.0.0',
49
+ });
50
+ // Helper — wraps tool handlers with error handling
51
+ function safe(fn) {
52
+ return fn()
53
+ .then(text => ({ content: [{ type: 'text', text }] }))
54
+ .catch(err => ({
55
+ content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
56
+ }));
57
+ }
58
+ // ---- Tools ------------------------------------------------------------------
59
+ // 1. Today's recovery
60
+ server.tool('get_recovery', "Get the user's most recent WHOOP recovery score, HRV, resting heart rate, and SpO2", {}, () => safe(async () => {
61
+ const recovery = await client.getLatestRecovery();
62
+ if (!recovery)
63
+ return 'No recovery data available yet. Make sure your WHOOP is synced.';
64
+ return formatRecovery(recovery);
65
+ }));
66
+ // 2. Last night's sleep
67
+ server.tool('get_sleep', "Get the user's most recent WHOOP sleep data including duration, sleep stages (light, deep/SWS, REM), efficiency, and respiratory rate", {}, () => safe(async () => {
68
+ const sleep = await client.getLatestSleep();
69
+ if (!sleep)
70
+ return 'No sleep data available. Make sure your WHOOP is synced.';
71
+ return formatSleep(sleep);
72
+ }));
73
+ // 3. Today's strain
74
+ server.tool('get_strain', "Get the user's current day strain score, average heart rate, and calories burned from WHOOP", {}, () => safe(async () => {
75
+ const cycle = await client.getLatestCycle();
76
+ if (!cycle)
77
+ return 'No strain data available yet today.';
78
+ return formatCycle(cycle);
79
+ }));
80
+ // 4. Latest workout
81
+ server.tool('get_latest_workout', "Get the user's most recent WHOOP workout — sport type, duration, strain, heart rate zones, and calories", {}, () => safe(async () => {
82
+ const workout = await client.getLatestWorkout();
83
+ if (!workout)
84
+ return 'No recent workouts found.';
85
+ return formatWorkout(workout);
86
+ }));
87
+ // 5. Recovery trend
88
+ server.tool('get_recovery_trend', "Get the user's WHOOP recovery scores over the past N days (default 7) for trend analysis", { days: z.number().min(1).max(25).default(7).describe('Number of days to look back (max 25)') }, ({ days }) => safe(async () => {
89
+ const start = new Date();
90
+ start.setDate(start.getDate() - days);
91
+ const records = await client.getRecoveryCollection(start.toISOString(), undefined, days);
92
+ if (records.length === 0)
93
+ return 'No recovery data found for this period.';
94
+ return records.map(formatRecovery).join('\n\n---\n\n');
95
+ }));
96
+ // 6. Sleep trend
97
+ server.tool('get_sleep_trend', "Get the user's WHOOP sleep data over the past N days (default 7) for trend analysis", { days: z.number().min(1).max(25).default(7).describe('Number of days to look back (max 25)') }, ({ days }) => safe(async () => {
98
+ const start = new Date();
99
+ start.setDate(start.getDate() - days);
100
+ const records = await client.getSleepCollection(start.toISOString(), undefined, days);
101
+ if (records.length === 0)
102
+ return 'No sleep data found for this period.';
103
+ // Filter to main sleeps only (exclude naps) unless there are none
104
+ const mainSleeps = records.filter(s => !s.nap);
105
+ const toShow = mainSleeps.length > 0 ? mainSleeps : records;
106
+ return toShow.map(formatSleep).join('\n\n---\n\n');
107
+ }));
108
+ // 7. Workout history
109
+ server.tool('get_workout_history', "Get the user's recent WHOOP workout history — useful for understanding training load and patterns", { count: z.number().min(1).max(25).default(5).describe('Number of workouts to retrieve (max 25)') }, ({ count }) => safe(async () => {
110
+ const workouts = await client.getWorkoutCollection(undefined, undefined, count);
111
+ if (workouts.length === 0)
112
+ return 'No workouts found.';
113
+ return workouts.map(formatWorkout).join('\n\n---\n\n');
114
+ }));
115
+ // 8. User profile
116
+ server.tool('get_profile', "Get the user's WHOOP profile (name, email) and body measurements (height, weight, max heart rate)", {}, () => safe(async () => {
117
+ const [profile, body] = await Promise.all([
118
+ client.getProfile(),
119
+ client.getBodyMeasurements(),
120
+ ]);
121
+ return [
122
+ `Name: ${profile.first_name} ${profile.last_name}`,
123
+ `Email: ${profile.email}`,
124
+ `Height: ${(body.height_meter * 100).toFixed(0)} cm`,
125
+ `Weight: ${body.weight_kilogram.toFixed(1)} kg`,
126
+ `Max Heart Rate: ${body.max_heart_rate} bpm`,
127
+ ].join('\n');
128
+ }));
129
+ // ---- Start ------------------------------------------------------------------
130
+ const transport = new StdioServerTransport();
131
+ await server.connect(transport);
132
+ process.stderr.write('[whoop-mcp] Server running\n');
133
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,gFAAgF;AAChF,8BAA8B;AAC9B,EAAE;AACF,wDAAwD;AACxD,EAAE;AACF,SAAS;AACT,kEAAkE;AAClE,uDAAuD;AACvD,0DAA0D;AAC1D,8CAA8C;AAC9C,sDAAsD;AACtD,iDAAiD;AACjD,0CAA0C;AAC1C,6DAA6D;AAC7D,EAAE;AACF,QAAQ;AACR,+DAA+D;AAC/D,iFAAiF;AACjF,qEAAqE;AACrE,EAAE;AACF,uCAAuC;AACvC,MAAM;AACN,sBAAsB;AACtB,mBAAmB;AACnB,2CAA2C;AAC3C,wFAAwF;AACxF,UAAU;AACV,QAAQ;AACR,MAAM;AACN,gFAAgF;AAEhF,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,WAAW,EACX,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,gFAAgF;AAEhF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;AAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;AAErD,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,EAAE,CAAC;IAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,4EAA4E;QAC5E,wEAAwE,CACzE,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;AAE3D,gFAAgF;AAEhF,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,mDAAmD;AACnD,SAAS,IAAI,CAAC,EAAyB;IACrC,OAAO,EAAE,EAAE;SACR,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;SAC9D,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;KACzG,CAAC,CAAC,CAAC;AACR,CAAC;AAED,gFAAgF;AAEhF,sBAAsB;AACtB,MAAM,CAAC,IAAI,CACT,cAAc,EACd,oFAAoF,EACpF,EAAE,EACF,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,iBAAiB,EAAE,CAAC;IAClD,IAAI,CAAC,QAAQ;QAAE,OAAO,iEAAiE,CAAC;IACxF,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC,CAAC,CACH,CAAC;AAEF,wBAAwB;AACxB,MAAM,CAAC,IAAI,CACT,WAAW,EACX,uIAAuI,EACvI,EAAE,EACF,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,cAAc,EAAE,CAAC;IAC5C,IAAI,CAAC,KAAK;QAAE,OAAO,0DAA0D,CAAC;IAC9E,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC,CAAC,CACH,CAAC;AAEF,oBAAoB;AACpB,MAAM,CAAC,IAAI,CACT,YAAY,EACZ,6FAA6F,EAC7F,EAAE,EACF,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,cAAc,EAAE,CAAC;IAC5C,IAAI,CAAC,KAAK;QAAE,OAAO,qCAAqC,CAAC;IACzD,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC,CAAC,CACH,CAAC;AAEF,oBAAoB;AACpB,MAAM,CAAC,IAAI,CACT,oBAAoB,EACpB,yGAAyG,EACzG,EAAE,EACF,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,gBAAgB,EAAE,CAAC;IAChD,IAAI,CAAC,OAAO;QAAE,OAAO,2BAA2B,CAAC;IACjD,OAAO,aAAa,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC,CAAC,CACH,CAAC;AAEF,oBAAoB;AACpB,MAAM,CAAC,IAAI,CACT,oBAAoB,EACpB,0FAA0F,EAC1F,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sCAAsC,CAAC,EAAE,EAC/F,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IAC5B,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;IACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACzF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,yCAAyC,CAAC;IAC3E,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC,CAAC,CACH,CAAC;AAEF,iBAAiB;AACjB,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,qFAAqF,EACrF,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sCAAsC,CAAC,EAAE,EAC/F,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IAC5B,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;IACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACtF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,sCAAsC,CAAC;IACxE,kEAAkE;IAClE,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;IAC5D,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AACrD,CAAC,CAAC,CACH,CAAC;AAEF,qBAAqB;AACrB,MAAM,CAAC,IAAI,CACT,qBAAqB,EACrB,mGAAmG,EACnG,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,yCAAyC,CAAC,EAAE,EACnG,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IAC7B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAChF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,oBAAoB,CAAC;IACvD,OAAO,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC,CAAC,CACH,CAAC;AAEF,kBAAkB;AAClB,MAAM,CAAC,IAAI,CACT,aAAa,EACb,mGAAmG,EACnG,EAAE,EACF,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACxC,MAAM,CAAC,UAAU,EAAE;QACnB,MAAM,CAAC,mBAAmB,EAAE;KAC7B,CAAC,CAAC;IACH,OAAO;QACL,SAAS,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,SAAS,EAAE;QAClD,UAAU,OAAO,CAAC,KAAK,EAAE;QACzB,WAAW,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;QACpD,WAAW,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;QAC/C,mBAAmB,IAAI,CAAC,cAAc,MAAM;KAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC,CAAC,CACH,CAAC;AAEF,gFAAgF;AAEhF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC"}
@@ -0,0 +1,135 @@
1
+ import { OAuthConfig } from './auth.js';
2
+ export interface WhoopProfile {
3
+ user_id: number;
4
+ email: string;
5
+ first_name: string;
6
+ last_name: string;
7
+ }
8
+ export interface WhoopBodyMeasurements {
9
+ height_meter: number;
10
+ weight_kilogram: number;
11
+ max_heart_rate: number;
12
+ }
13
+ export interface WhoopCycleScore {
14
+ strain: number;
15
+ kilojoule: number;
16
+ average_heart_rate: number;
17
+ max_heart_rate: number;
18
+ }
19
+ export interface WhoopCycle {
20
+ id: number;
21
+ user_id: number;
22
+ created_at: string;
23
+ updated_at: string;
24
+ start: string;
25
+ end: string | null;
26
+ timezone_offset: string;
27
+ score_state: 'SCORED' | 'PENDING_SCORE' | 'UNSCORABLE';
28
+ score: WhoopCycleScore | null;
29
+ }
30
+ export interface WhoopRecoveryScore {
31
+ user_calibrating: boolean;
32
+ recovery_score: number;
33
+ resting_heart_rate: number;
34
+ hrv_rmssd_milli: number;
35
+ spo2_percentage: number | null;
36
+ skin_temp_celsius: number | null;
37
+ }
38
+ export interface WhoopRecovery {
39
+ cycle_id: number;
40
+ sleep_id: number;
41
+ user_id: number;
42
+ created_at: string;
43
+ updated_at: string;
44
+ score_state: 'SCORED' | 'PENDING_SCORE' | 'UNSCORABLE';
45
+ score: WhoopRecoveryScore | null;
46
+ }
47
+ export interface WhoopSleepScore {
48
+ stage_summary: {
49
+ total_in_bed_time_milli: number;
50
+ total_awake_time_milli: number;
51
+ total_no_data_time_milli: number;
52
+ total_light_sleep_time_milli: number;
53
+ total_slow_wave_sleep_time_milli: number;
54
+ total_rem_sleep_time_milli: number;
55
+ sleep_cycle_count: number;
56
+ disturbance_count: number;
57
+ };
58
+ sleep_needed: {
59
+ baseline_milli: number;
60
+ need_from_sleep_debt_milli: number;
61
+ need_from_recent_strain_milli: number;
62
+ need_from_recent_nap_milli: number;
63
+ };
64
+ respiratory_rate: number | null;
65
+ sleep_performance_percentage: number | null;
66
+ sleep_consistency_percentage: number | null;
67
+ sleep_efficiency_percentage: number | null;
68
+ }
69
+ export interface WhoopSleep {
70
+ id: number;
71
+ user_id: number;
72
+ created_at: string;
73
+ updated_at: string;
74
+ start: string;
75
+ end: string;
76
+ timezone_offset: string;
77
+ nap: boolean;
78
+ score_state: 'SCORED' | 'PENDING_SCORE' | 'UNSCORABLE';
79
+ score: WhoopSleepScore | null;
80
+ }
81
+ export interface WhoopWorkoutScore {
82
+ strain: number;
83
+ average_heart_rate: number;
84
+ max_heart_rate: number;
85
+ kilojoule: number;
86
+ percent_recorded: number;
87
+ distance_meter: number | null;
88
+ altitude_gain_meter: number | null;
89
+ altitude_change_meter: number | null;
90
+ zone_duration: {
91
+ zone_zero_milli: number | null;
92
+ zone_one_milli: number | null;
93
+ zone_two_milli: number | null;
94
+ zone_three_milli: number | null;
95
+ zone_four_milli: number | null;
96
+ zone_five_milli: number | null;
97
+ };
98
+ }
99
+ export interface WhoopWorkout {
100
+ id: number;
101
+ user_id: number;
102
+ created_at: string;
103
+ updated_at: string;
104
+ start: string;
105
+ end: string;
106
+ timezone_offset: string;
107
+ sport_id: number;
108
+ score_state: 'SCORED' | 'PENDING_SCORE' | 'UNSCORABLE';
109
+ score: WhoopWorkoutScore | null;
110
+ }
111
+ export interface PaginatedResponse<T> {
112
+ records: T[];
113
+ next_token: string | null;
114
+ }
115
+ export declare const SPORT_NAMES: Record<number, string>;
116
+ export declare class WhoopClient {
117
+ private tokenManager;
118
+ constructor(config: OAuthConfig);
119
+ private request;
120
+ getProfile(): Promise<WhoopProfile>;
121
+ getBodyMeasurements(): Promise<WhoopBodyMeasurements>;
122
+ getLatestRecovery(): Promise<WhoopRecovery | null>;
123
+ getRecoveryCollection(start?: string, end?: string, limit?: number): Promise<WhoopRecovery[]>;
124
+ getLatestSleep(): Promise<WhoopSleep | null>;
125
+ getSleepCollection(start?: string, end?: string, limit?: number): Promise<WhoopSleep[]>;
126
+ getLatestCycle(): Promise<WhoopCycle | null>;
127
+ getCycleCollection(start?: string, end?: string, limit?: number): Promise<WhoopCycle[]>;
128
+ getLatestWorkout(): Promise<WhoopWorkout | null>;
129
+ getWorkoutCollection(start?: string, end?: string, limit?: number): Promise<WhoopWorkout[]>;
130
+ }
131
+ export declare function millisToHours(ms: number): string;
132
+ export declare function formatRecovery(r: WhoopRecovery): string;
133
+ export declare function formatSleep(s: WhoopSleep): string;
134
+ export declare function formatWorkout(w: WhoopWorkout): string;
135
+ export declare function formatCycle(c: WhoopCycle): string;
package/dist/whoop.js ADDED
@@ -0,0 +1,204 @@
1
+ // =============================================================================
2
+ // whoop.ts — WHOOP API client
3
+ //
4
+ // Wraps all WHOOP v2 REST API endpoints.
5
+ // Auth: OAuth2 with automatic token refresh via TokenManager
6
+ // Base URL: https://api.prod.whoop.com/developer
7
+ //
8
+ // API docs: https://developer.whoop.com/api
9
+ // =============================================================================
10
+ import { TokenManager } from './auth.js';
11
+ const BASE_URL = 'https://api.prod.whoop.com/developer';
12
+ // ---- Sport ID map -----------------------------------------------------------
13
+ export const SPORT_NAMES = {
14
+ '-1': 'Activity',
15
+ 0: 'Running', 1: 'Cycling', 16: 'Baseball', 17: 'Basketball',
16
+ 18: 'Rowing', 19: 'Fencing', 20: 'Field Hockey', 21: 'Football',
17
+ 22: 'Golf', 24: 'Ice Hockey', 25: 'Lacrosse', 27: 'Rugby',
18
+ 28: 'Sailing', 29: 'Skiing', 30: 'Soccer', 31: 'Softball',
19
+ 32: 'Squash', 33: 'Swimming', 34: 'Tennis', 35: 'Track & Field',
20
+ 36: 'Volleyball', 37: 'Water Polo', 38: 'Wrestling', 39: 'Boxing',
21
+ 42: 'Dance', 43: 'Pilates', 44: 'Yoga', 45: 'Weightlifting',
22
+ 47: 'Cross Country Skiing', 48: 'Functional Fitness', 49: 'Duathlon',
23
+ 51: 'Gymnastics', 52: 'Hiking', 53: 'Horseback Riding', 55: 'Kayaking',
24
+ 56: 'Martial Arts', 57: 'Mountain Biking', 59: 'Obstacle Course Racing',
25
+ 60: 'Olympic Weightlifting', 61: 'Paddle Tennis', 62: 'Motorcycling',
26
+ 63: 'Powerlifting', 64: 'Rock Climbing', 65: 'Paddleboarding',
27
+ 66: 'Triathlon', 67: 'Walking', 68: 'Surfing', 69: 'Elliptical',
28
+ 70: 'Stairmaster', 71: 'Meditation', 73: 'Other', 74: 'Diving',
29
+ 75: 'Operations - Tactical', 76: 'Operations - Medical',
30
+ 77: 'Operations - Flying', 78: 'Operations - Water',
31
+ 82: 'Commuting', 83: 'Gaming', 84: 'Snowboarding', 85: 'Motocross',
32
+ 86: 'Caddying', 87: 'Obstacle Racing', 88: 'Motor Racing', 89: 'HIIT',
33
+ 90: 'Spin', 91: 'Jiu Jitsu', 92: 'Manual Labor', 93: 'Cricket',
34
+ 94: 'Pickleball', 95: 'Inline Skating', 96: 'Box Fitness', 97: 'Spikeball',
35
+ 98: 'Wheelchair Pushing', 99: 'Paddle Sports', 100: 'Barre',
36
+ 101: 'Stage Performance', 102: 'High Stress Work', 103: 'Ice Bath',
37
+ 104: 'Coaching', 105: 'Ice Skating', 106: 'Cross-training', 107: 'Parkour',
38
+ 108: 'Badminton', 109: 'Table Tennis', 110: 'Racquetball', 111: 'Jump Rope',
39
+ 112: 'Australian Football', 113: 'Skateboarding', 114: 'Coaching Sports',
40
+ 115: 'Sauna', 116: 'Disc Golf', 117: 'Ultimate Frisbee', 118: 'Snorkeling',
41
+ 119: 'Esports', 121: 'Pickleball',
42
+ };
43
+ // ---- API Client -------------------------------------------------------------
44
+ export class WhoopClient {
45
+ tokenManager;
46
+ constructor(config) {
47
+ this.tokenManager = new TokenManager(config);
48
+ }
49
+ async request(path, params) {
50
+ // Always get a fresh (auto-refreshed if needed) token
51
+ const token = await this.tokenManager.getAccessToken();
52
+ const url = new URL(`${BASE_URL}${path}`);
53
+ if (params) {
54
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
55
+ }
56
+ const response = await fetch(url.toString(), {
57
+ headers: {
58
+ Authorization: `Bearer ${token}`,
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ });
62
+ if (!response.ok) {
63
+ const body = await response.text();
64
+ throw new Error(`WHOOP API error ${response.status}: ${body}`);
65
+ }
66
+ return response.json();
67
+ }
68
+ // User
69
+ async getProfile() {
70
+ return this.request('/v2/user/profile/basic');
71
+ }
72
+ async getBodyMeasurements() {
73
+ return this.request('/v2/user/measurement/body');
74
+ }
75
+ // Recovery
76
+ async getLatestRecovery() {
77
+ const data = await this.request('/v2/recovery', { limit: '1' });
78
+ return data.records[0] ?? null;
79
+ }
80
+ async getRecoveryCollection(start, end, limit = 7) {
81
+ const params = { limit: String(limit) };
82
+ if (start)
83
+ params.start = start;
84
+ if (end)
85
+ params.end = end;
86
+ const data = await this.request('/v2/recovery', params);
87
+ return data.records;
88
+ }
89
+ // Sleep
90
+ async getLatestSleep() {
91
+ const data = await this.request('/v2/activity/sleep', { limit: '1' });
92
+ const mainSleeps = data.records.filter(s => !s.nap);
93
+ return mainSleeps[0] ?? data.records[0] ?? null;
94
+ }
95
+ async getSleepCollection(start, end, limit = 7) {
96
+ const params = { limit: String(Math.min(limit, 25)) };
97
+ if (start)
98
+ params.start = start;
99
+ if (end)
100
+ params.end = end;
101
+ const data = await this.request('/v2/activity/sleep', params);
102
+ return data.records;
103
+ }
104
+ // Cycles
105
+ async getLatestCycle() {
106
+ const data = await this.request('/v2/cycle', { limit: '1' });
107
+ return data.records[0] ?? null;
108
+ }
109
+ async getCycleCollection(start, end, limit = 7) {
110
+ const params = { limit: String(Math.min(limit, 25)) };
111
+ if (start)
112
+ params.start = start;
113
+ if (end)
114
+ params.end = end;
115
+ const data = await this.request('/v2/cycle', params);
116
+ return data.records;
117
+ }
118
+ // Workouts
119
+ async getLatestWorkout() {
120
+ const data = await this.request('/v2/activity/workout', { limit: '1' });
121
+ return data.records[0] ?? null;
122
+ }
123
+ async getWorkoutCollection(start, end, limit = 10) {
124
+ const params = { limit: String(Math.min(limit, 25)) };
125
+ if (start)
126
+ params.start = start;
127
+ if (end)
128
+ params.end = end;
129
+ const data = await this.request('/v2/activity/workout', params);
130
+ return data.records;
131
+ }
132
+ }
133
+ // ---- Formatting helpers -----------------------------------------------------
134
+ export function millisToHours(ms) {
135
+ const h = Math.floor(ms / 3600000);
136
+ const m = Math.floor((ms % 3600000) / 60000);
137
+ return `${h}h ${m}m`;
138
+ }
139
+ export function formatRecovery(r) {
140
+ if (r.score_state !== 'SCORED' || !r.score) {
141
+ return `Recovery for cycle ${r.cycle_id}: ${r.score_state}`;
142
+ }
143
+ const s = r.score;
144
+ const zone = s.recovery_score >= 67 ? '🟢 Green' : s.recovery_score >= 34 ? '🟡 Yellow' : '🔴 Red';
145
+ return [
146
+ `Recovery Score: ${s.recovery_score}/100 (${zone})`,
147
+ `HRV: ${s.hrv_rmssd_milli.toFixed(1)}ms`,
148
+ `Resting Heart Rate: ${s.resting_heart_rate} bpm`,
149
+ s.spo2_percentage ? `SpO2: ${s.spo2_percentage.toFixed(1)}%` : null,
150
+ s.skin_temp_celsius ? `Skin Temp: ${s.skin_temp_celsius.toFixed(1)}°C` : null,
151
+ `Date: ${new Date(r.created_at).toLocaleDateString()}`,
152
+ ].filter(Boolean).join('\n');
153
+ }
154
+ export function formatSleep(s) {
155
+ if (s.score_state !== 'SCORED' || !s.score) {
156
+ return `Sleep on ${new Date(s.start).toLocaleDateString()}: ${s.score_state}`;
157
+ }
158
+ const sc = s.score;
159
+ const ss = sc.stage_summary;
160
+ return [
161
+ `Sleep: ${new Date(s.start).toLocaleDateString()} (${s.nap ? 'Nap' : 'Main sleep'})`,
162
+ `Performance: ${sc.sleep_performance_percentage?.toFixed(0) ?? 'N/A'}%`,
163
+ `Efficiency: ${sc.sleep_efficiency_percentage?.toFixed(0) ?? 'N/A'}%`,
164
+ `Total in bed: ${millisToHours(ss.total_in_bed_time_milli)}`,
165
+ `Awake: ${millisToHours(ss.total_awake_time_milli)}`,
166
+ `Light sleep: ${millisToHours(ss.total_light_sleep_time_milli)}`,
167
+ `Deep sleep (SWS): ${millisToHours(ss.total_slow_wave_sleep_time_milli)}`,
168
+ `REM sleep: ${millisToHours(ss.total_rem_sleep_time_milli)}`,
169
+ `Disturbances: ${ss.disturbance_count}`,
170
+ sc.respiratory_rate ? `Respiratory rate: ${sc.respiratory_rate.toFixed(1)} breaths/min` : null,
171
+ ].filter(Boolean).join('\n');
172
+ }
173
+ export function formatWorkout(w) {
174
+ const sportName = SPORT_NAMES[w.sport_id] ?? `Sport #${w.sport_id}`;
175
+ if (w.score_state !== 'SCORED' || !w.score) {
176
+ return `${sportName} on ${new Date(w.start).toLocaleDateString()}: ${w.score_state}`;
177
+ }
178
+ const s = w.score;
179
+ const duration = (new Date(w.end).getTime() - new Date(w.start).getTime()) / 60000;
180
+ return [
181
+ `Workout: ${sportName}`,
182
+ `Date: ${new Date(w.start).toLocaleDateString()}`,
183
+ `Duration: ${Math.round(duration)} min`,
184
+ `Strain: ${s.strain.toFixed(1)}/21`,
185
+ `Avg HR: ${s.average_heart_rate} bpm`,
186
+ `Max HR: ${s.max_heart_rate} bpm`,
187
+ `Calories: ${(s.kilojoule / 4.184).toFixed(0)} kcal`,
188
+ s.distance_meter ? `Distance: ${(s.distance_meter / 1000).toFixed(2)} km` : null,
189
+ ].filter(Boolean).join('\n');
190
+ }
191
+ export function formatCycle(c) {
192
+ if (c.score_state !== 'SCORED' || !c.score) {
193
+ return `Cycle ${new Date(c.start).toLocaleDateString()}: ${c.score_state}`;
194
+ }
195
+ const s = c.score;
196
+ return [
197
+ `Day Strain: ${s.strain.toFixed(1)}/21`,
198
+ `Avg HR: ${s.average_heart_rate} bpm`,
199
+ `Max HR: ${s.max_heart_rate} bpm`,
200
+ `Calories: ${(s.kilojoule / 4.184).toFixed(0)} kcal`,
201
+ `Date: ${new Date(c.start).toLocaleDateString()}`,
202
+ ].join('\n');
203
+ }
204
+ //# sourceMappingURL=whoop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoop.js","sourceRoot":"","sources":["../src/whoop.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@souravpn/whoop-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for WHOOP — gives Claude access to your recovery, sleep, strain, and workout data",
5
+ "keywords": [
6
+ "mcp",
7
+ "whoop",
8
+ "health",
9
+ "fitness",
10
+ "claude",
11
+ "ai"
12
+ ],
13
+ "author": "Sourav Nayak",
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "bin": {
17
+ "whoop-mcp": "dist/index.js",
18
+ "whoop-mcp-auth-setup": "dist/auth-setup.js"
19
+ },
20
+ "main": "./dist/index.js",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "dev": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "prepublishOnly": "npm run build",
29
+ "auth-setup": "node dist/auth-setup.js"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.10.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "typescript": "^5.4.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ }
41
+ }