garmin-mcp
README.md
# garmin-mcp
An MCP server that connects Claude to your Garmin Connect account. Ask Claude things like
"how did I sleep this week?", "am I recovered enough for intervals today?" or "put a
6x800 session on my calendar for Thursday" and it reads (and, if you allow it, writes)
your Garmin data.
It uses the unofficial Garmin Connect API via
[python-garminconnect](https://github.com/cyberjunky/python-garminconnect), for your
own account only. Garmin's official developer APIs require business approval; all
Garmin calls live in `garmin_client.py` so that route can be added later.
## Tools
| Area | Tools |
|---|---|
| Profile | `get_profile` |
| Daily health | `get_daily_summary`, `get_sleep`, `get_hrv`, `get_body_battery`, `get_stress`, `get_heart_rate`, `get_body_composition`, `get_health_range` (multi-day table) |
| Training | `get_training_readiness`, `get_training_status`, `get_race_predictions`, `get_personal_records`, `list_activities`, `get_recent_activities`, `get_activity` |
| Workouts | `list_workouts`, `get_scheduled_workouts` |
| Writes (off unless `GARMIN_ALLOW_WRITES=true`) | `create_workout`, `schedule_workout`, `unschedule_workout`, `delete_workout`, `update_activity`, `add_weigh_in` |
| Prompts | `morning_check`, `weekly_review` |
Responses are trimmed summaries; every read tool accepts `raw=true` for Garmin's full JSON.
## 1. Log in to Garmin once (on your own computer)
Your password never leaves your machine and is never stored. The login script prints
a token that the server uses instead.
```bash
pipx install git+https://github.com/alyssindewonderland/garmin_mcp # or: pip install -e .
garmin-mcp-login # asks for email, password and MFA code
garmin-mcp-login --save ~/.garmin-tokens # also keep a copy for local use
```
The last line it prints is your `GARMIN_TOKENS` value. Treat it like a password: it
grants access to your Garmin account. The server refreshes it automatically and keeps
the refreshed copy in `GARMIN_TOKEN_DIR`; if it ever stops working, run the script again.
## 2a. Use it locally (Claude Desktop / Claude Code)
Claude Code:
```bash
claude mcp add garmin -e GARMIN_TOKEN_DIR=$HOME/.garmin-tokens -- garmin-mcp
```
Claude Desktop (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"garmin": {
"command": "garmin-mcp",
"env": { "GARMIN_TOKEN_DIR": "/Users/you/.garmin-tokens" }
}
}
}
```
## 2b. Deploy it so claude.ai (web, mobile) can use it
The server runs streamable HTTP at `/mcp`, protected by a random secret
(`MCP_AUTH_SECRET`). Clients either call `https://<host>/mcp/<secret>` or send
`Authorization: Bearer <secret>`. Everything else gets a 404.
### Vercel (recommended, free Hobby plan)
1. In Vercel: **Add New → Project**, import this GitHub repo, keep the defaults, and deploy.
2. In the project: **Storage → Create Database → Upstash for Redis** (free tier) and
connect it to the project. This adds `KV_REST_API_URL` and `KV_REST_API_TOKEN`, where
the server keeps refreshed Garmin tokens (Garmin rotates them, and Vercel has no disk).
3. In **Settings → Environment Variables** add:
- `GARMIN_TOKENS`: the output of `garmin-mcp-login`
- `MCP_AUTH_SECRET`: a random string of 32+ characters
(`python3 -c 'import secrets; print(secrets.token_urlsafe(32))'`)
- optionally `GARMIN_ALLOW_WRITES=true`
4. Redeploy (Deployments → ⋯ → Redeploy) so the variables take effect. Every push to
`main` deploys automatically after that.
Your server is at `https://<project>.vercel.app/mcp/<MCP_AUTH_SECRET>`.
### Fly.io
```bash
fly launch --no-deploy --copy-config # pick a unique app name, keep fly.toml
fly volumes create garmin_data --size 1
fly secrets set \
GARMIN_TOKENS='<output of garmin-mcp-login>' \
MCP_AUTH_SECRET="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" \
MCP_ALLOWED_HOSTS=<app>.fly.dev
fly deploy
```
### Anywhere else
Any Docker host with HTTPS in front works (Railway, Render, a VPS, a home server behind
Cloudflare Tunnel). Run the image with the same env vars and a persistent volume at `/data`:
```bash
docker build -t garmin-mcp .
docker run -d -p 8000:8000 -v garmin_data:/data \
-e GARMIN_TOKENS='...' -e MCP_AUTH_SECRET='...' garmin-mcp
```
### Add the connector in Claude
In claude.ai: **Settings → Connectors → Add custom connector**, name it "Garmin", and use
`https://<host>/mcp/<MCP_AUTH_SECRET>` as the URL. Then enable it in a chat from the
tools menu.
For Claude Code against the deployed server:
```bash
claude mcp add --transport http garmin https://<host>/mcp --header "Authorization: Bearer <secret>"
```
## Configuration
| Variable | Default | Purpose |
|---|---|---|
| `GARMIN_TOKENS` | | Token JSON from `garmin-mcp-login` |
| `GARMIN_TOKEN_DIR` | `~/.garmin-tokens` | Where refreshed tokens are kept (use a volume) |
| `KV_REST_API_URL`, `KV_REST_API_TOKEN` | | Upstash Redis for refreshed tokens instead of disk (also `UPSTASH_REDIS_REST_URL`/`_TOKEN`) |
| `GARMIN_EMAIL`, `GARMIN_PASSWORD` | | Fallback login; fails if your account has MFA |
| `GARMIN_IS_CN` | `false` | Use garmin.cn |
| `GARMIN_ALLOW_WRITES` | `false` | Enable write tools |
| `GARMIN_CACHE_TTL` | `300` | Seconds to cache Garmin responses |
| `MCP_AUTH_SECRET` | | Required for HTTP mode, 24+ characters |
| `MCP_ALLOWED_HOSTS` | | Comma-separated public hostnames for Host header checks |
| `HOST`, `PORT` | `0.0.0.0`, `8000` | HTTP bind |
## Development
```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest && ruff check .
```
Tests use a fake Garmin client, so they never touch your account.
## Caveats
- This uses Garmin's private API. Garmin can change it at any time; pin and update
`garminconnect` when things break. Use it for your own account only.
- Logins from cloud data-center IPs are more likely to be challenged, which is why login
happens on your computer and the server only refreshes tokens.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues