garmin-unofficial-mcp
by Danielsuri
README.md
# garmin-unofficial-mcp
<!-- Banner is an AI-generated image; it is decorative and carries no data. -->

[](https://github.com/danielsuri/garmin-unofficial-mcp/actions/workflows/ci.yml)
[](pyproject.toml)
[](LICENSE)
[](https://m8ven.ai/mcp/danielsuri-garmin-unofficial-mcp-04x645)
A local, single-user, read-only [MCP](https://modelcontextprotocol.io) server
that gives Claude access to your own Garmin health and training data. It
exposes composite tools — health snapshot, training status, recent runs, run
detail, body metrics and training analysis — so Claude can ground recovery
advice and workout recommendations in your actual sleep, HRV, training load
and run history instead of guessing. Everything Garmin returns is cached
day-by-day in a local SQLite database, so repeated questions don't repeatedly
hit Garmin's servers.
## Before you start
This uses the unofficial `garminconnect` library, which signs in with your
Garmin **password and MFA code** — not OAuth. There is no scoped, read-only
Garmin grant available to third-party apps today, so the token this stores is
full-account access. It is stored only on your own machine.
Nor is one coming soon. Garmin's Connect Developer Program is enterprise-only
and has never accepted personal-use applications; since 2026 it has **paused
new access requests entirely**, with the request form withdrawn and no
published re-open date. Garmin has also put Cloudflare TLS fingerprinting in
front of the Connect endpoints, which is why `curl_cffi` is a dependency here —
`garminconnect` uses it to present a browser-like TLS handshake. That is a
workaround, not a supported interface, and it can stop working whenever Garmin
changes the check.
The server never writes to your Garmin account: no `set_`, `add_`, `delete_`,
`upload_`, `import_`, `create_` or `remove_` call exists anywhere in it,
deliberately.
See [docs/privacy.md](docs/privacy.md) for exactly what is stored and where.
## Requirements
- Python 3.12 or newer
- A Garmin Connect account
- Optionally, the `claude` CLI, so `install.sh` can register the server for you
## Install
```bash
git clone https://github.com/danielsuri/garmin-unofficial-mcp.git
cd garmin-unofficial-mcp
./install.sh
.venv/bin/garmin-unofficial-mcp login you@example.com
.venv/bin/garmin-unofficial-mcp doctor
```
`login` prompts for your password and, if enabled, an MFA code. It needs a real
terminal — `getpass` fails without a TTY. `doctor` then checks that the stored
tokens are accepted, that a live call works, and that the cache directory is
writable.
`install.sh` creates `.venv`, installs the package into it, and finds the
`claude` CLI to register the server at user scope if found. It's idempotent —
safe to re-run any time; it never clobbers an existing venv or registration it
doesn't own. Pass `--dev` to also install the test extras, `--dry-run` to
preview every action without doing it, or `--help` for the full rundown.
On success, tokens are written to `~/.garminconnect`. They're good for roughly
a year; after that (or if Garmin invalidates them) re-run `login`.
## Check the install
```bash
.venv/bin/garmin-unofficial-mcp doctor
```
`doctor` checks three things: that the stored tokens are accepted, that a live
call to Garmin actually succeeds, and that the cache directory
(`~/.garmin-unofficial-mcp/`) is writable. Healthy output looks like:
```
auth: OK (tokenstore accepted)
live call: OK (userProfileId=12345678)
cache path: OK (/Users/you/.garmin-unofficial-mcp/cache.db)
```
Any `FAIL` line points at what to fix — usually re-running `login` if auth
fails, or a permissions problem if the cache path fails.
## Register with Claude Code
`install.sh` does this automatically when it can find the `claude` CLI. To do
it by hand instead (or if it couldn't find `claude`):
```bash
claude mcp add -s user garmin-unofficial -- <your/install/dir>/garmin-unofficial-mcp/.venv/bin/garmin-unofficial-mcp serve
```
This runs the server over stdio. Once registered, a fresh Claude Code session
will have the tools below available — MCP servers are loaded at startup, so
the session you register from won't see them.
`-s user` matters: without it `claude mcp add` defaults to *local* scope, and
the tools appear only when you're working inside this repo. User scope makes
them available in every project, which is what you want for a personal health
server.
Note the registration is an **absolute path** into this repo's venv, so moving
or deleting the repo breaks it (re-run `install.sh`, or the command above,
after moving it). Tokens (`~/.garminconnect`) and the cache
(`~/.garmin-unofficial-mcp/cache.db`) live outside the repo and are unaffected by scope.
## The tools
- **`get_health_snapshot(days=7)`** — sleep (total/deep/light/REM/awake
minutes, sleep score), HRV, body battery (the day's true high and low), resting
heart rate, and stress over the window, plus rolling baselines. Use this to
judge how recovered you are.
- **`get_training_status(weeks=4)`** — acute and chronic training load, the
acute:chronic load ratio, VO2max, and training readiness score per day. Use
this to judge whether training load is ramping safely.
- **`get_recent_runs(n=10)`** — your last `n` runs with pace (average,
moving, grade-adjusted, best), heart-rate zones, training effect and
training load, VO2max, fastest splits, PR flag, calories, body-battery
cost and elevation, plus weekly mileage rollups and race-time predictions.
Use this to spot trends and prescribe a specific run.
- **`get_run_detail(activity_id=None)`** — one run in full: everything
`get_recent_runs` reports for that run, plus running dynamics (ground
contact time and balance, vertical oscillation and ratio, stride length),
power (average/normalized/max watts and power zones), respiration
(average/max/min breaths per minute), min/max temperature, lap count,
step count, activity name, and run/walk/idle time detection
(`run_walk`: seconds spent running vs. walking vs. stopped, from Garmin's
typed splits). Omit `activity_id` for the most recent run found in the
last 56 days. Use this to deep-dive a specific run. **Costs one extra
Garmin API call** beyond `get_recent_runs` (typed splits, for run/walk
detection) — each of the other fields comes from the same activity record
`get_recent_runs` already fetches.
- **`get_body_metrics(days=30)`** — weight, body fat, steps, calories, floors
climbed, and intensity minutes, with simple weight and step trends.
- **`get_training_analysis(weeks=8)`** — the marathon-block view: within-run
form indicators (efficiency factor, aerobic decoupling, ground-contact-time
drift, duty factor, split pattern) for every run in the window, plus
across-block trends (weekly mileage, ramp rate with the base it was computed
from, HR-zone polarization, efficiency and decoupling trend, recovery
coupling, VO2max trend). It computes; it does not advise. **Costs two extra
Garmin API calls per run it hasn't seen before** (laps and typed splits),
both cached permanently.
Arguments are clamped to a range the server can actually serve — `days` to
1–365, `weeks` to 1–52, `n` to 1–100, `activity_id` to a non-negative
64-bit value — so an out-of-range value narrows or widens to the nearest
bound (or, for an `activity_id` that matches nothing, yields a clean
all-null result) rather than failing.
Every tool returns a `partial: true` flag when it couldn't be certain all of
its data is fresh or complete — see Troubleshooting below.
## Testing
```bash
.venv/bin/pytest
```
The suite covers the MCP tools, cache, fetcher and assemblers with fake
Garmin clients — no network, no real account required.
## Troubleshooting
- **Auth errors / "Garmin rejected the stored tokens"** — your tokens expired
or were revoked. Re-run `garmin-unofficial-mcp login you@example.com` from a real
terminal.
- **`partial: true` in a tool's response** — Garmin rate-limited a request or
a connection failed transiently. The tool still returns whatever it has
cached; treat the numbers as possibly incomplete or stale and try again
later rather than assuming Garmin has no data.
- **Weight fields are `null`** — this is normal on any day without a weigh-in.
`weight_kg`, `body_fat_pct`, and the weight trend fields in
`get_body_metrics` only populate on days you actually stepped on a
connected scale.
## Licence
MIT — see [`LICENSE`](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues