Skip to main content
WHOOP health data integration (sleep, recovery, workouts). Demonstrates wrapping an existing third-party OAuth API and designing tools that minimize latency. GitHub: poke-mcp-examples/whoop-integration Authentication: OAuth Proxy Who should follow this: Developers wrapping existing OAuth APIs (GitHub, Strava, Spotify), learning how to bridge third-party auth, or optimizing tool design for conversational AI.
Reference implementation by the Poke team. See Tool Design Philosophy for detailed analysis.

What to Notice

1. OAuth Proxy to Existing Provider

The problem: WHOOP has OAuth, but it doesn’t support Dynamic Client Registration. You need to manually create an OAuth app, get credentials, manage client registration. OAuth Proxy solution: Your server bridges MCP clients to WHOOP’s OAuth:
from fastmcp.server.auth import OAuthProxy

auth = OAuthProxy(
    # WHOOP's OAuth endpoints
    upstream_authorization_endpoint="https://api.whoop.com/oauth/oauth2/auth",
    upstream_token_endpoint="https://api.whoop.com/oauth/oauth2/token",

    # Your WHOOP OAuth app credentials (created once in WHOOP dashboard)
    upstream_client_id=os.environ["WHOOP_CLIENT_ID"],
    upstream_client_secret=os.environ["WHOOP_CLIENT_SECRET"],

    # Forward PKCE through the proxy
    forward_pkce=True,

    # Custom token verification (WHOOP uses opaque tokens)
    token_verifier=WhoopTokenVerifier(required_scopes=REQUIRED_SCOPES),

    base_url=os.environ["PUBLIC_BASE_URL"]
)

mcp = FastMCP("WHOOP Integration", auth=auth)
What happens:
1. Poke → Your server: Discovers OAuth via /.well-known endpoints
2. Your server → Poke: "Use WHOOP's OAuth" (proxied through your server)
3. User authorizes with WHOOP
4. Your server validates tokens, forwards to WHOOP API with user's token
Why: You create one OAuth app in WHOOP’s dashboard. Your server exposes MCP-compatible OAuth to clients. All MCP clients connect through your proxy using that one app. Token verification with caching:
class WhoopTokenVerifier(TokenVerifier):
    def __init__(self, cache_ttl_s: int = 300):
        self._cache = {}  # {token: (expires_at, data)}

    async def verify_token(self, token: str) -> Optional[AccessToken]:
        # Check cache first (5-minute TTL)
        if token in self._cache and time.time() < self._cache[token][0]:
            return AccessToken(token=token, client_id="whoop", scopes=SCOPES)

        # Validate by calling WHOOP's profile API
        response = await client.get(
            f"{WHOOP_BASE}/v2/user/profile/basic",
            headers={"Authorization": f"Bearer {token}"}
        )

        if response.status_code != 200:
            return None

        self._cache[token] = (time.time() + self._cache_ttl_s, response.json())
        return AccessToken(token=token, client_id="whoop", scopes=SCOPES)
Why cache: WHOOP uses opaque tokens (not JWTs). We validate by calling their API. Caching prevents hitting WHOOP on every single tool call.

2. Tool Design: Combine API Calls to Minimize Latency

WHOOP has 12+ API endpoints. Users ask complete questions: “How did I sleep last night?” That needs recovery + sleep + cycles + workouts. Don’t make the agent call 4 tools in a loop - combine them:
@mcp.tool
async def get_daily_update() -> dict[str, Any]:
    """Latest recovery, last completed sleep, recent cycles, and today's workouts."""

    # Fetch all 4 API endpoints in parallel
    recovery = await whoop_api.get("/v2/recovery", {"limit": 1})
    sleep = await whoop_api.get("/v2/activity/sleep", {"limit": 1})
    cycles = await whoop_api.get("/v2/cycle", {"start": days_ago(2)})
    workouts = await whoop_api.get("/v2/activity/workout", {"start": start_of_day()})

    return {
        "recovery": recovery["records"][0],
        "sleep": sleep["records"][0],
        "recent_cycles": cycles["records"],
        "today_workouts": workouts["records"]
    }
Why: One tool call answers the complete question. Agent gets everything in one shot. 4x faster than calling 4 separate tools.

3. Anticipate Follow-Up Questions

Comparison questions need both periods. Pre-package them:
@mcp.tool
async def get_trends(period: Literal["week", "month"] = "week") -> dict[str, Any]:
    """Compare current period to previous period."""

    # Calculate both time windows
    current_start, current_end = calculate_current_period(period)
    prev_start, prev_end = calculate_previous_period(period)

    # Fetch both in parallel
    current_data = await fetch_all_activities(current_start, current_end)
    previous_data = await fetch_all_activities(prev_start, prev_end)

    return {
        "current": {"window": {...}, **current_data},
        "previous": {"window": {...}, **previous_data}
    }
Why: Agent doesn’t need to call twice for “how’s my recovery this week vs last week?” One call, both periods, immediate comparison.

Quick Start

# Clone
git clone https://github.com/InteractionCo/poke-mcp-examples.git
cd poke-mcp-examples/whoop-integration

# Environment
conda create -n whoop-integration python=3.12
conda activate whoop-integration

# Install
pip install -r requirements.txt

# Configure (requires creating WHOOP OAuth app - see repo README)
cp .env.example .env
# Edit .env with WHOOP_CLIENT_ID and WHOOP_CLIENT_SECRET

# Run
python src/server.py
Deployment: See repo README for WHOOP app setup and Render deployment.

Key Takeaway

Wrapping an existing OAuth API? Use OAuth Proxy. Create one OAuth app in the provider’s dashboard, configure OAuthProxy, your server bridges MCP clients to their OAuth. Tool design lesson: Combine related API calls into single tools - agent calls once, not in loops. Faster, more natural responses.