garmin-coach-mcp
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 WindowsCreates 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.pyProvisions 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 |
| Is the connection working? Whose account? |
| "Show my last N workouts" |
| Detailed metrics for one activity |
| Per-set exercise / reps / weight / est. 1RM |
| "Is my squat going up?" |
| Sleep + daily stats + training readiness |
| Resting HR, steps, stress over time |
| Progressive-overload recommendation |
| Saved workouts (name + id) |
Write (3) — set GARMIN_COACH_READONLY=1 to drop these from the manifest entirely.
Tool | What it does |
| Build a strength workout on Garmin (experimental) |
| Put an existing workout on your calendar |
| Undo / remove a workout |
Strength answers only have data if you log Strength activities with weights on your watch.
create_strength_workoutuses Garmin's unofficial workout schema — check the result in the Garmin Connect app anddelete_workoutif 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, troubleshootingDocumentation
Run it on your PC | |
Deploy to Azure with Microsoft sign-in | |
How it's built and why | |
Threat model, what protects what, rotating secrets | |
Organised by what you actually see | |
Context for AI agents working on this repo |
Scripts
Task | Command |
Set up locally |
|
Diagnose anything |
|
Deploy to Azure |
|
Register the Entra app |
|
Upload the Garmin token |
|
Rotate secrets |
|
Get the connector URL |
|
Scan for secrets |
|
Configuration
.env.example documents every setting. The ones you are most likely to touch:
Variable | Default | Meaning |
|
|
|
| off | Hide the three write tools entirely |
|
| Where the Garmin OAuth token lives |
| your machine's | Container |
|
| 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 pytest109 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.