@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 +196 -0
- package/dist/auth-setup.d.ts +2 -0
- package/dist/auth-setup.js +152 -0
- package/dist/auth-setup.js.map +1 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +98 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/whoop.d.ts +135 -0
- package/dist/whoop.js +204 -0
- package/dist/whoop.js.map +1 -0
- package/package.json +41 -0
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,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":";AACA,gFAAgF;AAChF,kDAAkD;AAClD,EAAE;AACF,6DAA6D;AAC7D,+DAA+D;AAC/D,EAAE;AACF,SAAS;AACT,4BAA4B;AAC5B,EAAE;AACF,gBAAgB;AAChB,8DAA8D;AAC9D,+CAA+C;AAC/C,6DAA6D;AAC7D,gDAAgD;AAChD,gEAAgE;AAChE,gDAAgD;AAChD,gDAAgD;AAChD,6CAA6C;AAC7C,EAAE;AACF,uCAAuC;AACvC,mDAAmD;AACnD,uCAAuC;AACvC,yBAAyB;AACzB,yCAAyC;AACzC,sDAAsD;AACtD,kDAAkD;AAClD,4CAA4C;AAC5C,gFAAgF;AAChF,gFAAgF;AAEhF,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,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,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;IAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,YAAY,GAAG,gCAAgC,CAAC;AACtD,MAAM,QAAQ,GAAG,8CAA8C,CAAC;AAChE,MAAM,SAAS,GAAG,+CAA+C,CAAC;AAClE,MAAM,MAAM,GAAG,8FAA8F,CAAC;AAE9G,qDAAqD;AACrD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAE1D,8BAA8B;AAC9B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;AAClC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;AAClD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAChD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;AACvD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AAC1C,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAEzC,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;AAC3D,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;AACnD,OAAO,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC1C,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;AACxD,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;AAClE,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAE9C,6CAA6C;AAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAC7C,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAE5C,IAAI,KAAK,EAAE,CAAC;QACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,GAAG,CAAC,mCAAmC,KAAK,MAAM,CAAC,CAAC;QACxD,OAAO,CAAC,KAAK,CAAC,yBAAyB,KAAK,EAAE,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,aAAa,KAAK,KAAK,EAAE,CAAC;QAC5B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;QAC1D,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAChC,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,2BAA2B;IAC3B,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;IAEtE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;YAC/B,UAAU,EAAE,oBAAoB;YAChC,IAAI;YACJ,SAAS,EAAE,QAAQ;YACnB,aAAa,EAAE,YAAY;YAC3B,YAAY,EAAE,YAAY;SAC3B,CAAC,CAAC;QAEH,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;YAC3C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;YAChE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;SACtB,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;YACtB,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,0BAA0B,aAAa,CAAC,MAAM,MAAM,IAAI,EAAE,CAAC,CAAC;QAC9E,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,EAIpC,CAAC;QAEF,UAAU,CAAC;YACT,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,IAAI;SAChD,CAAC,CAAC;QAEH,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,GAAG,CAAC;;;;KAIP,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC,CAAC;QACrE,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAC;QAChE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;YACzB,UAAU,EAAE;gBACV,KAAK,EAAE;oBACL,OAAO,EAAE,WAAW,EAAG,eAAe;oBACtC,GAAG,EAAE;wBACH,eAAe,EAAE,QAAQ;wBACzB,mBAAmB,EAAE,YAAY;qBAClC;iBACF;aACF;SACF,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACb,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC;IAExF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,GAAG,CAAC,GAAG,CAAC,oBAAoB,GAAG,MAAM,CAAC,CAAC;QACvC,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACvB,wCAAwC;IACxC,MAAM,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE;QACxC,IAAI,CAAC,SAAS,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;QACZ,kCAAkC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
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
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
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":"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"}
|
package/dist/whoop.d.ts
ADDED
|
@@ -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
|
+
}
|