Skip to main content

MCP Server

The CLI includes an MCP (Model Context Protocol) server that exposes WHOOP data to AI assistants like Claude.

What is MCP?​

MCP is Anthropic's protocol for connecting AI assistants to external tools and data sources. It provides:

  • Standardized tool definitions
  • Request/response handling
  • Multiple transport options

Learn more at modelcontextprotocol.io.

Starting the Server​

STDIO Transport (Default)​

For Claude Desktop integration:

wellpipe serve

The server communicates via stdin/stdout, which is what Claude Desktop expects.

HTTP Transport​

For custom integrations:

wellpipe serve --transport http --port 3000

The server exposes an HTTP endpoint at http://localhost:3000/mcp.

Claude Desktop Configuration​

Add Wellpipe to your Claude Desktop config:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

{
"mcpServers": {
"wellpipe": {
"command": "npx",
"args": ["@wellpipe/cli", "serve"],
"env": {
"WHOOP_ACCESS_TOKEN": "your-access-token",
"WHOOP_REFRESH_TOKEN": "your-refresh-token"
}
}
}
}

Or using a config file:

{
"mcpServers": {
"wellpipe": {
"command": "npx",
"args": ["@wellpipe/cli", "serve"],
"cwd": "/path/to/your/.env/directory"
}
}
}

Server Architecture​

┌────────────────────────────────────────────────────────────┐
│ MCP Server │
├────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌────────────────────────────────┐ │
│ │ Transport │◀──▶│ Tool Registry │ │
│ │ (stdio/http)│ │ │ │
│ └──────────────┘ │ ┌──────────────────────────┐ │ │
│ │ │ whoop-get-sleep-* │ │ │
│ ┌──────────────┐ │ ├──────────────────────────┤ │ │
│ │ Token │◀──▶│ │ whoop-get-recovery-* │ │ │
│ │ Provider │ │ ├──────────────────────────┤ │ │
│ └──────────────┘ │ │ whoop-get-workout-* │ │ │
│ │ ├──────────────────────────┤ │ │
│ ┌──────────────┐ │ │ garmin-get-sleep-* │ │ │
│ │ API Clients │◀──▶│ ├──────────────────────────┤ │ │
│ │ WHOOP/Garmin │ │ │ garmin-get-hrv-* │ │ │
│ └──────────────┘ │ └──────────────────────────┘ │ │
│ └────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘

Implementation​

Server Setup​

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { WhoopClient, getAllTools } from '@wellpipe/whoop';

export async function createServer(tokenProvider: TokenProvider) {
const client = new WhoopClient(tokenProvider);
const tools = getAllTools();

const server = new Server(
{ name: 'wellpipe', version: '1.0.0' },
{ capabilities: { tools: {} } }
);

// Register tool list handler
server.setRequestHandler('tools/list', async () => ({
tools: tools.map(t => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
})),
}));

// Register tool call handler
server.setRequestHandler('tools/call', async (request) => {
const tool = tools.find(t => t.name === request.params.name);
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}

const result = await tool.handler(request.params.arguments, client);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
});

return server;
}

Token Provider​

The CLI uses an environment-based token provider:

import dotenv from 'dotenv';
import fs from 'fs';

class EnvTokenProvider implements TokenProvider {
private envPath: string;

constructor(envPath: string = '.env') {
this.envPath = envPath;
dotenv.config({ path: envPath });
}

async getAccessToken(): Promise<string> {
if (await this.isExpired()) {
await this.refresh();
}
return process.env.WHOOP_ACCESS_TOKEN!;
}

async getRefreshToken(): Promise<string | undefined> {
return process.env.WHOOP_REFRESH_TOKEN;
}

async updateTokens(
accessToken: string,
refreshToken?: string,
expiresAt?: Date
): Promise<void> {
// Update environment variables
process.env.WHOOP_ACCESS_TOKEN = accessToken;
if (refreshToken) process.env.WHOOP_REFRESH_TOKEN = refreshToken;
if (expiresAt) process.env.WHOOP_TOKEN_EXPIRES_AT = expiresAt.toISOString();

// Persist to .env file
const content = `
WHOOP_ACCESS_TOKEN=${accessToken}
WHOOP_REFRESH_TOKEN=${refreshToken || ''}
WHOOP_TOKEN_EXPIRES_AT=${expiresAt?.toISOString() || ''}
`.trim();

fs.writeFileSync(this.envPath, content);
}

async isExpired(): Promise<boolean> {
const expiresAt = process.env.WHOOP_TOKEN_EXPIRES_AT;
if (!expiresAt) return false;

const expiry = new Date(expiresAt);
const buffer = 5 * 60 * 1000; // 5 minutes
return Date.now() >= expiry.getTime() - buffer;
}

private async refresh(): Promise<void> {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}

// Call WHOOP token endpoint
const response = await fetch('https://api.prod.whoop.com/oauth/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.WHOOP_CLIENT_ID!,
refresh_token: refreshToken,
}),
});

const tokens = await response.json();
await this.updateTokens(
tokens.access_token,
tokens.refresh_token,
new Date(Date.now() + tokens.expires_in * 1000)
);
}
}

Environment Variables​

The server reads these environment variables:

VariableRequiredDescription
WHOOP_ACCESS_TOKENYesCurrent access token
WHOOP_REFRESH_TOKENYesRefresh token for renewal
WHOOP_TOKEN_EXPIRES_ATNoToken expiration timestamp
WHOOP_CLIENT_IDFor refreshDeveloper app client ID

Debugging​

Verbose Logging​

DEBUG=wellpipe:* wellpipe serve

Test Tool Calls​

With HTTP transport, you can test tools directly:

curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "whoop-get-recent-sleep",
"arguments": { "days": 7 }
},
"id": 1
}'

Check Available Tools​

curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": 1
}'

Error Handling​

The server returns structured errors:

{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "WHOOP API error",
"data": {
"status": 401,
"detail": "Token expired"
}
},
"id": 1
}

Common error codes:

CodeMeaning
-32600Invalid request
-32601Unknown tool
-32602Invalid parameters
-32000Provider error