Skip to main content
Glama
ilyanehay
by ilyanehay
README.md
# Health Vault

Self-hosted **Xiaomi Fitness / Smart Band → SQLite → authenticated MCP** for [Grok](https://grok.com) (CLI and grok.com Custom Connector).

```text
Mi Band  →  Mi Fitness (phone)  →  Xiaomi cloud
                                      ↓
                              health sync (this project)
                                      ↓
                                   SQLite
                                      ↓
                         MCP tools (OAuth or API key)
                                      ↓
                              Grok answers with real data
```

**Not affiliated with Xiaomi or xAI.** Uses an unofficial reverse-engineered Health API. Personal / research use. Wearable data is **wellness only**, not medical advice.

---

## Requirements

| Need | Notes |
|------|--------|
| **Python 3.12+** | Required (`requires-python >= 3.12`) |
| **Xiaomi Smart Band** + **Mi Fitness** app | Band must sync to the phone app first |
| **Xiaomi account** | QR login from this tool |
| Optional: **cloudflared** or **ngrok** | For grok.com (public HTTPS) |
| Optional: **Grok CLI / Grok Build** | Local MCP with Bearer API key |

---

## Quick install

```bash
git clone <this-repo> health-vault && cd health-vault

# Option A: uv
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e .

# Option B: plain venv
python3.12 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -U pip
pip install -e .
```

CLI entrypoint: `.venv/bin/health` (or `health` after activate).

Runtime data goes to `./data/` (gitignored): Xiaomi token, SQLite DB, OAuth state.

---

## Scenario A — Local data + Grok CLI

Best for development and private use on one machine.

### 1. Pull data from Xiaomi

1. On the phone, open **Mi Fitness** and sync the band.
2. On the computer:

```bash
.venv/bin/health login    # QR → scan with Xiaomi Account app
# QR is also saved as data/qr_login.png if you cannot see the terminal
.venv/bin/health sync --days 7
.venv/bin/health status   # expect sync_health: ok
.venv/bin/health show --days 14
```

### 2. API key + MCP server

```bash
.venv/bin/health key create --name grok   # prints Bearer token ONCE — save it
.venv/bin/health serve --host 127.0.0.1 --port 8787
```

### 3. Point Grok CLI / Grok Build at the server

`~/.grok/config.toml` (or your agent’s MCP config):

```toml
[mcp_servers.health]
url = "http://127.0.0.1:8787/mcp"
headers = { Authorization = "Bearer hlt_PASTE_YOUR_TOKEN" }
```

Restart the CLI/agent so it reloads MCP. Ask something concrete, e.g. *“How did I sleep in the last 7 days?”*

### Smoke checks (A)

```bash
curl -sS http://127.0.0.1:8787/healthz
# → ok

curl -sS -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8787/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{}'
# → 401  (auth required)

.venv/bin/health tool health_status
.venv/bin/health tool health_context --hint sleep --days 7
```

---

## Scenario B — grok.com Custom Connector (tunnel)

Grok’s cloud must call a **public HTTPS** MCP URL. Easiest path: local server + [Cloudflare quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) or [ngrok](https://ngrok.com). See also [xAI tunneling notes](https://docs.x.ai/grok/connectors/custom-mcp-tunneling).

### 1. Data + owner password

```bash
.venv/bin/health login
.venv/bin/health sync --days 7
.venv/bin/health oauth set-password    # password for the browser login page
```

### 2. Serve with a public URL

**One-shot (integrated tunnel):**

```bash
# brew install cloudflared   # if needed
.venv/bin/health serve --host 127.0.0.1 --port 8787 --tunnel
```

Copy the printed URL, e.g. `https://xxxx.trycloudflare.com`.

**More reliable (two terminals):**

```bash
# Terminal 1
.venv/bin/health serve --host 127.0.0.1 --port 8787 --public-url https://PLACEHOLDER

# Terminal 2 — after Uvicorn is up
cloudflared tunnel --url http://127.0.0.1:8787
# copy https://xxxx.trycloudflare.com

# Restart terminal 1 with the real public URL (OAuth issuer must match HTTPS host):
.venv/bin/health serve --host 127.0.0.1 --port 8787 \
  --public-url https://xxxx.trycloudflare.com
```

> Free tunnel URLs often **change on restart** — update the Grok connector when they do.  
> If the public URL returns **530 / Error 1033**, wait a few seconds or use the two-terminal flow.

### 3. Connect in Grok

1. Open [grok.com/connectors](https://grok.com/connectors) → **New Connector** → **Custom**.
2. Server URL: `https://xxxx.trycloudflare.com/mcp` (must include **`/mcp`**).
3. If Grok shows an OAuth form: use **PKCE**; authorization/token endpoints from  
   `https://xxxx.trycloudflare.com/.well-known/oauth-authorization-server`;  
   scope **`health:read`** (Client Secret usually empty for public + PKCE).
4. Browser opens **Health Vault** login → enter the password from `health oauth set-password`.
5. Chat with a real question (not only “list tools”).

More detail: [docs/GROK_CONNECT.md](docs/GROK_CONNECT.md).

### Smoke checks (B)

```bash
curl -sS https://xxxx.trycloudflare.com/healthz
curl -sS https://xxxx.trycloudflare.com/.well-known/oauth-authorization-server | head
curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://xxxx.trycloudflare.com/mcp \
  -H 'Content-Type: application/json' -d '{}'
# → 401 without token
```

---

## Scenario C — VPS / home server (generic production)

Pattern: **app listens on HTTP on a private port**; **TLS and domain** are terminated by a reverse proxy (Caddy, nginx, Traefik, etc.) in front. Do **not** put secrets in git.

Example names below are placeholders — choose your own domain and port.

### 1. Install on the host

```bash
sudo mkdir -p /opt/health-vault/data
sudo chown "$USER:$USER" /opt/health-vault /opt/health-vault/data
chmod 700 /opt/health-vault/data

# copy or git clone the project into /opt/health-vault
cd /opt/health-vault
python3.12 -m venv .venv
.venv/bin/pip install -U pip
.venv/bin/pip install -e .
```

### 2. Environment file (secrets, not in git)

```bash
sudo tee /etc/health-vault.env >/dev/null <<'EOF'
HEALTH_ROOT=/opt/health-vault
HEALTH_DATA_DIR=/opt/health-vault/data
PUBLIC_BASE_URL=https://health.example.com
HOST=0.0.0.0
PORT=8002
# Optional: bootstrap owner password once if DB has none yet
OAUTH_LOGIN_PASSWORD=change-me-to-a-long-random-string
EOF
sudo chmod 640 /etc/health-vault.env
# owner should be root + the service user (e.g. root:deploy)
```

`PUBLIC_BASE_URL` **must** be the public HTTPS origin users and Grok hit (no path).

### 3. systemd unit

Example unit (also see `deploy/health-mcp.service` as a template — rename paths):

```ini
[Unit]
Description=Health Vault MCP server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=YOUR_USER
Group=YOUR_GROUP
WorkingDirectory=/opt/health-vault
EnvironmentFile=/etc/health-vault.env
ExecStart=/opt/health-vault/.venv/bin/python -m health_mcp
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/health-vault/data

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now health-vault.service
systemctl is-active health-vault
curl -sS http://127.0.0.1:8002/healthz
```

### 4. Reverse proxy (TLS)

Terminate HTTPS on the proxy and reverse-proxy to `127.0.0.1:8002` (or the app host). Preserve `Host` and `X-Forwarded-Proto`.

**Caddy example:**

```caddyfile
health.example.com {
    reverse_proxy 127.0.0.1:8002
}
```

**nginx sketch:**

```nginx
server {
    listen 443 ssl;
    server_name health.example.com;
    # ssl_certificate ...;
    location / {
        proxy_pass http://127.0.0.1:8002;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
```

Point DNS A/AAAA for `health.example.com` at your server (or your edge).

### 5. Data + Grok

```bash
cd /opt/health-vault
# if OAUTH_LOGIN_PASSWORD was not used:
HEALTH_ROOT=/opt/health-vault .venv/bin/health oauth set-password

HEALTH_ROOT=/opt/health-vault .venv/bin/health login
HEALTH_ROOT=/opt/health-vault .venv/bin/health sync --days 7
```

Optional cron (every 30 minutes):

```cron
*/30 * * * * cd /opt/health-vault && HEALTH_ROOT=/opt/health-vault .venv/bin/health sync --days 3 >>/var/log/health-sync.log 2>&1
```

Connect Grok Custom Connector to:

```text
https://health.example.com/mcp
```

### Smoke checks (C)

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://health.example.com/          # 200
curl -sS -o /dev/null -w '%{http_code}\n' https://health.example.com/healthz   # 200
curl -sS https://health.example.com/.well-known/oauth-authorization-server | head
curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://health.example.com/mcp \
  -H 'Content-Type: application/json' -d '{}'                                  # 401
```

---

## Auth model

| Method | Use case |
|--------|----------|
| **Bearer API key** (`health key create`) | Grok CLI / local agents |
| **OAuth 2.1 + PKCE + owner password** | grok.com Custom Connector |
| **Xiaomi QR token** | Ingest only — never sent to Grok |

- API keys are stored as **SHA-256 hashes**; plaintext shown once.
- OAuth access/refresh tokens hashed in SQLite under `data/`.
- Unauthenticated `/mcp` → **401**.

---

## MCP tools

All tools return `sync_health`, `attention_required`, and a wellness disclaimer.

| Tool | Purpose |
|------|---------|
| `health_status` | Last sync, coverage, token presence |
| `health_get_daily` | Daily rows (steps, sleep, HR, SpO2, stress, …), max 90 days |
| `health_context` | Compact brief for one question (`question_hint`) |
| `health_trends` | Mean / min / max / half-window delta for one metric |
| `health_anomalies` | Points outside mean ± 2σ |

If `attention_required` is true (stale/broken sync), Grok should **warn first** and suggest `health login` / `health sync`.

Env thresholds: `HEALTH_STALE_AFTER_HOURS` (default 36), `HEALTH_BROKEN_AFTER_HOURS` (default 72).

---

## CLI reference

```text
health login                         # Xiaomi QR → data/token.json
health sync [--days N] [--uid …] [--base-url …]
health status
health show [--days N]

health key create|list|revoke
health oauth set-password|status

health serve [--host] [--port] [--public-url] [--tunnel]
health tool <name> [--days N] [--hint …] [--metric …]
```

Production process entry: `python -m health_mcp` (reads env above).

---

## Layout

```text
src/health_core/       SQLite schema, queries, sync_health
src/health_ingest/     Xiaomi cloud → daily_metrics
src/health_auth/       API keys, owner password, OAuth AS
src/health_mcp/        MCP server, tools, tunnel helper
src/health_cli/        `health` CLI
vendor/mi-fitness-python/   vendored SDK (GPL-3.0)
deploy/                example systemd unit (template only)
docs/GROK_CONNECT.md   Grok connection details
data/                  runtime (gitignored)
```

---

## Troubleshooting

| Symptom | What to try |
|---------|-------------|
| Login QR invisible | Open `data/qr_login.png`; use Xiaomi **Account** app to scan |
| `Token not found` / `TokenExpiredError` | `health login` again |
| Sync OK but empty metrics | Sync the band in **Mi Fitness** on the phone; wait; re-run `health sync` |
| `not support HlthRelativesCareClient` | Expected on some accounts; own `user_id` path still works |
| Wrong region / empty cloud | Try `--base-url` / `HEALTH_API_BASE` if your account is not on default RU IDC |
| MCP always 401 with key | Use full `Bearer hlt_…` header; create a new key if revoked |
| grok.com cannot connect | Must be public **HTTPS** + path `/mcp`; OAuth password set; tunnel alive |
| Tunnel 530 / hostname fails | Restart tunnel; two-terminal setup; confirm `cloudflared` has network |
| Stale data in Grok answers | Check `health status`; run sync on a schedule |
| `attention_required` in every tool | Fix ingest; do not treat metrics as current until `sync_health.status` is `ok` |

---

## Security notes

- Never commit `data/`, `.env`, tokens, or passwords (see `.gitignore`).
- Prefer long random `OAUTH_LOGIN_PASSWORD`; rotate via `health oauth set-password`.
- Expose only HTTPS to the internet; keep the app port private when possible.
- Single-owner design: one vault password / API keys for that host.

---

## Acknowledgements

This project stands on reverse-engineering and open-source work that made Xiaomi Fitness data reachable outside the official app.

- **[alexgetmancom/miband-bot](https://github.com/alexgetmancom/miband-bot)** — personal self-hosted Telegram bot for Xiaomi Fitness / Mi Band (auth, cloud sync, SQLite, parsers). The approach and practical stack around Band 10 / Mi Fitness cloud were a major starting point for this vault. **Thank you** to the author for publishing the research and code. See also the write-up [Reverse Engineering the Xiaomi Smart Band 10](https://habr.com/en/articles/1057044/) (Habr).
- **[MistEO/MiSDK](https://github.com/MistEO/MiSDK)** (`mi-fitness` / `mi_fitness`) — Python SDK for Xiaomi Health / “relatives” APIs (QR login, RC4 transport, typed clients). Vendored under `vendor/mi-fitness-python/` (GPL-3.0).

Neither project is affiliated with this repo; any bugs here are ours.

---

## License

This project is free software under the **[GNU General Public License v3.0 or later](LICENSE)** (`GPL-3.0-or-later`).

You may run, study, share, and modify it. If you **distribute** modified versions (or a product that combines this code with the vendored SDK), you must keep them under GPL-compatible terms and preserve copyright/license notices. See [LICENSE](LICENSE) for the full text.

- Includes **`vendor/mi-fitness-python`** ([MistEO/MiSDK](https://github.com/MistEO/MiSDK)), also **GPL-3.0** — attribution required.
- Unofficial Xiaomi Health protocol — may break without notice when Xiaomi changes apps or servers.

---

## Optional reading

- [docs/GROK_CONNECT.md](docs/GROK_CONNECT.md) — Grok auth modes and endpoints  
- [PLAN.md](PLAN.md) — architecture and design notes  
- [NOTES_EXPLORE.md](NOTES_EXPLORE.md) — which metrics the API returned in testing  

Maintenance

ActivityMaintained
ResponsivenessSyncing