garmin-mcp
README.md
# Garmin MCP
Your Garmin Connect data — sleep, HRV, recovery, training load, workouts —
available to any AI that speaks MCP. Runs entirely on your own machine. Your
Garmin credentials never leave it.
Works with Claude, ChatGPT, Perplexity, Le Chat, Cursor, Zed, or anything else
that supports MCP. No subscription, no third-party server.
---
## Setup
Open Terminal (press ⌘+Space, type "Terminal", hit Enter) and paste this:
```bash
cd ~/Downloads && git clone https://github.com/aayush-lunawat/garmin-mcp.git && cd garmin-mcp && bash install.sh
```
That installs the dependencies, signs you in to Garmin, verifies the connection
and prints the config block to paste into your AI app. It is safe to re-run —
it skips the sign-in step if you're already signed in.
If you'd rather see each step, or you want to know what the prompts mean before
you hit them, [SETUP.md](SETUP.md) is the same process written out in plain
language with the failure modes explained.
<details>
<summary>Or run the four steps by hand</summary>
**1. Go to this folder.** Don't type a path by hand — let Finder supply it:
1. Type `cd` followed by **one space**. Don't press Enter yet.
2. Drag the `garmin-mcp` folder from Finder onto the Terminal window and let go.
The full path appears by itself.
3. Now press Enter.
The finished line looks something like `cd /Users/you/Downloads/garmin-mcp` —
the exact path depends on where the folder actually is on your Mac.
If Terminal says `no such file or directory`, the path is wrong. Drag the
folder in again rather than retyping it; that method can't produce a typo.
To confirm you're in the right place, run `ls`. You should see `setup.sh`,
`login.py`, `doctor.py` and `README.md` listed.
**2. Install.** Creates a self-contained Python environment inside this folder
and installs the two dependencies. Nothing is installed system-wide. If you
don't have a recent enough Python, this tells you how to get one.
```bash
bash setup.sh
```
**3. Sign in to Garmin.** Asks for your Garmin email and password, plus a
verification code if you have two-factor enabled. Your password is exchanged
for an access token and then discarded — it is never written to disk. The token
lands in `~/.garmin-mcp/tokens` and lasts about a year.
```bash
.venv/bin/python login.py
```
**4. Check it works.** Calls every endpoint against your real account and
reports what came back.
```bash
.venv/bin/python doctor.py
```
Then get the config block for your AI app:
```bash
.venv/bin/python doctor.py --config
```
In **Claude Desktop**: Settings → Developer → Edit Config, paste it in, save,
restart Claude. Other apps take the same `command` and `args`.
Ask it *"how did I sleep this week?"* to confirm.
</details>
---
## What you can ask
The tools are designed around questions, not endpoints:
- *"Should I run today?"* → readiness, HRV vs. baseline, yesterday's load
- *"Review my training week"* → activities with load, effect and HR zones
- *"Is my sleep getting worse?"* → 14 days of stages, score and overnight HRV
- *"Why did Saturday feel so hard?"* → that workout's splits against recovery
- *"Am I building fitness or just tired?"* → training status and acute:chronic load
## Tools
| Tool | Returns |
|---|---|
| `get_readiness` | Training readiness score and its contributing factors |
| `get_hrv` | Overnight HRV against your personal baseline |
| `get_body_battery` | Charge, drain, peak and trough per day |
| `get_stress` | All-day stress average and peak |
| `get_sleep` | Duration, stages, score, SpO₂, respiration, overnight HRV |
| `get_training_status` | Status, VO₂ max, load balance, acute:chronic ratio |
| `get_activities` | Workouts by date range or the most recent N |
| `get_activity_detail` | Lap splits and HR zones for one workout |
| `get_daily_summary` | Steps, resting HR, calories, intensity minutes |
| `get_body_composition` | Weight and composition from smart-scale weigh-ins |
| `get_race_predictions` | Predicted 5K / 10K / half / marathon times |
| `get_profile` | Connection check — whose account, which timezone |
| `get_raw` | Diagnostic: unshaped response from any read endpoint |
| `refresh` | Clear the 5-minute cache after a fresh watch sync |
Date arguments accept `YYYY-MM-DD`, `today`, or `yesterday`. Most tools also
take `days=N` for a trailing window. Everything is read-only — nothing in this
server writes to your Garmin account.
---
## How it's built
Three layers, separated so the fragile part is isolated from the stable part:
```
server.py MCP tools and their descriptions. Transport-specific.
|
client.py Fan-out, caching, error tolerance. Reusable as-is.
|
auth.py Where credentials live. Swap this to go multi-user.
```
`auth.py` is the only file that knows about token storage. It exposes
`get_client(user_id)`. A hosted version implements that same method against an
encrypted per-user store, and `client.py`, `shaping.py` and `dates.py` carry
over untouched. `server.py` changes one line — `transport="stdio"` becomes
`transport="streamable-http"` — plus an auth middleware.
Two decisions worth knowing about:
**Payloads are compacted before the model sees them.** A raw Garmin sleep
response is several thousand tokens, mostly nulls, internal IDs and per-minute
movement arrays. `shaping.py` reduces it to about twenty fields. Three
unshaped calls would otherwise crowd out the conversation.
**Missing data stays missing.** A field Garmin didn't send is absent from the
output rather than defaulted to zero, because "no HRV reading" and "HRV of
zero" mean very different things and a model will happily average the latter.
---
## The fragility you should know about
Garmin has no public API for this data. Everything here talks to the same
private endpoints the Garmin Connect website uses.
In March 2026 Garmin enabled Cloudflare TLS fingerprinting, which blocks
ordinary HTTP clients. This broke `garth`, the library nearly every Garmin
integration depended on, and it was
[deprecated](https://github.com/matin/garth/discussions/222) as a result.
This server depends on `python-garminconnect` 0.3.x, which survived by
rebuilding authentication on `curl_cffi` — it impersonates a real browser's TLS
handshake and falls back across four different login strategies.
It works today. It is also cat-and-mouse: Garmin can tighten the check again.
The same risk applies to every paid service in this space; the difference is
that you can patch this one yourself. If logins start failing, check the
[python-garminconnect issues](https://github.com/cyberjunky/python-garminconnect/issues)
and update:
```bash
.venv/bin/pip install --upgrade garminconnect
```
---
## Troubleshooting
**"No Garmin tokens found"** — run `.venv/bin/python login.py`.
**Login fails with 429 or Cloudflare** — Garmin is rate limiting. Wait 15–30
minutes. Retrying immediately extends the block.
**A tool returns nothing** — usually means your watch doesn't record that
metric, or the day predates it. `doctor.py` shows which endpoints have data.
**Claude doesn't see the server** — the config needs absolute paths, which is
what `doctor.py --config` generates. Restart the app fully after saving.
**A tool returns less than it should** — Garmin renamed a field. Ask your AI to
call `get_raw` with the matching method (e.g. `get_sleep_data`) and a date, then
fix the field name in `shaping.py`.
## More than one account
A token authenticates exactly one Garmin account, but nothing stops you running
one server per account and pointing the same AI at all of them — useful for a
couple, or a coach with a few athletes.
Both `login.py` and the server read `GARMIN_MCP_TOKENS`, so a second account is
one command. Log in as them:
```bash
GARMIN_MCP_TOKENS=~/.garmin-mcp/tokens-partner .venv/bin/python login.py
```
It prints the destination before asking for credentials, and warns before
replacing an existing token.
Then add a second entry to your AI config, identical to the first apart from
the name and the `env` block:
```json
"garmin-partner": {
"command": "/absolute/path/to/garmin-mcp/.venv/bin/python",
"args": ["-m", "garmin_mcp.server"],
"env": { "GARMIN_MCP_TOKENS": "/Users/you/.garmin-mcp/tokens-partner" }
}
```
Tools are namespaced per server, so both sets stay distinct and you can ask
things like *"compare our sleep this week"*. Each account revokes
independently — delete just that token directory.
Their health data reaches whichever AI you've connected, exactly as yours does.
Worth them agreeing to that explicitly.
## Tests
No Garmin account needed, and no tokens are read. Paste the whole block from
inside the `garmin-mcp` folder:
```bash
.venv/bin/pip install -q -r requirements-dev.txt # one time: installs pytest
.venv/bin/python -m pytest tests/ -q # date maths and payload shaping
.venv/bin/python tests/test_protocol.py # full MCP handshake over stdio
```
`pytest` is deliberately kept out of `requirements.txt` so that running the
server doesn't drag in test tooling. If you skip the first line you'll get
`No module named pytest`.
## Privacy
Your password is used once, in memory, and discarded. Tokens are stored at
`~/.garmin-mcp/tokens` with owner-only permissions. Health data flows from
Garmin to your machine to whichever AI app you connect — never through anyone
else's server. Delete the token folder to revoke access:
```bash
rm -rf ~/.garmin-mcp
```
Note that when you ask a cloud AI a question, the data it fetches does go to
that provider as part of the conversation, the same as anything else you type.
Only a fully local model avoids that.
## License
MIT — see [LICENSE](LICENSE). Use it, change it, redistribute it. No warranty:
Garmin's private endpoints can change without notice and this will break when
they do.
This project is not affiliated with, endorsed by, or connected to Garmin Ltd.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues