Skip to main content
Glama
jhoy1020

garmin-coach-mcp

by jhoy1020

garmin-coach-mcp

An MCP server that lets Claude read and answer questions about your Garmin Connect 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

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:

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.

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

Run it on your PC

docs/remote-setup.md

Deploy to Azure with Microsoft sign-in

docs/architecture.md

How it's built and why

docs/security.md

Threat model, what protects what, rotating secrets

docs/troubleshooting.md

Organised by what you actually see

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 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

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.