Skip to main content
Glama
README.md
# garmin-mcp

Export your Garmin Connect data (built for the Garmin Cirqa, works with any Garmin device) into a
local SQLite database, then read it from a React dashboard or from an AI agent through an
[MCP](https://modelcontextprotocol.io) server. Everything runs on your own machine.

## How it works

A Python script pulls your data twice a day with the community
[python-garminconnect](https://github.com/cyberjunky/python-garminconnect) library, reusing a cached login
token instead of a password. Every API response is first stored untouched in SQLite; replayable
transformers then turn those responses into clean tables (daily summary, sleep stages, HRV, readiness,
training status, and per-minute series for heart rate, stress, Body Battery and SpO2). Views on top
(`v_day`, `v_week`, `v_trends` with 7/28-day baselines and z-scores) and a `metric_catalog` table describe
what each metric means. One read-only service layer feeds two front ends: a FastAPI server with a React
dashboard, and an MCP server whose tools (`get_day`, `compare_periods`, `get_sleep_detail`,
`get_timeseries`, a restricted `run_sql`, ...) let an agent answer questions such as "how did I sleep this
week versus last week, and why?" against your own baselines. The MCP server runs over stdio, so there is no
open network port and no credentials, and it cannot read the raw payloads (they contain GPS coordinates
and your name).

```mermaid
flowchart LR
    W["Garmin band"] --> GC["Garmin Connect<br/>(cloud)"]

    subgraph Machine["Your machine"]
        T["systemd timer<br/>twice a day"] --> S["garmin-optimizer sync<br/>(cached token)"]
        S --> RAW[("raw_response<br/>JSON as received")]
        RAW --> TR["Transformers<br/>(replayable)"]
        TR --> DB[("SQLite garmin.db<br/>tables, views,<br/>metric_catalog")]
        DB --> SVC["Service layer<br/>(read-only)"]
        SVC --> API["FastAPI<br/>127.0.0.1:8000"]
        SVC --> MCP["MCP server<br/>(stdio, 10 tools)"]
        API --> UI["React dashboard"]
        AG["AI agent<br/>(Claude Code, Hermes, ...)"] -- "starts on demand" --> MCP
    end

    GC -- "HTTPS" --> S
```

**Status:** early. Export (login, dump, sync, backfill), the transformers into clean tables, a
read-only HTTP API, an MCP server for AI agents and a React dashboard work. Manual inputs (food,
weight, training log) are next. Activities and body composition are implemented from the
library's documented fields; activities are verified against a real workout, weigh-ins are not yet.

## Disclaimer
This uses the unofficial [python-garminconnect](https://github.com/cyberjunky/python-garminconnect)
library, which talks to the same private API as the Garmin Connect app. It is not endorsed by
Garmin, may break when Garmin changes its login, and may be against their terms of service. Use it
for your own data at personal scale. Everything stays on your machine; if you connect an AI agent,
the data it reads is sent to that agent's provider.

## Quick start
```bash
uv sync
uv run garmin-optimizer login                       # once: asks for email, password, MFA code
uv run garmin-optimizer dump --date 2026-10-16      # raw JSON per endpoint in data/dumps/
uv run garmin-optimizer sync                        # store new days in data/garmin.db
uv run garmin-optimizer backfill --since 2026-01-01 # resumable history
uv run garmin-optimizer rebuild                     # re-derive all tables from stored raw data
uv run garmin-optimizer doctor                      # what has been fetched, per endpoint
```
Only the cached token is used after `login`; a password is never stored.

## Automatic sync (Linux, systemd)
```bash
./deploy/install-systemd.sh              # installs a user timer: 07:20 and 21:40 local time (+ up to 20 min random delay)
./deploy/install-systemd.sh --uninstall
```
It runs as your user (no root), uses the cached token only, and catches up after the machine was off
(`Persistent=true`). Useful commands:
```bash
systemctl --user list-timers garmin-optimizer-sync.timer   # next run
systemctl --user start garmin-optimizer-sync.service       # run now
journalctl --user -u garmin-optimizer-sync -e              # logs
systemctl --user --failed                                  # did the last run fail?
uv run garmin-optimizer doctor                             # token, last sync time, per-endpoint counts
```
A failed run (expired token, Garmin 429) is not retried immediately: the next scheduled run resumes where
it stopped. If the token expires, run `garmin-optimizer login` again. Each run re-fetches the last 3 days;
if your band went more than 3 days without syncing to the phone, use `backfill --since <date>` once.
Change the times with `systemctl --user edit --full garmin-optimizer-sync.timer`.

## Using the data
```bash
uv run garmin-optimizer api                  # dashboard at http://127.0.0.1:8000/ (after building web/, see web/README.md)
                                             # API docs at /docs. Read-only, localhost only.

# AI agents (MCP). Run from anywhere; --directory makes the relative data/ folder resolve:
claude mcp add garmin -- uv --directory /path/to/garmin-mcp run garmin-optimizer mcp

# Hermes Agent (use the absolute path to uv: the gateway service has its own PATH)
hermes mcp add garmin --command /path/to/uv --args --directory /path/to/garmin-mcp run garmin-optimizer mcp
```
The dashboard has no login and serves health data, so the API binds to localhost only. To view it from
another machine, use an SSH tunnel: `ssh -L 8000:127.0.0.1:8000 user@host`.
The MCP server offers `status`, `get_day`, `get_range_summary`, `compare_periods`, `get_sleep_detail`,
`get_timeseries`, `get_activities`, `list_metrics` and a read-only `run_sql`. `run_sql` can read the
cleaned tables and the views `v_day`, `v_week`, `v_trends`, but never the raw API payloads (they
contain GPS coordinates and your name). Intraday times are returned in local time.

## Privacy
- Your password is never stored; only the session token is, in `~/.garminconnect/` (mode 600). Anyone who can read that file can access your Garmin account, so protect it.
- `data/` (database, raw dumps), `.env` and tokens are git-ignored. Tests use made-up payloads.
- An AI agent that reads this data sends what it reads to its provider, and may store it in its own memory.

## Development
```bash
uv sync
uv run ruff check . && uv run pytest        # Python
cd web && npm ci && npm run typecheck && npm test && npm run build   # dashboard
```

## Adding an endpoint
Add one `Endpoint` entry in `src/garmin_optimizer/garmin/endpoints.py`. A transformer that turns
the raw JSON into tables goes in `src/garmin_optimizer/transform/`.