Skip to main content
Glama
ClemensHar

Wahoo MCP Server

by ClemensHar
README.md
# Wahoo MCP Server

[![CI](https://github.com/armonge/wahoo-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/armonge/wahoo-mcp/actions/workflows/ci.yml)
[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)

A Model Context Protocol (MCP) server for interacting with the Wahoo Cloud API, focusing on reading workout information.

## Features

- **Workouts**: List workouts with pagination and date filtering, get detailed workout information
- **Routes**: List and retrieve saved cycling/running routes
- **Training Plans**: Access and create training plan templates in your Wahoo account
- **Workout Scheduling**: Schedule workouts on the calendar and attach plans for ELEMNT app visibility
- **Power Zones**: View power zone configurations for different workout types
- **OAuth 2.0 Authentication**: Secure authentication with automatic token refresh
- **Comprehensive workout type support**: 72 different workout types with location and family categorization
- **Async/await implementation**: High-performance async operations using httpx
- **Automatic token management**: Tokens are refreshed automatically when they expire

## Installation

### Using uv (recommended)

First, install `uv` if you haven't already:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Then install the project dependencies:
```bash
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv pip install -e .
```

For development:
```bash
uv pip install -e ".[dev]"
```

### Using pip (alternative)

If you prefer using pip:
```bash
python3.13 -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e .
```

For development:
```bash
pip install -e ".[dev]"
```

## Configuration

### Getting an Access Token

1. Register your application at [Wahoo's Developer Portal](https://developers.wahooligan.com/) to get a Client ID and Client Secret.

2. Create a `.env` file from the example:
   ```bash
   cp .env.example .env
   ```

   Then edit `.env` and add your credentials:
   ```env
   WAHOO_CLIENT_ID=your_client_id_here
   WAHOO_CLIENT_SECRET=your_client_secret_here
   ```

3. Set the token file path in your `.env` file:
   ```env
   WAHOO_TOKEN_FILE=token.json
   ```

4. Use the authentication helper:
   ```bash
   make auth
   # or
   uv run python src/auth.py
   ```

   This will:
   - Use credentials from `.env` (or prompt if not set)
   - Open a browser for OAuth authentication
   - Start a local server to receive the callback
   - Save your tokens to the file specified by `WAHOO_TOKEN_FILE`
   - Tokens will be automatically refreshed when needed

### Configuration Options

The auth server can be configured via environment variables:

**Server Configuration:**
- `WAHOO_AUTH_HOST`: Auth server bind address (default: `localhost`)
- `WAHOO_AUTH_PORT`: Auth server port (default: `8080`)

**Redirect URL Configuration:**
- `WAHOO_REDIRECT_HOST`: OAuth callback host (default: uses `WAHOO_AUTH_HOST`)
- `WAHOO_REDIRECT_PORT`: OAuth callback port (default: uses `WAHOO_AUTH_PORT`)
- `WAHOO_REDIRECT_SCHEME`: URL scheme - `http` or `https` (default: `http`)

**Credentials:**
- `WAHOO_CLIENT_ID`: Your Wahoo Client ID
- `WAHOO_CLIENT_SECRET`: Your Wahoo Client Secret
- `WAHOO_TOKEN_FILE`: Path to store OAuth tokens (required)

**Example Configurations:**

1. **Local Development (default):**
   ```env
   # Redirect URL will be: http://localhost:8080/callback
   ```

2. **Using ngrok:**
   ```env
   WAHOO_AUTH_HOST=localhost
   WAHOO_AUTH_PORT=8080
   WAHOO_REDIRECT_HOST=your-app.ngrok.io
   WAHOO_REDIRECT_PORT=443
   WAHOO_REDIRECT_SCHEME=https
   # Redirect URL will be: https://your-app.ngrok.io:443/callback
   ```

**Note**: When registering your app with Wahoo, use the redirect URL that matches your configuration.

## Usage

### Running the MCP Server

```bash
uv run python -m src.server
```

Or if you've activated the virtual environment:
```bash
python -m src.server
```

### Using with Claude Desktop

Add the following to your Claude Desktop configuration file:

**Configuration file location:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`

**Example configuration:**

```json
{
  "mcpServers": {
    "wahoo": {
      "type": "stdio",
      "command": "/path/to/uv",
      "args": [
        "--project",
        "/path/to/wahoo-mcp",
        "run",
        "python",
        "-m",
        "src.server"
      ],
      "env": {
        "WAHOO_TOKEN_FILE": "/path/to/wahoo-mcp/token.json"
      }
    }
  }
}
```

Make sure to replace `/path/to/` with your actual paths.

### Available Tools

#### list_workouts
List workouts from your Wahoo account.

Parameters:
- `page` (optional): Page number (default: 1)
- `per_page` (optional): Number of items per page (default: 30)
- `start_date` (optional): Filter workouts created after this date (ISO 8601 format)
- `end_date` (optional): Filter workouts created before this date (ISO 8601 format)

Example:
```
Use the list_workouts tool to show my recent workouts
```

#### get_workout
Get detailed information about a specific workout.

Parameters:
- `workout_id` (required): The ID of the workout to retrieve

Example:
```
Use the get_workout tool to get details for workout ID 12345
```

#### list_routes
List routes from your Wahoo account.

Parameters:
- `external_id` (optional): Filter routes by external ID

Example:
```
Use the list_routes tool to show my saved routes
```

#### get_route
Get detailed information about a specific route.

Parameters:
- `route_id` (required): The ID of the route to retrieve

Example:
```
Use the get_route tool to get details for route ID 456
```

#### list_plans
List training plans from your Wahoo account.

Parameters:
- `external_id` (optional): Filter plans by external ID

Example:
```
Use the list_plans tool to show my training plans
```

#### get_plan
Get detailed information about a specific plan.

Parameters:
- `plan_id` (required): The ID of the plan to retrieve

Example:
```
Use the get_plan tool to get details for plan ID 789
```

#### list_power_zones
List power zones from your Wahoo account.

Parameters: None

Example:
```
Use the list_power_zones tool to show my power zones
```

#### get_power_zone
Get detailed information about a specific power zone.

Parameters:
- `power_zone_id` (required): The ID of the power zone to retrieve

Example:
```
Use the get_power_zone tool to get details for power zone ID 321
```

#### create_plan
Create a reusable training plan template in your Wahoo account. A plan holds the workout structure — intervals, power/HR targets, and duration — but has no date and is not tied to a specific ride. It will not appear in the ELEMNT app on its own; use `create_workout` to schedule it.

Parameters:
- `plan` (required): Complete workout plan structure containing:
  - `name` (required): Name of the workout plan
  - `description` (optional): Description of the workout
  - `intervals` (required): List of workout intervals, each containing:
    - `duration` (required): Duration in seconds
    - `targets` (required): List of targets (power, heart_rate, speed, pace, rpe, cadence)
    - `name` (optional): Name/description of the interval
    - `interval_type` (optional): Type (work, rest, warmup, cooldown, tempo, threshold, recovery, active, or Wahoo types: wu, cd, lt, map, ac, nm, ftp, recover)
  - `workout_type` (optional): Type of workout (bike, run, swim) - defaults to "bike"
  - `estimated_duration` (optional): Estimated total duration in seconds
  - `estimated_tss` (optional): Estimated Training Stress Score
- `external_id` (required): Unique external ID for the plan
- `provider_updated_at` (required): External date/time the file was updated (ISO 8601 format)
- `filename` (optional): Name of the plan file

Example:
```
Use the create_plan tool to create a new training plan with intervals for power and heart rate zones
```

#### create_workout
Schedule a workout on the calendar and optionally attach a plan to it. A workout is a **scheduled instance** — a calendar entry with a name, start time, and duration. Attaching a `plan_id` links the plan template to that slot.

> **Important:** For a plan to appear in the Wahoo ELEMNT app, it must be attached to a workout scheduled within the current day through 6 days from now. A plan alone (without a linked workout) does **not** appear in the ELEMNT app.

Parameters:
- `name` (required): Workout name
- `workout_token` (required): Unique app-side identifier (e.g. a UUID or slug)
- `workout_type_id` (required): Workout type ID (e.g. `0` for outdoor biking, `12` for indoor biking, `1` for running outdoor)
- `starts` (required): ISO 8601 datetime for when the workout starts — must be within the current day through 6 days from now for ELEMNT app visibility
- `minutes` (required): Workout duration in minutes
- `plan_id` (optional): ID of a previously created plan to attach to this workout
- `route_id` (optional): ID of a route to attach to this workout

Example — scheduling a plan for tomorrow:
```
First use create_plan to create the plan, then use create_workout with the returned plan_id and a starts date within the next 6 days
```

#### Typical workflow: plan a training session for the ELEMNT app

1. Call `create_plan` with your intervals and targets → note the returned `id`
2. Call `create_workout` with `plan_id` set to that `id` and `starts` set to a date within the next 6 days
3. The workout (with the attached plan) will appear in the Wahoo ELEMNT app

## Development

### Running Tests

```bash
uv run pytest
```

Or if you've activated the virtual environment:
```bash
pytest
```

### Project Structure

```
wahoo-mcp/
├── src/
│   ├── __init__.py
│   ├── server.py       # Main MCP server implementation
│   ├── auth.py         # OAuth authentication helper
│   ├── token_store.py  # Token storage and refresh logic
│   └── models.py       # Pydantic models for API data structures
├── tests/
│   ├── __init__.py
│   ├── test_server.py  # Server test suite
│   └── test_token_store.py  # Token store tests
├── pyproject.toml      # Project configuration
└── README.md          # This file
```

## API Reference

The server implements the following Wahoo Cloud API endpoints:

**Workouts:**
- `GET /v1/workouts` - List workouts with pagination and date filtering
- `GET /v1/workouts/{id}` - Get detailed workout information

**Routes:**
- `GET /v1/routes` - List saved routes
- `GET /v1/routes/{id}` - Get route details including GPS data

**Training Plans:**
- `GET /v1/plans` - List training plans
- `GET /v1/plans/{id}` - Get plan details
- `POST /v1/plans` - Create a new training plan template

**Workout Scheduling:**
- `POST /v1/workouts` - Schedule a workout and optionally attach a plan

**Power Zones:**
- `GET /v1/power_zones` - List power zone configurations
- `GET /v1/power_zones/{id}` - Get specific power zone details

For full API documentation, see [Wahoo Cloud API](https://cloud-api.wahooligan.com/).

## License

This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.

TDQS

B3.4/5.0

Scored across 7 tools

Disambiguation5/5

Each tool targets a distinct resource and action: workouts, plans, power zones, and routes with clear CRUD-like verbs. No overlap in functionality.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (create_, get_, list_) with underscores. No mixing of conventions.

Tool Count5/5

Seven tools is well-scoped for the Wahoo fitness domain, covering essential operations without unnecessary bloat.

Completeness3/5

Missing get_workout, get_route, and CRUD operations for plans, power zones, and routes beyond list and get. Gaps may require workarounds.