Skip to main content
Glama
AryanP45

Garmin Health MCP Server

by AryanP45
README.md
# Garmin Health MCP Server

A **remote MCP server** that exposes your Garmin Connect health data as tools
that Claude (or any MCP-compatible client) can call. Runs over Streamable HTTP
so it can be added as a custom connector on claude.ai.

> ⚠️ **Disclaimer:** This project uses the unofficial
> [`garminconnect`](https://github.com/cyberjunky/python-garminconnect) library,
> which authenticates with your Garmin email/password instead of Garmin's
> official OAuth partner API. This approach is common for personal projects but
> is against Garmin's Terms of Service — use it knowing that risk.

---

## Features

- 10 ready-to-use health tools (steps, sleep, HR, HRV, stress, Body Battery, activities, and more)
- Bearer-token auth so only you can access your data
- Session caching — logs in once, reuses the token for months
- Docker-ready for deployment on any host (Fly.io, Render, Railway, VPS, etc.)

---

## Project structure

| File | Purpose |
|---|---|
| [`server.py`](server.py) | MCP server (FastMCP + Streamable HTTP) — defines all tools |
| [`garmin_client.py`](garmin_client.py) | Garmin session handling, retry-on-expiry |
| [`login_once.py`](login_once.py) | Run **locally** once to complete MFA and save the session |
| [`test_connection.py`](test_connection.py) | Sanity-check that the cached token is still valid |
| [`Dockerfile`](Dockerfile) | Container for deployment |
| [`.env.example`](.env.example) | Required environment variables |

---

## Available tools

| Tool | Description |
|---|---|
| `get_daily_summary(target_date)` | Steps, calories, floors climbed, resting HR, distance |
| `get_heart_rate(target_date)` | Resting HR, min/max, and full time-series |
| `get_sleep(target_date)` | Duration, stages (Deep/Light/REM/Awake), sleep score |
| `get_body_battery(target_date)` | Body Battery (energy reserve) levels |
| `get_stress(target_date)` | Stress level readings throughout the day |
| `get_hrv(target_date)` | Heart Rate Variability status |
| `get_body_composition(target_date)` | Weight, body fat %, muscle mass |
| `get_recent_activities(limit)` | Latest recorded workouts, newest first |
| `get_activity_details(activity_id)` | Full details for one activity by ID |
| `get_week_summary(start_date)` | 7-day rollup of daily summaries |

All `target_date` / `start_date` parameters accept `YYYY-MM-DD`. Omit them to default to **today**.

---

## Setup

### 1. Clone & install

```bash
git clone https://github.com/<you>/garmin-mcp-server.git
cd garmin-mcp-server
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
```

### 2. Configure environment

```bash
cp .env.example .env
# Edit .env — fill in GARMIN_EMAIL, GARMIN_PASSWORD, and MCP_AUTH_TOKEN
```

Generate a strong `MCP_AUTH_TOKEN`:

```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```

### 3. Handle MFA locally (one-time)

Garmin may challenge the first login with an MFA code. A headless server can't
answer that prompt, so do the interactive login on your own machine first:

```bash
python login_once.py
```

This saves a `garmin_tokens/` directory containing the session token. Garmin
tokens typically stay valid for months — the server auto-refreshes them.

Verify it worked:

```bash
python test_connection.py
```

### 4. Deploy the server

Build and run locally:

```bash
docker build -t garmin-mcp .
docker run -d -p 8000:8000 \
  -v $(pwd)/garmin_tokens:/data/garmin_tokens \
  --env-file .env \
  garmin-mcp
```

For cloud deployment (Fly.io, Render, Railway, etc.):

1. Mount a persistent volume at `/data` — copy your local `garmin_tokens/`
   folder into `/data/garmin_tokens` on it so the container resumes the session
   without needing a fresh login.
2. Set these environment variables on the host:
   - `GARMIN_EMAIL` and `GARMIN_PASSWORD` — fallback credentials for full re-login
   - `MCP_AUTH_TOKEN` — the secret you generated above
3. Expose port `8000`. Your MCP endpoint will be `https://your-domain.com/mcp`.

---

## Connect to Claude

1. In **claude.ai** → Settings → Connectors → **Add custom connector**
2. Enter your server URL: `https://your-domain.com/mcp`
3. Open **Advanced settings → Request headers** and add:
   - Key: `Authorization`
   - Value: `Bearer <your MCP_AUTH_TOKEN>`
4. Save, then enable the connector in a conversation.

> **Note:** Request-header auth is currently a beta feature on claude.ai. If
> you don't see it yet, check back soon or swap the middleware in `server.py`
> for OAuth.

---

## Running locally (without Docker)

```bash
source venv/bin/activate
python server.py
# Server starts at http://localhost:8000/mcp
```

---

## Notes & limitations

- **Single-user only** — this server is designed for your personal Garmin account.
- **Unofficial API** — Garmin may change internal endpoints at any time and break
  the `garminconnect` library. A quick `pip install -U garminconnect` usually fixes it.
- **Keep your token secret** — anyone with `MCP_AUTH_TOKEN` can read your Garmin
  health data through this server. Treat it like a password.
- **Token expiry** — if Garmin invalidates your session (e.g. after a password
  change), re-run `login_once.py` locally and upload/mount the refreshed token store.

---

## License

MIT