Skip to main content
Glama
jhoy1020

garmin-coach-mcp

by jhoy1020
README.md
# garmin-coach-mcp

An MCP server that lets Claude read and answer questions about your
[Garmin Connect](https://connect.garmin.com) data — recent activities, per-set strength
history, sleep and recovery, and simple trends — and optionally build workouts back onto
your watch.

Same code, two ways to run it:

| Mode | Where | Use it for | Setup |
|---|---|---|---|
| **Local** | Your PC, launched by Claude Code | Day-to-day at your desk | ~2 min, free |
| **Hosted** | Azure Container Apps | Asking from your phone, PC off | ~20 min, ~$5/mo |

Local is the default: nothing listens on a socket, so nothing needs authenticating.
Hosted is opt-in and sits behind Microsoft Entra sign-in.

> **Garmin's API is unofficial.** This uses `python-garminconnect`, which is not
> sanctioned by Garmin and can break when they change their backend. Fine for personal
> use; don't build anything load-bearing on it.

## Quick start

**Local**

```bash
git clone https://github.com/jhoy1020/garmin-coach-mcp.git
cd garmin-coach-mcp
./scripts/setup.sh          # or .\scripts\setup.ps1 on Windows
```

Creates the virtualenv, installs the package, runs the one-time Garmin login, and
registers the server with Claude Code using this checkout's own path. Nothing to edit.
Verify with `python scripts/doctor.py`, then restart Claude Code and ask it something.

**Hosted** — after the local setup works:

```bash
python scripts/deploy.py
```

Provisions Azure, registers the Entra application, seeds your Garmin token, and prints
the connector URL.

## What leaves your machine

- **Local mode:** nothing. The server talks to Garmin directly from your PC. Your Garmin
  password is typed into a subprocess and never stored — only a refreshable token, in
  `~/.garminconnect`.
- **Hosted mode:** that token is uploaded to an Azure file share in your own
  subscription, and your Garmin data passes through your own container. Requests come
  from claude.ai's servers. See [docs/security.md](docs/security.md).

## Tools

**Read (9)**

| Tool | What it answers |
|------|-----------------|
| `garmin_status` | Is the connection working? Whose account? |
| `list_activities(limit, activity_type)` | "Show my last N workouts" |
| `get_activity(activity_id)` | Detailed metrics for one activity |
| `get_exercise_sets(activity_id)` | Per-set exercise / reps / weight / est. 1RM |
| `get_strength_history(exercise_name, weeks)` | "Is my squat going up?" |
| `get_recovery(date)` | Sleep + daily stats + training readiness |
| `get_trends(metric, weeks, sample_days)` | Resting HR, steps, stress over time |
| `suggest_next_weight(exercise_name, ...)` | Progressive-overload recommendation |
| `list_workouts(limit)` | Saved workouts (name + id) |

**Write (3)** — set `GARMIN_COACH_READONLY=1` to drop these from the manifest entirely.

| Tool | What it does |
|------|--------------|
| `create_strength_workout(name, exercises, schedule_date)` | Build a strength workout on Garmin (experimental) |
| `schedule_workout(workout_id, date)` | Put an existing workout on your calendar |
| `delete_workout(workout_id)` | Undo / remove a workout |

> Strength answers only have data if you log Strength activities **with weights** on
> your watch. `create_strength_workout` uses Garmin's unofficial workout schema — check
> the result in the Garmin Connect app and `delete_workout` if it's wrong.

## Layout

```
src/garmin_coach_mcp/
  server.py            tool definitions + entry point
  config.py            settings; refuses to start on unsafe combinations
  http_app.py          SecretGate - the auth ladder for hosted requests
  oauth.py             validates Entra-issued tokens
  oauth_broker.py      a small OAuth 2.1 server (the path real clients use)
  ratelimit.py         token buckets for the unauthenticated endpoints
  garmin_client.py     cached, token-authenticated Garmin client
  analysis.py          strength-history and trend computations, argument clamps
  strength_workout.py  Garmin workout payload builder
  login.py             one-time interactive Garmin login
scripts/               setup, deploy, doctor, rotate, secret scanner
infra/                 Bicep for the hosted deployment
tests/                 109 tests
docs/                  setup, architecture, security, troubleshooting
```

## Documentation

| | |
|---|---|
| [docs/local-setup.md](docs/local-setup.md) | Run it on your PC |
| [docs/remote-setup.md](docs/remote-setup.md) | Deploy to Azure with Microsoft sign-in |
| [docs/architecture.md](docs/architecture.md) | How it's built and why |
| [docs/security.md](docs/security.md) | Threat model, what protects what, rotating secrets |
| [docs/troubleshooting.md](docs/troubleshooting.md) | Organised by what you actually see |
| [CLAUDE.md](CLAUDE.md) | Context for AI agents working on this repo |

## Scripts

| Task | Command |
|---|---|
| Set up locally | `python scripts/setup_local.py` |
| Diagnose anything | `python scripts/doctor.py` (`--remote`, `--json`) |
| Deploy to Azure | `python scripts/deploy.py` |
| Register the Entra app | `python scripts/entra_app.py` |
| Upload the Garmin token | `python scripts/seed_token.py` |
| Rotate secrets | `python scripts/rotate_secrets.py --help` |
| Get the connector URL | `python scripts/doctor.py --connector-url` |
| Scan for secrets | `python scripts/check_secrets.py` |

## Configuration

`.env.example` documents every setting. The ones you are most likely to touch:

| Variable | Default | Meaning |
|---|---|---|
| `GARMIN_COACH_TRANSPORT` | `stdio` | `stdio` or `http` |
| `GARMIN_COACH_READONLY` | off | Hide the three write tools entirely |
| `GARMINTOKENS` | `~/.garminconnect` | Where the Garmin OAuth token lives |
| `GARMIN_COACH_TIMEZONE` | your machine's | Container `TZ`; wrong value makes "today" wrong |
| `GARMIN_COACH_ALLOWED_REDIRECT_HOSTS` | `claude.ai,claude.com` + loopback | Hosts that may receive an authorization code |

Hosted mode also needs the Entra values, which `scripts/deploy.py` sets for you — see
[docs/remote-setup.md](docs/remote-setup.md) for the name mapping between what you type
and what the container reads.

`config.py` refuses to start on unsafe combinations rather than running half-secured:
a partial Entra configuration, an `http` transport with no way to authenticate, a
too-short secret, a non-https public URL, access logging alongside a URL secret, or a
wildcard redirect-host allowlist without an explicit second opt-in.

## Tests

```bash
python scripts/setup_local.py --dev    # installs pytest
python -m pytest
```

109 tests. The security-relevant ones are written to fail if the fix is reverted — the
concurrency test genuinely serialises without the threadpool, and the rate-limiter test
fails if the limiter key goes back to the spoofable leftmost `X-Forwarded-For` entry.

## Contributing

`python scripts/install_hooks.py` enables the pre-commit scanner (setup does this for
you). It blocks credentials **and** personal data — this repository is public, and the
thing that actually needed cleaning up was absolute paths and resource names, not keys.
CI runs the same checks.

## Licence

[MIT](LICENSE).

TDQS

A4.1/5.0

Scored across 12 tools

Disambiguation5/5

Each tool targets a distinct concern: status, activity retrieval, strength analysis, recovery trends, and workout management. Even the strength-related tools are clearly separated by scope (single activity vs. history vs. recommendation), so an agent can reliably select the right one.

Naming Consistency4/5

Most tools follow a verb_noun pattern (list_activities, get_activity, create_strength_workout, schedule_workout, delete_workout, suggest_next_weight), but garmin_status deviates from this pattern, making the naming slightly inconsistent overall.

Tool Count5/5

With 12 tools, the server is well-scoped for its purpose: it covers activity data, health metrics, strength analysis, and workout management without unnecessary redundancy or bloat.

Completeness4/5

The server covers the main coach workflows: reading activities, analyzing strength progression, suggesting next weights, and managing workouts. Minor gaps such as listing scheduled workouts or updating an existing workout are easy to work around and do not cripple the overall tool surface.

Maintenance

ActivitySlowing
ResponsivenessNo issues