Metadata-Version: 2.4
Name: vaultbeat-mcp
Version: 0.3.1
Summary: Local MCP server for Vaultbeat — AI Health Sync: your AI agent reads your end-to-end-encrypted Apple Health data (sleep, cycle, weight, water), decrypted only on your machine
Project-URL: Homepage, https://vaultbeat.app
Project-URL: Repository, https://github.com/Fino-wind/vaultbeat-mcp
Project-URL: Issues, https://github.com/Fino-wind/vaultbeat-community/issues
Project-URL: Documentation, https://vaultbeat.app/mcp
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: cryptography<50.0.0,>=48.0.1
Requires-Dist: httpx<1.0.0,>=0.28.0
Requires-Dist: keyring>=24
Requires-Dist: mcp<2.0.0,>=1.28.1
Provides-Extra: dev
Requires-Dist: pytest<9.0.0,>=8.3.0; extra == "dev"
Requires-Dist: ruff<1.0.0,>=0.11.0; extra == "dev"
Requires-Dist: mypy<2.0.0,>=1.13.0; extra == "dev"
Requires-Dist: uvicorn>=0.30.0; extra == "dev"
Provides-Extra: qr
Requires-Dist: qrcode<9.0.0,>=8.0.0; extra == "qr"

# Vaultbeat Local MCP Server

Local service program for Vaultbeat's encrypted health-data recipient flow.
Published externally as [`Fino-wind/vaultbeat-mcp`](https://github.com/Fino-wind/vaultbeat-mcp)
(public package name `vaultbeat-mcp`; `vaultbeat-mcp-local` remains a back-compat console
script). This directory is the source of truth — after any user-visible change here,
re-export the public repo and update its README tool table + the website `/mcp` page
(see AGENTS.md "Sync duty").

It runs on the user's computer, generates the Curve25519 keypair used by the iOS app,
shows a QR binding payload, receives a one-time server token from the cloud API, and
then exposes decrypted health data — sleep, water, weight, cycle, activity, vitals —
through either a CLI or a stdio MCP server. Read-only: data is written by the iOS app.

## Commands

```bash
python -m pip install -e './mcp-local-server[qr]'
vaultbeat-mcp bind
vaultbeat-mcp status

# read decrypted health data — every data subcommand accepts
# --owner <user-id prefix> to filter to one person (omitting it mixes
# both partners' records into one pool; aggregates become meaningless)
vaultbeat-mcp sleep --limit 5 --owner dce9        # sleep sessions + provenance
vaultbeat-mcp sleep-detail --limit 1 --owner dce9 # HR+RR+stage timeline
vaultbeat-mcp water --limit 30 --owner dce9       # water intake + daily average
vaultbeat-mcp weight --limit 90 --owner f835      # weight trend (latest/avg/weekly rate)
vaultbeat-mcp menstrual --limit 60 --owner f835   # menstrual cycle (sensitive)
vaultbeat-mcp activity --limit 30 --owner dce9    # daily activity rings
vaultbeat-mcp resting-hr --limit 30 --owner dce9  # resting heart rate
vaultbeat-mcp workouts --limit 20 --owner dce9    # workout records
vaultbeat-mcp mindfulness --limit 30 --owner dce9 # mindful minutes
vaultbeat-mcp hrv --limit 30 --owner dce9         # HRV / SDNN (hourly buckets by default; --granularity raw for per-sample)
vaultbeat-mcp wrist-temp --limit 30 --owner dce9  # sleeping wrist temperature
vaultbeat-mcp symptoms --limit 30                 # symptom days (grouped by owner)
vaultbeat-mcp notes --kind sleep --limit 30       # free-text day annotations

# run as an MCP server
vaultbeat-mcp serve --transport stdio
vaultbeat-mcp serve --transport http --host 127.0.0.1 --port 8000 --path /mcp
```

`http` is a CLI alias for MCP's `streamable-http` transport.
The default transport remains `stdio` for local desktop MCP clients.

The config file defaults to `~/.tether/mcp-local/config.json` and is written with
`0600` permissions. It contains the local private key and the cloud-issued server
token; do not commit or share it.

> **`.tether`, not `.vaultbeat` — that is deliberate, do not "fix" it.** The app
> was renamed but this path is frozen at the pre-rename location, because the
> Keychain username embeds the resolved config path (`_keychain_username` in
> `store.py`). Moving the directory orphans the bound config *and* its
> private-key Keychain entry for every existing install. Until 2026-07-28 this
> README wrote `~/.vaultbeat/...`, which does not exist — so anyone who came here
> to destroy their credentials `rm -rf`'d an empty path, got no error, and left
> the real key in place.

When using HTTP transport, the server binds to `127.0.0.1:8000` and serves MCP at
`/mcp` by default, and requires a bearer token (see "Authenticating HTTP transport"
below). Binding a non-loopback address fails closed unless you pass both a token and
`--allow-remote`; always front a network-exposed server with TLS (a reverse proxy).

## Binding Flow

1. `vaultbeat-mcp bind` generates a fresh `pollID` and prints a QR payload:
   `{"pollID":"...","publicKeyBase64":"...","serverName":"..."}`
2. The iOS app scans that payload and calls the `mcp-bind-local` Edge Function.
3. The local service polls the `mcp-poll-binding` Edge Function.
4. Once bound, the local config stores `serverID` and `serverToken`.
5. All read commands call the `mcp-sync` Edge Function, decrypt the returned envelopes
   locally, and return plaintext JSON. (All privileged routes are Supabase Edge
   Functions at `/functions/v1/<name>`.)

### Troubleshooting: `vaultbeat-mcp doctor`

If binding or reads fail, run the self-diagnosis:

```bash
vaultbeat-mcp doctor          # human-readable [OK]/[FAIL] checklist with hints
vaultbeat-mcp doctor --json   # machine-readable, for agents
```

It checks, in order: config file → identity key (Keychain) → cloud reachability →
binding state → a real fetch-and-decrypt round trip, and prints a targeted hint for
the first thing that's broken (e.g. "codes expire after 10 minutes — re-run bind for
a fresh QR", or "the stored key can no longer decrypt your data — delete this server
in the iOS app and bind again"). Exit code 0 = all healthy, 1 = something needs the
hint above.

## MCP Tools (24)

`vaultbeat-mcp serve` can start either a stdio MCP server or a streamable HTTP MCP
server. Every data tool accepts `owner` (user-ID prefix) to filter to one person and
`fresh` to bypass the local cache — omit `owner` and both partners' records mix into
one pool, so per-person analysis must always pass it. The tool names dropped the old
misleading `get_partner_*` prefix in the 16-tool release (2026-07-16): the tools
return whichever owners' envelopes this server holds, not specifically "the partner".

Binding / status:

- `vaultbeat_status` — local binding state (no keys/tokens in the result)
- `vaultbeat_start_binding` — generate a fresh QR binding payload
- `vaultbeat_poll_binding` — poll once for the iOS authorization

Health data:

- `vaultbeat_sync_sleep` — recent sleep records (incl. heart-rate samples) with per-day
  primary-session selection matching the iOS app
- `get_sleep_detail` — per-night HR+RR+stage timeline with stage intervals
- `get_water_intake` — recent daily intake + computed `average_daily_intake_liters`
- `get_weight_trend` — daily weights + latest/avg/min/max + OLS weekly rate
- `get_menstrual_cycle` — recent cycle samples + a next-period prediction (sensitive)
- `get_symptoms` — recent HealthKit symptom days grouped by data owner (sensitive)
- `get_notes` — free-text sleep/menstrual day annotations with their writer (sensitive)
- `get_strength_log` — strength-training sessions with exercise-level sets × reps and
  per-session `total_volume_kg` (owner's own sessions only; logged manually in the app)
- `get_activity` — daily activity rings (steps/energy/exercise/stand/distance)
- `get_resting_hr` — resting heart rate records + mean
- `get_workouts` — workout records (type/duration/calories/distance)
- `get_mindfulness` — mindful sessions per day
- `get_hrv` — HRV/SDNN records + mean. `granularity="hourly"` (default, `hrv_hourly` kind, one bucket/UTC hour w/ `sample_count`, 30-day window) or `"raw"` (`hrv` kind, per-sample, 3-day window)
- `get_wrist_temp` — sleeping wrist-temperature baseline deviation

(The health-memory fact tools — `health_recall_*` / `health_remember` — were deleted with
the fact system in `491c850`, 2026-06-29: long-lived health knowledge lives in local
markdown managed by the user's agent, not in an E2EE cloud round trip.)

Every health kind shares one decryption path (Curve25519 ECDH + HKDF-SHA256 + AES-GCM);
the server routes on `encrypted_sleep_blobs.metric_type` (17 kinds — see
`KNOWN_METRIC_TYPES` in `service.py`) and only the per-kind JSON decode/aggregate
differs. The same service-layer functions back both the MCP tools and the matching CLI
subcommands — no duplicated logic.

**Local record cache (2026-07-09):** all reads are cache-first. Decrypted records are
kept per metric type under `~/.tether/mcp-local/cache/` (owner-only 0600 files, 0700
dir, stamped with server_id + fetch time + the fetch's decrypt-error list). Default TTL
600 s — override with `VAULTBEAT_MCP_CACHE_TTL` (0 disables). Within the TTL a repeat query
is answered locally with zero network (~0.2 s vs 5-35 s); pass `--fresh` (CLI) or
`fresh=true` (MCP tools) to force a cloud round trip. (Re)binding clears the cache.
`mcp-sync` also accepts `?metric_type=` so single-metric fetches stop paying for every
other kind's ciphertext; the client keeps its own post-decrypt filter, so older edge
deployments stay correct.

Menstrual data is **sensitive**: it only reaches this server when the user explicitly
opted in on iOS (absent otherwise), is decrypted locally, and is never re-exported.

The MCP server never exposes the private key or server token through tool results.

### Transport Options

Stdio transport, for local MCP clients that launch the server as a subprocess:

```bash
vaultbeat-mcp-local serve --transport stdio
```

HTTP transport, for MCP clients that connect over a network or reverse proxy:

```bash
vaultbeat-mcp-local serve --transport http --host 127.0.0.1 --port 8000 --path /mcp
```

Optional HTTP flags:

- `--sse-response` to use SSE-style HTTP responses instead of JSON responses.
- `--stateful-http` to disable stateless HTTP mode for clients that require sessions.
- `--generate-token` to mint and persist a bearer token, print client config, then exit.
- `--show-token` to print the stored bearer token and exit.
- `--allow-remote` to permit a non-loopback bind (requires a token; confirms intent).
- `--no-token` to serve loopback HTTP without bearer auth.

### Authenticating HTTP transport

The HTTP tool surface exposes **decrypted** health data, so it is gated by a static
bearer token and refuses to bind a network-reachable address without explicit opt-in.

Generate (and persist) a token, then print ready-to-paste client config:

```bash
vaultbeat-mcp-local serve --generate-token
```

Serve over HTTP on loopback. Auth is on by default; the token is read from
`VAULTBEAT_MCP_HTTP_TOKEN` (preferred, keeps it out of shell history) or the stored config:

```bash
vaultbeat-mcp-local serve --transport http              # 127.0.0.1, bearer required
vaultbeat-mcp-local serve --transport http --no-token   # loopback only, no auth
```

Clients send the token as a request header:

```
Authorization: Bearer <token>
```

Example `mcp.json` (VS Code / Cursor style):

```json
{
  "servers": {
    "vaultbeat-local": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

**Binding beyond loopback** (e.g. `--host 0.0.0.0` for LAN/VPS) fails closed: it
requires both a token *and* the explicit `--allow-remote` flag. The token crosses the
wire in clear text, so you must terminate TLS in front of it (e.g. Caddy / Cloudflare /
nginx):

```bash
VAULTBEAT_MCP_HTTP_TOKEN=<token> vaultbeat-mcp-local serve \
  --transport http --host 0.0.0.0 --allow-remote
```

Claude Desktop's config only speaks stdio, so bridge it to the HTTP server with
[`mcp-remote`](https://github.com/geelen/mcp-remote):

```json
{
  "mcpServers": {
    "vaultbeat-local": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://127.0.0.1:8000/mcp",
        "--header", "Authorization: Bearer <token>"
      ]
    }
  }
}
```

Reveal the stored token any time with `vaultbeat-mcp-local serve --show-token`.

## Verification

```bash
python -m pytest -q mcp-local-server/tests
python -m ruff check mcp-local-server/src mcp-local-server/tests
python -m mypy mcp-local-server/src
```
