Skip to main content
Glama
Fino-wind

Vaultbeat MCP Server

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
TETHER_MCP_CACHE_TTLNoOverride the default cache TTL (600 seconds). Value is a number in seconds.600

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
vaultbeat_statusA

Return local Vaultbeat binding state without exposing private keys or server tokens.

vaultbeat_doctorA

Diagnose this Vaultbeat MCP install, and report which data types are unavailable.

Call this when a read tool returns nothing, or when anything fails, before telling the user their data is missing. Two distinct things come back:

checks — the install/binding chain: config present, keypair usable, binding valid, cloud reachable, a real record decryptable end to end. A failure here means the setup is broken, not that data is absent.

capabilities — which metric kinds actually have data, and which empty ones are explained by an older iOS app (with the release date each needs). An empty kind is NOT proof the user never recorded it: their app may predate the feature entirely.

scope — what this report does NOT cover. Everything here can pass and the setup still be broken on the client side: this server is a subprocess of your MCP client, so it cannot read the client's config file, cannot see which environment variables the client forwarded, and cannot tell whether it was launched with the arguments the user believes. A green report never clears the client. scope.env_overrides_received lists which Vaultbeat variables actually arrived — check that before guessing at the client's environment.

This runs a cloud round trip, so it is slower than vaultbeat_status (local config only) — prefer status for a quick liveness check.

vaultbeat_sync_sleepA

Fetch encrypted Vaultbeat sleep records, decrypt them locally, and return per-day primary session summaries matching the iOS app's display.

Returns daily_summary (one primary session per local date, selected by iOS priority: Watch > iPhone > inBedOnly) and sessions (all raw records). The limit controls how many raw blobs are fetched; 50 covers ~2-3 weeks. Use owner prefix to filter by person — take it from vaultbeat_status (owner_user_id_prefix is the paired user; server 0.7.1+), or vaultbeat_doctor for every owner present in the data. Without it, both partners' data is mixed and per-day selection may pick the wrong person's session.

⚠️ is_in_bed_only: true means sleep was NEVER MEASURED that night (the Watch wasn't worn) — NOT that the person slept zero. On those nights total_sleep_minutes is 0, duration_label reads "no sleep data", and in_bed_minutes holds the time actually recorded in bed. Report such a night as "no sleep data (in bed ~Xh)", never as "slept 0 hours".

Results are served from a short-lived local cache (default 10 min); pass fresh=True to force a cloud round trip.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

🔑 Pass summary_only=True when you only want the SIDE EFFECT. A default call returns every decrypted session — measured at 76,446 characters, which overflows a typical tool-result limit and gets spilled to a file the caller then has to read back. That is the right shape when you want the nights; it is pure waste when you called this to make the server do something (force a sync, check the link is alive, confirm a deploy took effect), which is a large share of real calls. summary_only=True returns the counts, the day range and the coverage block, and nothing else. For the nights themselves, get_sleep_detail is the tool that exists for it.

get_water_intakeA

Decrypt recent daily water intake locally and compute the average.

Returns one entry per day (newest first) with refill count, container volume, and derived intake in liters, plus average_daily_intake_liters over the window. Use owner prefix to filter by person — take it from vaultbeat_status (owner_user_id_prefix is the paired user; server 0.7.1+), or vaultbeat_doctor for every owner present in the data. Each record carries owner_user_id to identify whose data it is.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_weight_trendA

Decrypt recent body-weight records locally and compute the trend.

Returns one entry per day (newest first, kilograms) plus latest/average/min/max, the OLS weekly rate (kg/week), and — when goal_kg is given — the distance to goal. Use owner prefix to filter by person — take it from vaultbeat_status (owner_user_id_prefix is the paired user; server 0.7.1+), or vaultbeat_doctor for every owner present in the data. Each record carries owner_user_id to identify whose data it is.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_symptomsA

Decrypt recent HealthKit symptom days locally, grouped by data owner.

SENSITIVE: symptom data (cramps, headache, fatigue, coughing…) only reaches this server when a user explicitly opted in on iOS — their own AI toggle for their own data, or the partner-AI toggle for a partner's data. Both partners can track symptoms, so each entry in owners carries owner_user_id plus per-type counts and day-by-day samples with severity (mild/moderate/severe/present/…). Stays on-device, never re-exported.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_notesA

Decrypt recent free-text notes (day annotations) locally.

SENSITIVE free text. Each note carries owner_user_id (who wrote it), target_kind, and target_date (the local day it annotates) — join against the same-day metric data for pattern analysis. Kinds: "sleep" | "menstrual" are written manually in the iOS app by either partner (e.g. "昨晚舍友很吵" on a sleep day); "mood" | "general" are agent-authored via log_note. Pass target_kind to filter. Stays on-device, never re-exported.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_strength_logA

Decrypt recent strength-training sessions locally (newest first).

Exercise-level detail HealthKit's workout type cannot carry: each session lists exercises with their sets (weightKg × reps), an optional session note, and total_volume_kg (Σ weight × reps). Logged manually in Vaultbeat; owner's own sessions only — strength has no partner fan-out. Join date against sleep/HRV/weight for training-load analysis. Pass limit_days to cap how many sessions return.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

log_strength_entryA

Log one strength-training session on the owner's behalf (agent write).

⚠️ THIS TOOL DELETES. The supplied exercises become the day's ENTIRE session — any exercise you don't re-send is silently deleted. If you meant to ADD to a day rather than replace it, STOP and call log_strength_append instead; it cannot delete anything.

(merge=True still does the same thing as log_strength_append and keeps working for callers that already use it. New callers should use the separate tool: which one you called is visible to the owner, a flag buried in the arguments is not.)

The result carries replaced_exercises — the names this call deleted. If that list is non-empty and you did not intend to replace the day, you just destroyed those exercises; re-send them with merge=True.

note=None LEAVES THE EXISTING NOTE ALONE (pass note="" to clear it).

date is the LOCAL calendar day the session happened, "YYYY-MM-DD". exercises is [{"name": "卧推", "sets": [{"weightKg": 30, "reps": 8}, ...]}, ...]. Encrypted end-to-end before it ever leaves this machine — this server never sends plaintext. Requires a bind made after this feature shipped (carries owner_user_id/owner_public_key_base64/owner_device_id from the pairing handshake); an older bind must re-pair by calling vaultbeat_start_binding then vaultbeat_poll_binding.

log_strength_appendA

Add exercises to a session WITHOUT touching what is already logged (agent write).

This tool cannot delete anything you did not send. Your exercises are appended to the day's existing session; an exercise whose name matches an existing one gets its sets appended to it. This is the right tool for "log the set I forgot" or for logging a session in installments while the owner is still in the gym — which is how strength data actually arrives.

Reach for log_strength_entry ONLY when you intend the supplied exercises to become the day's ENTIRE session and everything else to be deleted.

date is the LOCAL calendar day the session happened, "YYYY-MM-DD". exercises is [{"name": "卧推", "sets": [{"weightKg": 30, "reps": 8}, ...]}, ...].

This tool deliberately cannot set the session note: that field is replace-only, and a tool that promises to delete nothing must not carry an exception. Use log_strength_entry to change it.

The result is the same shape log_strength_entry returns. replaced_exercises is always [] here — that empty list is the receipt that this call deleted nothing. Encrypted end-to-end before it ever leaves this machine. Requires a bind made after the agent write path shipped; an older bind must re-pair via vaultbeat_start_binding then vaultbeat_poll_binding.

get_food_logA

Decrypt recent daily food-intake logs locally (newest first).

Each day carries meals, each meal a list of items with food (name), optional free-text portion ("1 根" / "300g" / "小份"), optional per-item/per-meal note, and — when the logging agent estimated them — optional structured nutrition numbers (kcal, proteinGrams, fatGrams, carbGrams). Items without those fields need analysis-time estimation from name + portion; items with them can be summed directly. Owner's own days only. Pass limit_days to cap how many days return.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

log_food_entryA

Log one day's food intake on the owner's behalf (agent write).

⚠️ THIS TOOL DELETES. The supplied meals become the day's ENTIRE log — any meal you don't re-send is silently deleted. If you meant to ADD to a day rather than replace it, STOP and call log_food_append instead; it cannot delete anything.

(merge=True still does the same thing as log_food_append and keeps working for callers that already use it. New callers should use the separate tool: which one you called is visible to the owner, a flag buried in the arguments is not.)

The result carries replaced_meals — the meals this call deleted. If that list is non-empty and you did not intend to replace the day, you just destroyed them; re-send them with merge=True.

note=None LEAVES THE EXISTING NOTE ALONE in both modes (pass note="" to clear it).

date is the LOCAL calendar day, "YYYY-MM-DD". meals is a list of {name?, timeOfDay?, items: [...], note?} where each item is {food, portion?, note?, kcal?, proteinGrams?, fatGrams?, carbGrams?}, e.g. [{"name": "lunch", "items": [{"food": "香蕉", "portion": "1 根", "kcal": 105}]}]. Everything but food is optional so a rushed "just log 香蕉" still works; when you DO estimate nutrition at logging time, put the numbers in the structured fields (snake_case aliases like protein_g are accepted) — they persist for later sessions instead of being re-guessed each read.

ESTIMATING FROM A PHOTO: look for something of known size in the frame first — a utensil, a hand, a coin, the rim of a standard plate — and calibrate the portion against it. With no such reference an image cannot settle portion size, and portion size is what the whole estimate rests on. In that case say so in your reply and give a range rather than a precise-looking number. These values are persisted and summed into daily totals later, so a confident "650 kcal" that is wrong does more damage than "roughly 500-700, nothing in frame to judge size by" — the first silently poisons a week of trends, the second invites a correction. [keep the photo-estimation paragraph above in sync with log_food_append's copy] Encrypted end-to-end before it ever leaves this machine.

log_food_appendA

Add meals to a day WITHOUT touching what is already logged (agent write).

This tool cannot delete anything you did not send. Your meals are appended to whatever the day already holds; a meal whose name matches an existing meal gets its items appended to that meal. This is the right tool for "log the snack I forgot" / "add dinner to today" — which is almost every follow-up write of the day.

Reach for log_food_entry ONLY when you intend the supplied meals to become the day's ENTIRE log and everything else to be deleted.

date is the LOCAL calendar day, "YYYY-MM-DD". meals is a list of {name?, timeOfDay?, items: [...], note?} where each item is {food, portion?, note?, kcal?, proteinGrams?, fatGrams?, carbGrams?}, e.g. [{"name": "lunch", "items": [{"food": "香蕉", "portion": "1 根", "kcal": 105}]}]. Everything but food is optional so a rushed "just log 香蕉" still works; when you DO estimate nutrition at logging time, put the numbers in the structured fields (snake_case aliases like protein_g are accepted) — they persist for later sessions instead of being re-guessed each read.

ESTIMATING FROM A PHOTO: look for something of known size in the frame first — a utensil, a hand, a coin, the rim of a standard plate — and calibrate the portion against it. With no such reference an image cannot settle portion size, and portion size is what the whole estimate rests on. In that case say so in your reply and give a range rather than a precise-looking number. These values are persisted and summed into daily totals later, so a confident "650 kcal" that is wrong does more damage than "roughly 500-700, nothing in frame to judge size by" — the first silently poisons a week of trends, the second invites a correction. [keep the photo-estimation paragraph above in sync with log_food_entry's copy]

This tool deliberately cannot set the day's note: that field is replace-only, and a tool that promises to delete nothing must not carry an exception. Use log_food_entry to change it.

The result is the same shape log_food_entry returns. replaced_meals is always [] here — that empty list is the receipt that this call deleted nothing. Encrypted end-to-end before it ever leaves this machine.

log_noteA

Log a free-text note on the owner's behalf (agent write).

For narratives that belong next to the metric data instead of in chat history: kind="mood" for emotional state ("为什么今天情绪低落"), kind="general" for day events worth joining against sleep/HRV later. (sleep/menstrual notes stay iOS-authored — this tool refuses them.)

⚠️ THIS TOOL DELETES. There is one note per (kind, day), and your text becomes its ENTIRE contents — anything already written for that kind+day is silently deleted. If you meant to ADD to a day rather than replace it, STOP and call log_note_append instead; it cannot delete anything.

(merge=True still does the same thing as log_note_append and keeps working for callers that already use it. New callers should use the separate tool: which one you called is visible to the owner, a flag buried in the arguments is not.)

The result carries replaced_text — the note this call deleted. If it is non-null and you did not intend to replace, you just destroyed that text; re-send it with merge=True.

For symptoms, use log_note_append. Discomfort shows up in installments across a day (nausea at noon, dizziness at night), so the second write of the day is the normal case, not the exception — and this tool would replace the morning's entry with the evening's.

date = LOCAL calendar day "YYYY-MM-DD" (default today). Read back via get_notes (optionally target_kind="mood"/"general"). Encrypted end-to-end before it ever leaves this machine.

log_note_appendA

Add a line to a day's note WITHOUT erasing what is already there (agent write).

There is one note per (kind, day). This tool appends your text to it on a new line and cannot delete what is already written.

USE THIS FOR SYMPTOMS. Discomfort arrives in installments across a day — nausea at noon, dizziness at night — so the second write of the day is the normal case, not the exception. log_note would replace the morning's entry with the evening's.

Reach for log_note ONLY when you intend your text to become the note's ENTIRE contents and whatever is there now to be deleted (e.g. correcting something you yourself wrote a minute ago).

kind="general" for day events worth joining against sleep/HRV later, kind="mood" for emotional state. (sleep/menstrual notes stay iOS-authored — this tool refuses them.) date = LOCAL calendar day "YYYY-MM-DD" (default today). Read back via get_notes.

The result is the same shape log_note returns. replaced_text is always null here — that null is the receipt that this call deleted nothing. Encrypted end-to-end before it ever leaves this machine.

log_weight_entryA

Log the owner's weight (kg) on their behalf (agent write, 2026-07-21).

weight_kg: kilograms (positive, ≤500). date: LOCAL calendar day "YYYY-MM-DD" (default = today). Same-day upsert-in-place semantics (dayID = "body-{dayStart.epoch}") — re-logging the same day overwrites. Encrypted end-to-end before it ever leaves this machine.

Written data always lands in Vaultbeat cloud + MCP (visible to get_weight_trend). Whether it also reaches Apple Health depends on an iOS setting: Settings → Data & AI → "Allow AI to update Apple Health" (OFF by default). When it is on, weigh-ins logged here sync back into Apple Health on the next app sync.

⚠️ If the owner wants this number in the Apple Health app, tell them to turn that toggle ON — do NOT tell them to re-enter it by hand in the Vaultbeat weight card. Logging it in both places produces two entries for the same day from different sources and corrupts the trend line. (Before 2026-07-28 this docstring said propagation was impossible and instructed exactly that manual double-entry; the toggle shipped 2026-07-22.)

get_menstrual_cycleA

Decrypt recent menstrual cycle data locally and predict the next period.

SENSITIVE: menstrual data only reaches this server if the user explicitly opted in on iOS; it stays on-device and is never re-exported. Returns recent samples plus a next-period prediction. Use owner prefix to filter by person.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

vaultbeat_start_bindingA

Initialize a binding session: generates a keypair (if needed) and returns a QR payload that the user scans in the Vaultbeat iOS app to authorize this AI server.

⚠️ If the user says they cannot see the QR code, believe them. Many terminals and most agent transcripts drop the block characters it is drawn with, so the payload can reach you intact while their screen shows a blank gap — do not assert that it is there. Have them run uvx vaultbeat-apple-health bind in a real terminal instead.

Returns qr_payload_json — a JSON string the AI should render as a QR code for the user to scan, plus poll_id to pass to vaultbeat_poll_binding. After the user scans, call vaultbeat_poll_binding to complete authorization. The iOS path is Settings → Data & AI → Connect an AI server.

Opening a session does NOT disturb an existing binding: the current credentials keep working until a scan actually lands, and are only replaced at that moment. (Before 2026-08-11 this call wiped them up front, so an agent "just re-checking" silently unbound its owner.)

vaultbeat_poll_bindingA

Check whether the user has scanned the QR code and authorized this server.

Call this after vaultbeat_start_binding. THREE possible status values:

· "pending" — the pairing is alive and simply has not been scanned yet. Keep polling with short delays. · "bound" — success. The server can now decrypt health data. · "expired" — TERMINAL. Stop polling; no amount of retrying recovers it. Run uvx vaultbeat-apple-health bind for a fresh QR code.

🔴 "pending" is positive evidence that nothing is wrong. The endpoint looks the pairing row up by pollID and answers "expired" when it is gone, so a long run of "pending" means the row is still there and nobody has scanned — NOT that it went stale while you waited.

That distinction decides what you tell the user, and getting it backwards is destructive: re-running bind mints a NEW pollID, which invalidates the QR they are looking at — so "just run bind again" turns a pairing that was one scan away from working into one that cannot complete. Only do it on "expired".

So while it stays "pending", the useful action is to get the code scanned, not to restart anything. Connecting is open on every plan, so there is no tier to check; the scanner is at Settings → Data & AI → "Connect an AI server". Do not send them to check the network, reinstall this server, or run diagnostics — none of those are implicated.

If they cannot see a QR code at all, it is your output that failed, not their phone — see vaultbeat_start_binding.

get_sleep_detailA

Per-night sleep stages with per-stage HR/RR — depth on a few nights.

Which sleep tool to use: · THIS one for going deep on one or two specific nights (stage bands, per-stage vitals). It defaults to 2 nights because each is ~1-2k characters; see the SIZE note below. · vaultbeat_sync_sleep for anything spanning time — "how did I sleep this week/month", trends, averages. Its default of 50 covers ~2-3 weeks. Reach for it whenever the question is about a period rather than a night, and do not conclude from THIS tool's two rows that only two nights exist.

(This used to call itself "the primary tool for detailed sleep analysis", which read as "use this one for sleep" and handed back two days to anyone who asked how their week went.)

Returns stage_intervals (contiguous stage bands with start/end), stage_minutes, and stage_vitals (per-stage HR/RR min/mean/max). Use owner prefix to filter by person — the first characters of a user id. vaultbeat_doctor lists them under capabilities.owner_prefixes; there is no other way to discover one, and omitting owner on a paired account blends both people into one result.

⚠️ SIZE: each night is ~1-2k characters as returned. Setting include_timeline=True adds the raw per-sample array (hr, rr, stage, time) — about 13k characters PER NIGHT, which will overflow a typical 25k-token client budget after 2-3 nights. Ask for it only when you need sample-level vitals (e.g. "when exactly did HR spike"); stage_vitals already answers per-stage questions. Raise limit for trends, but keep it low whenever include_timeline=True.

⚠️ is_in_bed_only: true means sleep was NEVER MEASURED that night (the Watch wasn't worn) — NOT that the person slept zero. duration_label reads "no sleep data" and in_bed_minutes holds the time actually recorded in bed. Report it as "no sleep data (in bed ~Xh)", never as "slept 0 hours".

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_activityA

Decrypt recent daily activity rings (steps, active energy kcal, exercise minutes, stand hours, distance km). One entry per day, newest first. Use owner prefix to filter by person. Each record carries owner_user_id.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_resting_hrA

Decrypt recent resting heart rate samples (bpm). Returns per-day records plus average over the window. Use owner prefix to filter by person.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_workoutsA

Decrypt recent workout sessions (type, duration, calories, distance). Use owner prefix to filter by person.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_hrvA

Decrypt recent HRV (SDNN in ms) — returns records plus average over the window.

granularity selects between two backing kinds:

  • "hourly" (default) — routes to hrv_hourly kind: one bucket per UTC hour (arithmetic mean of every raw SDNN sample in the hour). 30-day rolling window, ≤720 records, includes sample_count per bucket. Records also carry a sdnn_ms alias equal to the hourly mean, so callers migrating from the pre-build-77 raw default keep working without a field rename. Right for trend / aggregate queries — SAVES CONTEXT vs raw.

  • "raw" — routes to hrv kind: one record per SDNN sample (Apple Watch emits every 5-15min). 3-day rolling window; older raw history lives in prior-recipient envelopes plus the VaultbeatHistoryBackfillCoordinator-driven historical push (advances 30d/24h on device wake-ups, up to 5 years). Use for spike-precision questions (e.g. "HRV during the 3 minutes I opened a stressful message"). Note: single-day count is often 30-100+ records.

⚠️ average_sdnn_ms from the two granularities is NOT directly comparable — they observe different windows (3d vs 30d) and, on the raw side, also include legacy per-sample blobs from before build 77. Use hourly for "what has my HRV been lately?" trend answers; use raw only when you need per-sample precision inside the last ~3 days. The equivalence claim in previous doc versions was retracted 2026-07-22 after an adversarial review pointed out the window mismatch.

Use owner prefix to filter by person — take it from vaultbeat_status (owner_user_id_prefix is the paired user; server 0.7.1+), or vaultbeat_doctor for every owner present in the data.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_wrist_tempA

Decrypt recent sleeping wrist temperature samples — ABSOLUTE °C.

⚠️ These are absolute skin temperatures (~35.5-36.5 °C), NOT baseline deltas — the legacy temperature_delta_celsius field name is a wire- contract misnomer (kept for compatibility; prefer the honest twin wrist_temperature_celsius). For cycle analysis, derive the deviation yourself: reading minus that person's rolling baseline. One sample per night. Use owner prefix to filter.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_basal_energyA

Decrypt recent basal-energy-burned samples (Apple Watch BMR estimate, kcal). Watch typically emits hourly samples; unlimited limit + daily aggregation returns per-day BMR (~1500-2000 kcal for active young adults) + average. Use owner prefix to filter by person.

READ hours_covered BEFORE QUOTING ANY SINGLE DAY. Basal arrives as one blob per hour, so a day the Watch spent off the wrist comes back as a real-looking row that is short in exact proportion — 883 kcal at 12 of 24 hours is half a day of data, NOT a collapsed metabolism. Rows with incomplete: true are already excluded from average_daily_basal_kcal (average_over_days is its denominator); if you quote such a day, say how many hours it covers.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_total_energy_burnedA

TDEE (total daily energy expenditure) = basal + active per day, last N days.

The truthful daily calorie burn from Watch's actual measurements — not a formula. Diet targets need to aim BELOW this to lose weight (e.g. eating avg_tdee - 500 = ~0.5 kg/week loss). Returns per-day breakdown {day, basal_kcal, active_kcal, total_kcal, basal_missing, partial, basal_hours_covered, basal_hours_expected, basal_incomplete} + average TDEE. Three kinds of day are excluded from the average and each is listed with its reason in average_excluded_days: today (partial, still accumulating), days with no basal data (basal_missing), and days whose Watch coverage was short (basal_incomplete — e.g. 16 of 24 hours). A short day's kcal is low in proportion to the hours it missed, so including it drags the average down and, since the error is one-directional, never cancels out. Quote average_tdee_kcal for diet targets, and if you quote a single day, check basal_incomplete first. Use owner prefix to filter by person.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_vo2maxA

Decrypt recent VO2Max samples (Apple Watch cardiorespiratory fitness). Unit: mL/(kg·min); higher = better. Male 20-29 reference: <35 poor, 35-42 fair, 42-46 good, 46-50 excellent, 50+ superior. Returns newest-first records plus latest / peak / trough / average over the window. Use owner prefix to filter by person. VO2Max is sparse (Watch computes it during outdoor brisk walk/run bouts, days apart), so a limit of 30 usually covers many months.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

get_mindfulnessA

Decrypt recent daily mindfulness summaries (session count, total minutes). Use owner prefix to filter by person.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend, and read coverage.window_satisfied: false as a shorter history than asked, not as a missing kind. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means this server can decrypt days OLDER than first_day that your limit left behind — re-read with a larger limit, or quote coverage.oldest_available as the real start of their history. Never report a limit-shaped window as the extent of their data.

list_metric_seriesA

List every series the trend / compare / correlate tools accept, with units.

Call this BEFORE guessing a series name. Kinds with a richer shape (sleep stages, workouts, strength sets, food, notes, symptoms, cycle) are deliberately absent — flattening them to one number per day would answer a question you did not ask; read them with their own get_* tool.

get_metric_trendA

Least-squares trend for one daily series: slope per day, endpoints, spread.

series is a name from list_metric_series. days selects the newest N days THAT HAVE DATA, not the last N calendar days — compare n_days with span_days to see whether the history is dense or sparse.

Returns arithmetic only. slope_per_day is in the series' own unit per day and carries no threshold, band or verdict; fewer than 3 days returns a null slope with a reason rather than a number fitted to noise.

Carries a coverage block over the days the arithmetic used: quote coverage.days_covered and coverage.span_days beside any number here, and read coverage.window_satisfied: false as a shorter history than you asked for rather than as a missing kind.

compare_metric_periodsA

Compare the newest days days of a series against the days before them.

"Before" means the next-oldest days WITH DATA, so a gap makes the earlier window older rather than emptier — read previous.first_day / previous.last_day to see which period you actually got, and quote them.

Returns both windows' mean/median/min/max and the differences. It does not say which window is better; that depends on the metric and on the person.

Carries a coverage block over the days the arithmetic used: quote coverage.days_covered and coverage.span_days beside any number here, and read coverage.window_satisfied: false as a shorter history than you asked for rather than as a missing kind.

correlate_metric_seriesA

Pearson r between two daily series, over days that have BOTH recorded.

Days missing on either side are dropped, never interpolated and never read as zero, so n_pairs is usually smaller than either series — quote it with r. Fewer than 3 shared days returns null with a reason: any two points are perfectly collinear, so a coefficient there is an artefact.

The result carries a caveat field about causation. Repeat its substance in your answer, and do not translate r into a word like "strong".

Carries a coverage block over the days the arithmetic used: quote coverage.days_covered and coverage.span_days beside any number here, and read coverage.window_satisfied: false as a shorter history than you asked for rather than as a missing kind.

Prompts

Interactive templates invoked by user choice

NameDescription
daily_briefA short readout of the most recent day, set against the fortnight behind it.
sleep_reviewA descriptive read of recent nights: duration, stages, and the nights that were not measured.
energy_balanceCalories in against calories out, with the incomplete days excluded rather than averaged in.
training_block_reviewRecent lifting and cardio volume, next to the recovery signals from the same weeks.
cycle_aware_readRead a metric against the same phase of earlier cycles instead of against last week.
partner_check_inRead both people's shared data side by side, without turning either into a judgement.
log_from_conversationTurn something I said in passing into a Vaultbeat entry, without inventing the parts I left out.
why_is_this_emptyWork out which cause is behind an empty or stale result, instead of guessing.

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4.1/5.0

Scored across 33 tools

Disambiguation4/5

The read tools are cleanly separated by metric, and the write tools are deliberately split into replace/append pairs with emphatic warnings, so an agent can usually select correctly. The main residual ambiguities are the entry/append pairs and vaultbeat_sync_sleep vs get_sleep_detail, whose names don't fully telegraph their different purposes.

Naming Consistency4/5

Reads follow get_<metric>, writes follow log_<domain>_<mode>, analytics share the metric_* pattern, and server operations use the vaultbeat_* prefix. Minor deviations — vaultbeat_sync_sleep is a read under a different prefix, log_note lacks the _entry suffix, and vaultbeat_status/doctor are noun-like — are noticeable but don't undermine predictability.

Tool Count2/5

At 33 tools this is a heavy surface for an agent to navigate, well past the 25+ threshold. Many get_* tools repeat identical owner-filtering and coverage semantics and could be consolidated, even though each metric has some unique fields.

Completeness4/5

The server covers an unusually broad set of health metrics, provides read, append, replace, and deliberate delete paths for the writable domains, and includes diagnostics plus a binding lifecycle. Minor gaps remain — no water-logging write, no dedicated symptom-logging tool (worked around via log_note_append) — but agents can generally proceed without dead ends.

Maintenance

ActivityActive
ResponsivenessNo issues