Skip to main content
Glama
marcelohensantos

garmin-mcp

README.md
# garmin-mcp

[![CI](https://github.com/marcelohensantos/garmin-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/marcelohensantos/garmin-mcp/actions/workflows/ci.yml)
[![Docker](https://github.com/marcelohensantos/garmin-mcp/actions/workflows/publish.yml/badge.svg)](https://github.com/marcelohensantos/garmin-mcp/pkgs/container/garmin-mcp)

MCP server that exposes Garmin Connect data and workout management to AI agents.
Supports tools, resources, and prompts — covering reading health data, creating and
scheduling structured workouts, and guided coaching workflows.

## Setup

```bash
git clone https://github.com/marcelohensantos/garmin-mcp
cd garmin-mcp
make setup
```

`make setup` handles everything interactively:
- prompts for your Garmin Connect credentials and creates `.env`
- creates the virtual environment and installs dependencies
- tests the connection against the Garmin API
- prints the exact JSON snippet to paste into your MCP client

> **First login** triggers a Garmin OAuth flow. If you have MFA enabled, check your
> email for a verification link, then re-run `make setup` or `make check`.

> **Switching accounts?** Clear the cached tokens first:
> ```bash
> make clean-auth && make setup
> ```

### Configure your MCP client

**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "garmin": {
      "command": "/path/to/garmin-mcp/.venv/bin/python",
      "args": ["/path/to/garmin-mcp/src/server.py"]
    }
  }
}
```

**Claude Code** — add to `.mcp.json` at the project root:

```json
{
  "mcpServers": {
    "garmin": {
      "command": "/path/to/garmin-mcp/.venv/bin/python",
      "args": ["/path/to/garmin-mcp/src/server.py"]
    }
  }
}
```

Restart the client after saving.

### Docker (no Python setup required)

```bash
# First run — authenticate and cache OAuth tokens
docker run --rm -it \
  --env-file .env \
  -v garmin-tokens:/root/.garminconnect \
  --entrypoint python \
  ghcr.io/marcelohensantos/garmin-mcp src/check.py

# MCP client config
{
  "mcpServers": {
    "garmin": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "--env-file", "/path/to/garmin-mcp/.env",
        "-v", "garmin-tokens:/root/.garminconnect",
        "ghcr.io/marcelohensantos/garmin-mcp"
      ]
    }
  }
}
```

The `garmin-tokens` volume persists OAuth tokens across container restarts — you only authenticate once. Create `.env` with your credentials before the first run (see [Setup](#setup)).

---

## Available tools

### Activities

| Tool | Description |
|------|-------------|
| `get_activities` | Most recent N activities; optional `activity_type` filter (`running`, `strength_training`, `swimming`, …) |
| `get_activities_by_date` | Activities in a date range; same optional `activity_type` filter |
| `get_activity_details` | Full details for one activity |
| `export_activity` | Download GPX / TCX / FIT / CSV for one activity |
| `export_activities_csv` | Summary CSV for a date range |

### Health & wellness

| Tool | Description |
|------|-------------|
| `get_stats` | Daily steps, calories, floors |
| `get_heart_rates` | Heart-rate timeline |
| `get_sleep` | Sleep stages and score |
| `get_stress` | Stress levels throughout the day |
| `get_body_battery` | Body Battery charge curve |
| `get_body_composition` | Weight, BMI, body fat |
| `get_hrv_data` | Heart Rate Variability |
| `get_spo2` | Blood oxygen saturation |

### Training & fitness

| Tool | Description |
|------|-------------|
| `get_training_status` | Training load and status |
| `get_personal_records` | All-time PRs |
| `get_vo2max` | VO2 max estimates |

### Workouts & calendar

| Tool | Description |
|------|-------------|
| `create_running_workout` | Structured running workout — pace zones, repeat groups, distance/time-based steps |
| `update_running_workout` | Update an existing running workout in-place via PUT (preserves calendar scheduling) |
| `create_swimming_workout` | Structured pool swimming workout — stroke types, pool length, repeat groups |
| `create_strength_workout` | Strength session — exercises grouped as repeat sets with weight and rest |
| `schedule_workout` | Schedule an existing workout on the Garmin calendar |
| `get_workouts` | List saved workouts |
| `delete_workout` | Delete a workout by ID |
| `get_scheduled_workouts` | List scheduled workouts in a date range |

### Plans

| Tool | Description |
|------|-------------|
| `save_plan` | Persist an agent-generated training plan as JSON to `~/devel/garmin/data/` |

### Profile & devices

| Tool | Description |
|------|-------------|
| `get_user_profile` | Account profile |
| `get_devices` | Linked Garmin devices |
| `get_gear` | Shoes, bikes, and other gear |

---

## Resources

Resources provide persistent context the agent can read without an explicit tool call.

| URI | Contents |
|-----|----------|
| `garmin://athlete/profile` | User profile + all personal records |
| `garmin://health/today` | Today's stats, Body Battery, HRV, and sleep snapshot |

Both resources are cached for 5 minutes to avoid redundant API calls.

---

## Prompts

Guided workflow templates for common coaching tasks.

| Prompt | Purpose |
|--------|---------|
| `create_running_workout` | Step-by-step guide: spec format, pace syntax, examples, schedule call |
| `plan_training_week` | Multi-step protocol: assess recovery data → check calendar → create and schedule workouts |
| `training_readiness_check` | Pre-session readiness: Body Battery, HRV, sleep, stress → go / reduce / rest decision |

---

## Project layout

```
src/
├── server.py           # Entry point — imports all modules to trigger registration
├── app.py              # FastMCP instance (shared across all modules)
├── auth.py             # Garmin client singleton; wraps login errors in GarminAuthError
├── cache.py            # @cached(ttl_seconds) in-memory TTL decorator
├── errors.py           # GarminAPIError hierarchy + tool_guard decorator
├── utils.py            # serialize(), today(), export_dir()
├── resources.py        # MCP resources: athlete/profile, health/today
├── prompts.py          # MCP prompts: create_running_workout, plan_training_week, …
├── check.py            # Connectivity smoke test (make check)
└── tools/
    ├── builder.py      # WorkoutBuilder base class + repeat_group() helper
    ├── running.py      # RunningBuilder + create/update_running_workout
    ├── swimming.py     # SwimmingBuilder + create_swimming_workout
    ├── strength.py     # StrengthBuilder + create_strength_workout
    ├── calendar.py     # schedule, get, delete, get_scheduled workouts
    ├── activities.py   # Activity retrieval, filtering, and export
    ├── health.py       # Health & wellness tools (cached 5 min)
    ├── training.py     # Training status, VO2 max, PRs (cached 5–10 min)
    ├── profile.py      # User profile, devices, gear (cached 10 min)
    └── plans.py        # save_plan — persist plans to data/

tests/
├── unit/               # Mocked — 50 tests, no network required
└── integration/        # Live tests against Garmin Connect API
```

---

## Running tests

```bash
# Unit tests (no credentials needed)
make test

# Integration tests (requires real Garmin account)
make test-integration
```

---

## Notes

- **Caching** — health, training, and profile tools cache responses for 5–10 minutes. If you need fresh data immediately, restart the server.
- **Error types** — API errors are typed: `GarminAuthError`, `GarminRateLimitError`, `GarminNotFoundError`. Tools return `{"error": "...", "type": "..."}` on failure.
- **Exported files** land in `~/garmin_exports/`.
- **MFA accounts** — set `return_on_mfa=True` in `auth.py` and handle the prompt manually.
- **First run** triggers a full OAuth login; subsequent runs reuse cached tokens at `~/.garminconnect/`.
- **Pace zones** — `pace.zone` targets use m/s internally (`targetValueOne > targetValueTwo`). Single pace `"4:31"` applies a tolerance (default ±10 s); range `["5:26", "5:59"]` uses exact bounds.
- **Easy runs** — a single no-pace interval with warmup/cooldown merges into one time step + lap-button cooldown.
- **Distance-based warmup/cooldown** — use `warmup_km` + `warmup_pace: ["fast", "slow"]` for E zone guidance on distance steps.
- **Bodyweight exercises** — set `weight_kg: -1.0` in `create_strength_workout`.
- **Activity filter** — `get_activities` filters by `activityType.typeKey` client-side; use a larger `limit` when filtering to avoid truncation before the filter runs.
- **`save_plan`** always writes to `~/devel/garmin/data/` — the directory is created if absent.