#
tokens: 6914/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── config
│   └── .env.example
├── pyproject.toml
├── README.md
├── requirements.txt
├── run_server.sh
├── run_whoop_server.sh
└── src
    ├── whoop_http_server.py
    └── whoop_server.py
```

# Files

--------------------------------------------------------------------------------
/config/.env.example:
--------------------------------------------------------------------------------

```
WHOOP_CLIENT_ID=your_client_id_here
WHOOP_CLIENT_SECRET=your_client_secret_here
WHOOP_REDIRECT_URI=http://localhost:8000/callback 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Environment files
.env
.env.*
!.env.example
config/.env
config/.env.*
!config/.env.example

# Virtual Environment
venv/
ENV/
env/

# IDE specific files
.idea/
.vscode/
*.swp
*.swo

# OS specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local configuration
.local/ 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Whoop MCP Server
Python Package License: MIT Python 3.12

A Model Context Protocol (MCP) server that provides access to the Whoop API. It allows language models to query cycles, recovery, strain, and workout data from the Whoop API.

## Available Tools

The server exposes the following tools:

### Cycle Queries
- `get_cycle_collection(start_date: str, end_date: str)`: Get cycle data for a specific date range
- `get_latest_cycle()`: Get the most recent cycle data

### Recovery and Strain
- `get_recovery_data(start_date: str, end_date: str)`: Get recovery data for a specific date range
- `get_strain_data(start_date: str, end_date: str)`: Get strain data for a specific date range
- `get_average_strain(days: int = 7)`: Calculate average strain over specified number of days

### Profile and Authentication
- `get_profile()`: Get user profile information
- `check_auth_status()`: Check authentication status with Whoop API

Dates should be provided in ISO format (YYYY-MM-DD).

## Usage

You'll need Whoop credentials to use this server. The server uses email/password authentication with the Whoop API.

### Claude for Desktop

Update your `claude_desktop_config.json` (located in `~/Library/Application\ Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%/Claude/claude_desktop_config.json` on Windows) to include the following:

```json
{
    "mcpServers": {
        "Whoop": {
            "command": "python",
            "args": ["/path/to/whoop/src/whoop_server.py"],
            "cwd": "/path/to/whoop",
            "env": {
                "WHOOP_EMAIL": "[email protected]",
                "WHOOP_PASSWORD": "your_password"
            }
        }
    }
}
```

### HTTP API Server

The project also includes an HTTP API server that exposes the same functionality over HTTP endpoints. To run it:

```bash
./run_whoop_server.sh
```

## Example Queries

Once connected, you can ask Claude questions like:

- "What's my recovery score for today?"
- "Show me my strain data for the past week"
- "What's my average strain over the last 7 days?"
- "Get my latest cycle data"

## Error Handling

The server provides human-readable error messages for common issues:
- Invalid date formats
- API authentication errors
- Network connectivity problems
- Missing or invalid credentials

## Project Structure

```
whoop/
├── src/
│   ├── whoop_server.py      # MCP server implementation
│   └── whoop_http_server.py # HTTP API server implementation
├── config/
│   └── .env                 # Environment variables
├── requirements.txt         # Python dependencies
└── run_whoop_server.sh     # Script to run HTTP server
```

## License

This project is licensed under the MIT License - see the LICENSE file for details. 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
git+https://github.com/modelcontextprotocol/python-sdk.git
fastapi>=0.68.0
uvicorn>=0.15.0
requests>=2.26.0
python-dotenv>=0.19.0
pydantic>=1.8.2 
```

--------------------------------------------------------------------------------
/run_whoop_server.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Change to the script directory
cd "$SCRIPT_DIR"

# Activate the virtual environment
source "$SCRIPT_DIR/venv/bin/activate"
echo "Activated virtual environment" >&2

# Run the Whoop server
python src/whoop_http_server.py 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "whoop-mcp-server"
version = "0.1.0"
description = "MCP server for Whoop API integration"
requires-python = ">=3.8"
dependencies = [
    "whoop",
    "python-dotenv",
    "fastapi",
    "uvicorn",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
whoop-mcp-server = "whoop_server:main"

[tool.hatch.build.targets.wheel]
packages = ["src"] 
```

--------------------------------------------------------------------------------
/run_server.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash
# Script to start the Whoop MCP server

# Get the absolute path of the script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Script directory: $SCRIPT_DIR" >&2

# Change to the project directory
cd "$SCRIPT_DIR"
echo "Changed to directory: $(pwd)" >&2

# Activate the virtual environment
source "$SCRIPT_DIR/venv/bin/activate"
echo "Activated virtual environment" >&2

# Print debug information
echo "Starting Whoop MCP server..." >&2
echo "Current directory: $(pwd)" >&2
echo "Python executable: $(which python)" >&2
echo "Python version: $(python --version)" >&2
echo "PATH: $PATH" >&2

# Kill any existing server processes
pkill -f "python $SCRIPT_DIR/src/whoop_server.py" || true
echo "Killed any existing server processes" >&2

# Start the server with absolute path
echo "Starting server with command: python $SCRIPT_DIR/src/whoop_server.py" >&2
exec python "$SCRIPT_DIR/src/whoop_server.py"
```

--------------------------------------------------------------------------------
/src/whoop_http_server.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
HTTP server for Whoop API integration.
This server exposes HTTP endpoints to query the Whoop API for cycles, recovery, and strain data.
"""

import os
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from datetime import datetime, timedelta
from dotenv import load_dotenv
import logging
from fastapi import FastAPI, HTTPException
import uvicorn
from whoop import WhoopClient

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    stream=sys.stderr
)
logger = logging.getLogger(__name__)

# Create FastAPI app
app = FastAPI(title="Whoop API Server", description="HTTP server for Whoop API integration")

# Initialize Whoop client
whoop_client: Optional[WhoopClient] = None

def initialize_whoop_client() -> None:
    """Initialize the Whoop client using environment variables."""
    global whoop_client
    
    # Load environment variables
    env_path = Path(__file__).parent.parent / 'config' / '.env'
    logger.info(f"Looking for .env file at: {env_path}")
    
    if not env_path.exists():
        logger.error(f"Environment file not found at {env_path}")
        return
    
    load_dotenv(dotenv_path=env_path)
    logger.info("Environment variables loaded")
    
    # Get credentials
    email = os.getenv("WHOOP_EMAIL")
    password = os.getenv("WHOOP_PASSWORD")
    
    if not email or not password:
        logger.error("Missing Whoop credentials in environment variables")
        return
        
    try:
        whoop_client = WhoopClient(username=email, password=password)
        logger.info("Successfully authenticated with Whoop API")
    except Exception as e:
        logger.error(f"Authentication failed: {str(e)}")

@app.get("/auth/status")
def check_auth_status() -> Dict[str, Any]:
    """Check if we're authenticated with Whoop."""
    if not whoop_client:
        return {
            "authenticated": False,
            "message": "Not authenticated with Whoop"
        }
    
    try:
        # Test authentication by getting profile
        profile = whoop_client.get_profile()
        return {
            "authenticated": True,
            "message": "Successfully authenticated with Whoop",
            "profile": profile
        }
    except Exception as e:
        return {
            "authenticated": False,
            "message": f"Authentication error: {str(e)}"
        }

@app.get("/cycles/latest")
def get_latest_cycle() -> Dict[str, Any]:
    """Get the latest cycle data from Whoop."""
    if not whoop_client:
        raise HTTPException(status_code=401, detail="Not authenticated with Whoop")
    
    try:
        # Get today's date and yesterday's date
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        
        # Get cycle collection for the last day
        cycles = whoop_client.get_cycle_collection(start_date, end_date)
        logger.debug(f"Received cycles response: {cycles}")
        if not cycles:
            raise HTTPException(status_code=404, detail="No cycle data available")
        return cycles[0]  # Return the most recent cycle
    except Exception as e:
        logger.error(f"Error getting latest cycle: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/strain/average")
def get_average_strain(days: int = 7) -> Dict[str, Any]:
    """Calculate average strain over the specified number of days."""
    if not whoop_client:
        raise HTTPException(status_code=401, detail="Not authenticated with Whoop")
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get cycle collection
        cycles = whoop_client.get_cycle_collection(start_date, end_date)
        if not cycles:
            raise HTTPException(status_code=404, detail="No cycle data available")
            
        # Extract strain values
        strains = []
        for cycle in cycles:
            if cycle.get('score') and cycle['score'].get('strain'):
                strains.append(cycle['score']['strain'])
        
        if not strains:
            raise HTTPException(status_code=404, detail="No strain data available")
            
        return {
            "average_strain": sum(strains) / len(strains),
            "days_analyzed": days,
            "samples": len(strains)
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/cycles")
def get_cycles(limit: int = 10) -> List[Dict[str, Any]]:
    """Get multiple cycles from Whoop API."""
    if not whoop_client:
        raise HTTPException(status_code=401, detail="Not authenticated with Whoop")
    
    try:
        # Calculate date range based on limit
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=limit)).strftime("%Y-%m-%d")
        
        # Get cycle collection
        cycles = whoop_client.get_cycle_collection(start_date, end_date)
        if not cycles:
            raise HTTPException(status_code=404, detail="No cycle data available")
        return cycles[:limit]  # Return only the requested number of cycles
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

def main() -> None:
    """Main entry point for the server."""
    try:
        logger.info("Starting Whoop HTTP Server...")
        initialize_whoop_client()
        uvicorn.run(app, host="0.0.0.0", port=8000)
    except Exception as e:
        logger.error(f"Server error: {str(e)}", exc_info=True)
        raise

if __name__ == "__main__":
    main() 
```

--------------------------------------------------------------------------------
/src/whoop_server.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
MCP server for Whoop API integration.
This server exposes methods to query the Whoop API for cycles, recovery, and strain data.
"""

import os
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from datetime import datetime, timedelta
from dotenv import load_dotenv
import logging

from mcp.server.fastmcp import FastMCP
from whoop import WhoopClient

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    stream=sys.stderr
)
logger = logging.getLogger(__name__)

# Create MCP server
mcp = FastMCP("Whoop API MCP Server")

# Initialize Whoop client
whoop_client: Optional[WhoopClient] = None

def initialize_whoop_client() -> None:
    """Initialize the Whoop client using environment variables."""
    global whoop_client
    
    # Load environment variables
    env_path = Path(__file__).parent.parent / 'config' / '.env'
    logger.info(f"Looking for .env file at: {env_path}")
    
    if not env_path.exists():
        logger.error(f"Environment file not found at {env_path}")
        return
    
    load_dotenv(dotenv_path=env_path)
    logger.info("Environment variables loaded")
    
    # Get credentials
    email = os.getenv("WHOOP_EMAIL")
    password = os.getenv("WHOOP_PASSWORD")
    
    if not email or not password:
        logger.error("Missing Whoop credentials in environment variables")
        return
        
    try:
        whoop_client = WhoopClient(username=email, password=password)
        logger.info("Successfully authenticated with Whoop API")
    except Exception as e:
        logger.error(f"Authentication failed: {str(e)}")

@mcp.tool()
def get_latest_cycle() -> Dict[str, Any]:
    """
    Get the latest cycle data from Whoop.
    
    Returns:
        Dictionary containing the latest cycle data including recovery score
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        # Get today's date and yesterday's date
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        
        # Get cycle collection for the last day - pass as positional arguments
        cycles = whoop_client.get_cycle_collection(start_date, end_date)
        logger.debug(f"Received cycles response: {cycles}")
        
        if not cycles:
            return {"error": "No cycle data available"}
            
        latest_cycle = cycles[0]  # Most recent cycle
        
        # Extract recovery score if available
        recovery_score = None
        if latest_cycle.get('score') and latest_cycle['score'].get('recovery'):
            recovery_score = latest_cycle['score']['recovery']
            
        return {
            "cycle": latest_cycle,
            "recovery_score": recovery_score,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting latest cycle: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def get_average_strain(days: int = 7) -> Dict[str, Any]:
    """
    Calculate average strain over the specified number of days.
    
    Args:
        days: Number of days to analyze (default: 7)
        
    Returns:
        Dictionary containing average strain data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get cycle collection
        cycles = whoop_client.get_cycle_collection(start_date, end_date)
        if not cycles:
            return {"error": "No cycle data available"}
            
        # Extract strain values
        strains = []
        for cycle in cycles:
            if cycle.get('score') and cycle['score'].get('strain'):
                strains.append(cycle['score']['strain'])
        
        if not strains:
            return {"error": "No strain data available"}
            
        return {
            "average_strain": sum(strains) / len(strains),
            "days_analyzed": days,
            "samples": len(strains),
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error calculating average strain: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def check_auth_status() -> Dict[str, Any]:
    """
    Check if we're authenticated with Whoop.
    
    Returns:
        Dictionary containing authentication status and profile info if available
    """
    if not whoop_client:
        return {
            "authenticated": False,
            "message": "Not authenticated with Whoop"
        }
    
    try:
        # Test authentication by getting profile
        profile = whoop_client.get_profile()
        return {
            "authenticated": True,
            "message": "Successfully authenticated with Whoop",
            "profile": profile,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        return {
            "authenticated": False,
            "message": f"Authentication error: {str(e)}"
        }

@mcp.tool()
def get_cycles(days: int = 10) -> List[Dict[str, Any]]:
    """
    Get multiple cycles from Whoop API.
    
    Args:
        days: Number of days to fetch (default: 10)
        
    Returns:
        List of cycle data dictionaries
    """
    if not whoop_client:
        return [{"error": "Not authenticated with Whoop"}]
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get cycle collection
        cycles = whoop_client.get_cycle_collection(start_date, end_date)
        if not cycles:
            return [{"error": "No cycle data available"}]
            
        return [{
            "cycle": cycle,
            "timestamp": datetime.now().isoformat()
        } for cycle in cycles]
    except Exception as e:
        logger.error(f"Error getting cycles: {str(e)}")
        return [{"error": str(e)}]

# NEW: Specific ID-Based Retrieval Methods

@mcp.tool()
def get_cycle_by_id(cycle_id: str) -> Dict[str, Any]:
    """
    Get a specific cycle by its ID from Whoop API.
    
    Args:
        cycle_id: The ID of the cycle to retrieve
        
    Returns:
        Dictionary containing the cycle data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        cycle = whoop_client.get_cycle(cycle_id)  # Assumes this method exists
        if not cycle:
            return {"error": f"No cycle found with ID {cycle_id}"}
        return {
            "cycle": cycle,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting cycle {cycle_id}: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def get_recovery_by_id(recovery_id: str) -> Dict[str, Any]:
    """
    Get a specific recovery entry by its ID from Whoop API.
    
    Args:
        recovery_id: The ID of the recovery entry to retrieve
        
    Returns:
        Dictionary containing the recovery data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        recovery = whoop_client.get_recovery(recovery_id)  # Assumes this method exists
        if not recovery:
            return {"error": f"No recovery found with ID {recovery_id}"}
        return {
            "recovery": recovery,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting recovery {recovery_id}: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def get_sleep_by_id(sleep_id: str) -> Dict[str, Any]:
    """
    Get a specific sleep entry by its ID from Whoop API.
    
    Args:
        sleep_id: The ID of the sleep entry to retrieve
        
    Returns:
        Dictionary containing the sleep data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        sleep = whoop_client.get_sleep(sleep_id)  # Assumes this method exists
        if not sleep:
            return {"error": f"No sleep found with ID {sleep_id}"}
        return {
            "sleep": sleep,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting sleep {sleep_id}: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def get_workout_by_id(workout_id: str) -> Dict[str, Any]:
    """
    Get a specific workout by its ID from Whoop API.
    
    Args:
        workout_id: The ID of the workout to retrieve
        
    Returns:
        Dictionary containing the workout data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        workout = whoop_client.get_workout(workout_id)  # Assumes this method exists
        if not workout:
            return {"error": f"No workout found with ID {workout_id}"}
        return {
            "workout": workout,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting workout {workout_id}: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def get_strain_by_id(strain_id: str) -> Dict[str, Any]:
    """
    Get a specific strain entry by its ID from Whoop API.
    
    Args:
        strain_id: The ID of the strain entry to retrieve
        
    Returns:
        Dictionary containing the strain data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        strain = whoop_client.get_strain(strain_id)  # Assumes this method exists
        if not strain:
            return {"error": f"No strain found with ID {strain_id}"}
        return {
            "strain": strain,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting strain {strain_id}: {str(e)}")
        return {"error": str(e)}

# NEW: Standalone Data Retrieval Methods

@mcp.tool()
def get_recoveries(days: int = 10) -> List[Dict[str, Any]]:
    """
    Get multiple recovery entries from Whoop API, independent of cycles.
    
    Args:
        days: Number of days to fetch (default: 10)
        
    Returns:
        List of recovery data dictionaries
    """
    if not whoop_client:
        return [{"error": "Not authenticated with Whoop"}]
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get recovery collection
        recoveries = whoop_client.get_recovery_collection(start_date, end_date)  # Assumes this method exists
        if not recoveries:
            return [{"error": "No recovery data available"}]
            
        return [{
            "recovery": recovery,
            "timestamp": datetime.now().isoformat()
        } for recovery in recoveries]
    except Exception as e:
        logger.error(f"Error getting recoveries: {str(e)}")
        return [{"error": str(e)}]

@mcp.tool()
def get_sleeps(days: int = 10) -> List[Dict[str, Any]]:
    """
    Get multiple sleep entries from Whoop API, independent of cycles.
    
    Args:
        days: Number of days to fetch (default: 10)
        
    Returns:
        List of sleep data dictionaries
    """
    if not whoop_client:
        return [{"error": "Not authenticated with Whoop"}]
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get sleep collection
        sleeps = whoop_client.get_sleep_collection(start_date, end_date)  # Assumes this method exists
        if not sleeps:
            return [{"error": "No sleep data available"}]
            
        return [{
            "sleep": sleep,
            "timestamp": datetime.now().isoformat()
        } for sleep in sleeps]
    except Exception as e:
        logger.error(f"Error getting sleeps: {str(e)}")
        return [{"error": str(e)}]

@mcp.tool()
def get_workouts(days: int = 10) -> List[Dict[str, Any]]:
    """
    Get multiple workout entries from Whoop API.
    
    Args:
        days: Number of days to fetch (default: 10)
        
    Returns:
        List of workout data dictionaries
    """
    if not whoop_client:
        return [{"error": "Not authenticated with Whoop"}]
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get workout collection
        workouts = whoop_client.get_workout_collection(start_date, end_date)  # Assumes this method exists
        if not workouts:
            return [{"error": "No workout data available"}]
            
        return [{
            "workout": workout,
            "timestamp": datetime.now().isoformat()
        } for workout in workouts]
    except Exception as e:
        logger.error(f"Error getting workouts: {str(e)}")
        return [{"error": str(e)}]

@mcp.tool()
def get_strains(days: int = 10) -> List[Dict[str, Any]]:
    """
    Get multiple strain entries from Whoop API, independent of cycles.
    
    Args:
        days: Number of days to fetch (default: 10)
        
    Returns:
        List of strain data dictionaries
    """
    if not whoop_client:
        return [{"error": "Not authenticated with Whoop"}]
    
    try:
        # Calculate date range
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
        
        # Get strain collection
        strains = whoop_client.get_strain_collection(start_date, end_date)  # Assumes this method exists
        if not strains:
            return [{"error": "No strain data available"}]
            
        return [{
            "strain": strain,
            "timestamp": datetime.now().isoformat()
        } for strain in strains]
    except Exception as e:
        logger.error(f"Error getting strains: {str(e)}")
        return [{"error": str(e)}]

# User Measurements Retrieval

@mcp.tool()
def get_user_body_measurements() -> Dict[str, Any]:
    """
    Get the user's body measurements from Whoop.
    Uses /v1/user/measurement/body endpoint.
    
    Returns:
        Dictionary containing height, weight, and max heart rate
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        # API endpoint: GET /v1/user/measurement/body
        measurements = whoop_client.get_body_measurement()
        
        if not measurements:
            return {"error": "No body measurement data available"}
            
        return {
            "measurements": measurements,
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting body measurements: {str(e)}")
        return {"error": str(e)}

@mcp.tool()
def get_latest_recovery() -> Dict[str, Any]:
    """
    Get the latest recovery data from Whoop.
    Uses /v1/recovery endpoint.
    
    Returns:
        Dictionary containing latest recovery data
    """
    if not whoop_client:
        return {"error": "Not authenticated with Whoop"}
    
    try:
        # Get today's recovery data
        end_date = datetime.now().strftime("%Y-%m-%d")
        start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        
        # API endpoint: GET /v1/recovery
        # Parameters: start, end, limit=10 (default)
        recoveries = whoop_client.get_recovery_collection(start_date, end_date)
        
        if not recoveries or not recoveries.get('records'):
            return {"error": "No recent recovery data available"}
            
        # Get the first record (most recent as API sorts by sleep start time descending)
        latest_recovery = recoveries['records'][0]
        
        return {
            "recovery": latest_recovery,
            "recovery_score": latest_recovery.get('score', {}).get('recovery_score'),
            "timestamp": datetime.now().isoformat()
        }
    except Exception as e:
        logger.error(f"Error getting latest recovery: {str(e)}")
        return {"error": str(e)}

# Team-Related Functionality (Optional)

@mcp.tool()
def get_team_members(team_id: str) -> List[Dict[str, Any]]:
    """
    Get team member data from Whoop API.
    
    Args:
        team_id: The ID of the team to retrieve members for
        
    Returns:
        List of team member data dictionaries
    """
    if not whoop_client:
        return [{"error": "Not authenticated with Whoop"}]
    
    try:
        members = whoop_client.get_team_members(team_id)  # Assumes this method exists
        if not members:
            return [{"error": f"No members found for team {team_id}"}]
        return [{
            "member": member,
            "timestamp": datetime.now().isoformat()
        } for member in members]
    except Exception as e:
        logger.error(f"Error getting team members for team {team_id}: {str(e)}")
        return [{"error": str(e)}]

def main() -> None:
    """Main entry point for the server."""
    try:
        logger.info("Starting Whoop MCP Server...")
        initialize_whoop_client()
        logger.info("Running MCP server with stdio transport")
        mcp.run(transport="stdio")
    except Exception as e:
        logger.error(f"Server error: {str(e)}", exc_info=True)
        raise

if __name__ == "__main__":
    main()
```