Skip to main content
Glama
README.md
# garmin-coach

A local stdio MCP server that gives Claude Code read access to your Garmin
Connect account, plus a gated, allow-listed path for creating and scheduling
workouts. Intended as the data layer for an AI training/fat-loss coach.

## Architecture

```
Claude Code
    ↓  MCP (stdio, local subprocess — no network)
garmin_mcp.server          25 read-only tools (+6 when writes are enabled)
    ↓
garmin_mcp.client          token-only auth, error normalisation
    ↓  python-garminconnect 0.3.11
Garmin Connect (HTTPS)
    ↓  device sync
Garmin Forerunner 970
```

Everything runs on your Mac. The only network traffic is normal HTTPS to
Garmin. Nothing is sent to a third party.

## Install

```bash
cd ~/Developer/garmin-coach
uv sync
```

Python is pinned to 3.12 via `.python-version`. `garminconnect` requires ≥3.12.

## Initial Garmin login — run this yourself

Credentials are only ever entered interactively, in your own terminal:

```bash
cd ~/Developer/garmin-coach
uv run python scripts/garmin_login.py
```

It prompts for your email, then your password via `getpass` (hidden, never in
shell history), and handles an MFA code if Garmin asks. On success it mints
OAuth tokens and does a small harmless read to prove the session works.

If Garmin starts rate limiting, the script stops and tells you to wait — it
does not retry in a loop, which is what gets accounts throttled harder.

## Token storage and security

- Tokens live in `~/.garmin-coach/tokens` — **outside** the project, so they
  can never be swept into git.
- The directory is forced to `0700`, every file inside to `0600`.
- Your password is used exactly once, to mint tokens. It is never stored, never
  written to `.env`, never in MCP config, never in the repo.
- Token contents are never logged. Logging goes to stderr only (stdout is the
  MCP protocol channel).
- Override the location with `GARMIN_TOKENSTORE=/some/path`.

**Treat `~/.garmin-coach/tokens` as equivalent to your password.** Anyone with
those files has full read/write access to your Garmin account.

## Test the MCP

```bash
uv run python scripts/smoke_test.py
```

Stage 1 runs without any Garmin auth — it exercises the stdio transport, tool
discovery, the bundled exercise catalog, the workout builder, and the error
paths. Stage 2 runs the live read tools, and is skipped automatically until
you have logged in.

## Connect to Claude Code

Already registered user-scoped (available in every project):

```bash
claude mcp add --scope user garmin -- \
  /opt/homebrew/bin/uv run --directory ~/Developer/garmin-coach \
  python -m garmin_mcp.server

claude mcp get garmin     # check status
```

`uv run --directory` resolves the project's own virtualenv, so you never need
to activate anything manually.

## Tools

**Health** — `garmin_today`, `garmin_sleep`, `garmin_hrv`, `garmin_stress`,
`garmin_body_battery`, `garmin_heart_rate`, `garmin_recovery`

**Training** — `garmin_training_readiness`, `garmin_training_status`,
`garmin_hr_zones`, `garmin_performance_metrics`, `garmin_recent_activities`,
`garmin_activities_by_date`, `garmin_activity`, `garmin_strength_sets`,
`garmin_exercise_history`

**Workout library (read-only)** — `garmin_workouts`, `garmin_workout`,
`garmin_scheduled_workouts`, `garmin_search_exercises`,
`garmin_exercise_categories`, `garmin_resolve_exercise`,
`garmin_workout_step_formats`, `garmin_devices`,
`garmin_preview_strength_workout`

All dates are ISO `YYYY-MM-DD` and default to today. Responses are normalised
and compact; **a value Garmin did not return is omitted rather than guessed**,
so "field absent" and "value is zero" stay distinguishable.

### Write tools (disabled by default)

Not registered at all unless `GARMIN_MCP_ENABLE_WRITES=1` is in the server
environment, and independently re-checked at call time:

`garmin_create_strength_workout`, `garmin_create_cardio_workout`,
`garmin_update_workout`, `garmin_schedule_workout`,
`garmin_unschedule_workout`, `garmin_push_workout_to_device`

This is an allow-list, not a passthrough. Deleting activities, gear, or
profile data is not exposed. `garmin_unschedule_workout` additionally requires
`confirm=true`.

To enable:

```bash
claude mcp remove garmin -s user
claude mcp add --scope user garmin -e GARMIN_MCP_ENABLE_WRITES=1 -- \
  /opt/homebrew/bin/uv run --directory ~/Developer/garmin-coach \
  python -m garmin_mcp.server
```

Use `garmin_preview_strength_workout` first — it builds and renders the exact
payload without uploading. Verify the structure before anything reaches your
calendar or watch.

## Known Garmin limitations

The unofficial API does not expose everything the watch shows. The full
matrix — what's on the **watch** vs in **Garmin Connect** vs reachable via
**python-garminconnect** — is in [`docs/forerunner-970.md`](docs/forerunner-970.md).

The ones that shape the coaching design:

- **No RIR/RPE field exists.** Garmin has nowhere to put it.
- **Garmin stores one concrete rep target per set**, not a range. A
  "5–8 @ 1 RIR" prescription cannot round-trip — push the concrete target
  (e.g. 6) and keep the programming rule in the coaching layer. Garmin is the
  execution interface, not the source of truth for the program.
- **Per-set heart rate is not available**; HR is session-level only.
- **Auto-detected reps are often wrong** for machines/cables until corrected
  in Garmin Connect.
- **Use `garmin_hr_zones`, never computed zones.** Zone 2 is a heart-rate
  range from your Garmin profile, not a hard-coded pace.

## Re-authenticate or revoke

```bash
rm -rf ~/.garmin-coach/tokens              # revoke locally
uv run python scripts/garmin_login.py      # re-authenticate
```

Tokens auto-refresh, so this is only needed if the refresh token expires or
you revoke it. To revoke server-side, change your Garmin password — that
invalidates the tokens.

## Remove from Claude Code

```bash
claude mcp remove garmin -s user
```

That only unregisters the server. To fully remove: also delete
`~/.garmin-coach` and `~/Developer/garmin-coach`.

## Troubleshooting

| Symptom | Cause / fix |
|---|---|
| `No Garmin tokens found in …` | Run `scripts/garmin_login.py` |
| `Saved Garmin tokens were rejected` | Refresh token expired/revoked — log in again |
| `Garmin is rate limiting this account` | Wait 15–30 min. Do **not** retry in a loop |
| `Could not reach Garmin Connect` | Network down, or Garmin maintenance |
| Tool returns `available: false` | Garmin genuinely has no data for that date |
| MCP not detected | `claude mcp get garmin`; check `uv` path is absolute |

Set `GARMIN_MCP_LOG_LEVEL=DEBUG` for verbose stderr logging.
# Garmin-MCP

TDQS

B3.2/5.0

Scored across 25 tools

Disambiguation4/5

Most tools map to a distinct Garmin metric or resource, and the descriptions clearly separate daily summaries, sleep, HRV, stress, recovery, readiness, and training status. A couple of composite tools overlap in content—recovery vs readiness, training status vs performance metrics—but the descriptions are specific enough to avoid serious misselection.

Naming Consistency4/5

All tools share a clear garmin_ prefix and mostly follow a resource-oriented naming pattern. Some verbs appear inconsistently (search_exercises, resolve_exercise, preview_strength_workout) compared to noun-only names, but the overall pattern remains predictable and readable.

Tool Count3/5

At 25 tools, this sits right at the heavy end of the borderline range. Each tool maps to a real Garmin endpoint, but the set includes several overlapping composite views and could likely be consolidated without losing capability.

Completeness2/5

The read side is comprehensive for health metrics and activities, but the workout workflow is incomplete: preview_strength_workout explicitly avoids uploading, and devices mentions needing IDs to push a workout, yet no tool actually pushes, saves, schedules, or edits a workout. This is a significant dead end for the stated coaching purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues