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_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. A kind under kinds_not_checked could not be counted this run; it is neither empty nor full.

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.

binding — the local pairing state (which account, which server, whether demo mode is serving synthetic data). No keys or tokens.

This runs a cloud round trip, so it takes a moment.

get_symptomsA

Decrypt recent symptoms locally, grouped by data owner — both sources.

SENSITIVE. Each entry in owners carries owner_user_id and two lists: · days — symptoms imported from Apple Health (cramps, headache, fatigue, coughing…), one row per day with each sample's severity (mild/moderate/severe/present/…). These reach this server only when the user opted in on iOS — their own AI toggle, or the partner-AI toggle for a partner's. symptom_counts counts logged DAYS per type. · reported — episodes a person reported themselves, through log_symptom or the Vaultbeat app: entry_id, symptom_type, display_name (their own words), severity, local_date, onset_at / end_at (end_recorded: false means no end was logged — ask, do not assume it is still going), duration_minutes, body_location, triggers, note. reported_counts counts EPISODES per type. A type that exists in Apple Health is spelled the same in both lists (healthkit_type: true), so "headache" from either source is one symptom. To correct or remove a reported entry use update_symptom / delete_symptom with its entry_id; imported days are edited in Apple Health. Stays on-device, never re-exported.

Returns YOUR symptoms by default; partner=true returns your partner's Apple Health days only (if they share symptoms with you from their own app). A partner's reported episodes never reach you: the app and log_symptom seal them for their owner's own AI alone, so an empty reported under partner=true means "not shareable", not "none logged".

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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.

Returns notes about YOU by default. partner=true returns notes about your partner: the ones they wrote themselves and shared, plus the ones your AI recorded about them (about: "partner", written with log_note_append(partner=true)).

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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. duplicate_sessions, when present, lists stored entries that only repeat another session of the same day; they are already left out of sessions and session_count, so do not add their volume back.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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 running uvx vaultbeat-apple-health@latest bind in a terminal.

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 by running uvx vaultbeat-apple-health@latest bind in a terminal.

get_food_logA

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

limit counts LOGGED days: by default the newest 14 days that have a log — not a calendar fortnight. Days with nothing logged are skipped, so the 14 can reach further back; coverage.span_days and coverage.days_missing_in_span say how far. since / until ("YYYY-MM-DD", local days, both inclusive) read a calendar window instead — "what did I eat in the first week of August" — and then limit applies only if you pass it. Each day's date is its local calendar day (the same as local_date, and the form log_food_entry and get_strength_log use). A logged day is roughly 0.5-1k tokens (more with notes, and Chinese text counts more), so a month can already be past what a client takes in one result — it then comes back as result_too_large; ask for fewer days or a narrower window.

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. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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.

Symptoms do NOT go here: use log_symptom, one call per symptom. A note is free text nothing can be matched against; a symptom entry keeps its type, severity and timing so it can later be lined up with sleep, food and heart rate. A note is for what surrounds it (mood, events, how a day went).

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.

partner=true records a note ABOUT your partner ("she had a stomach ache this morning"). It is stored in YOUR account and sealed to you and this machine only — your partner's account is never touched, and it never mixes into your own notes: plain reads leave it out, get_notes(partner=true) returns it. Default false = a note about you.

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.

The second note of a day is the normal case, not the exception — a morning event and an evening one — and log_note would replace the first with the second. Symptoms do NOT go here: use log_symptom, which keeps type, severity and timing as fields a later analysis can match.

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.

partner=true records a note ABOUT your partner ("she had a stomach ache this morning"). It is stored in YOUR account and sealed to you and this machine only — your partner's account is never touched, and it never mixes into your own notes: plain reads leave it out, get_notes(partner=true) returns it. Default false = a note about you.

log_symptomA

Record one symptom the person reports — one call per symptom (agent write).

This is where "我肚子疼" / "headache since lunch" / "拉肚子了" goes, not a note: each episode is kept as fields (type, severity, timing, place, suspected causes) so a later read can line it up with sleep, food, heart rate and wrist temperature. Two symptoms at once are two calls. This tool only ever adds; it cannot change or delete anything already recorded.

symptom_type: a short English token. Use the Apple Health name when one fits — abdominal_cramps, bloating, constipation, diarrhea, heartburn, nausea, vomiting, headache, dizziness, fatigue, fever, chills, coughing, sore_throat, runny_nose, lower_back_pain, chest_tightness_or_pain, shortness_of_breath, acne, night_sweats… — so it merges with what Apple Health imports; otherwise a plain descriptive one (rectal_bleeding, stomach_spasm, eye_strain). Any spelling is folded to one form. display_name: the person's own words, e.g. "便血" / "胃痉挛".

severity: mild | moderate | severe | unspecified. Use unspecified unless they said how bad it is — never grade it for them.

onset_at: when it began, ISO 8601 with their UTC offset, e.g. "2026-09-30T10:49+08:00". Omit it only for something happening NOW (it defaults to the current time). If they named a day but no time ("昨天头疼"), pass that day as a bare "2026-09-29" — do not invent an hour. end_at: when it eased, if they said; add it later with update_symptom. date: the local day to file it under, only when it differs from the onset's own day. body_location, note: free text, only what they said. triggers: what THEY suspect caused it, as short tokens (["bbq_dinner", "spicy_food"], ["sleep_deprivation"]). Record their suspicion; do not add causes of your own.

Returns entry_id (keep it for update_symptom / delete_symptom) and the entry as read back. Read everything with get_symptoms. Sealed for the owner and this machine only, end to end, before it leaves here.

update_symptomA

Change fields of one reported symptom (agent write) — e.g. it eased.

The common call is update_symptom(entry_id, end_at="…") when the person says it has stopped, or a new severity when it got worse. Only the fields you pass change; omitted ones keep their value. Passing a field OVERWRITES it: note replaces the old note rather than adding to it, and triggers replaces the whole list — read the entry first and send the combined list if you mean to add one. "" clears a text field or a mistaken end_at; [] clears triggers.

entry_id comes from log_symptom or from get_symptoms' reported list. Symptoms imported from Apple Health cannot be edited here.

Returns previous — the old value of every field this call changed — and the entry as read back.

delete_symptomA

Erase one reported symptom that was logged by mistake (agent write).

Only for an entry that should never have existed — a duplicate, the wrong person, a symptom they did not have. A symptom that simply ended is NOT deleted: record its end with update_symptom(entry_id, end_at=…).

The entry's content is overwritten in the cloud, so it disappears from get_symptoms and from the app. The result carries deleted_entry — the full entry as it was — so a mistaken delete can be logged again with log_symptom. Symptoms imported from Apple Health cannot be deleted here.

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_metric series "weight_kg"). Whether it also reaches Apple Health depends on an iOS setting, OFF by default: the MCP tab → "Allow AI to update Apple Health" (app 1.2.8 and earlier: Settings → Data & AI). 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. Reads YOUR cycle by default; if the cycle being asked about is your partner's, pass partner=true (it reaches this server only if they share it).

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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_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 ~3k characters; see the SIZE note below. · get_sleep_nights for anything spanning time — "how did I sleep this week/month/year", patterns, worst nights. One compact row per night, a whole year in one read. 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. · get_metric with the sleep series (bedtime_minutes, deep_sleep_minutes, awakenings, ...) for averages computed for you.

(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). Reads YOUR nights by default; partner=true reads your partner's shared sleep.

limit = the newest N nights, 2 by default. To read a particular night, pass its date as since and until ("YYYY-MM-DD", the local day the night ENDS on — the local_date / date column of get_sleep_nights); a wider since / until reads every night in that window, and limit then applies only if you pass it.

⚠️ SIZE: each night is ~3k characters as returned (a week ~21k). 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. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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_sleep_nightsA

Every night as one compact row — the tool for reading a sleep HISTORY.

One row per night, newest first: date, weekday, bed and wake time, time asleep, deep / REM / core / awake minutes, deep and REM share, number of awakenings, longest unbroken stretch of sleep, heart and breathing rate while asleep, and any OTHER sleep that day (naps, the second half of a broken night) — so a day's total sleep is asleep plus other. Column names come once in columns; each row is the values in that order, so a whole year is ~40k characters. Read it when the question is about a period or a pattern: weekends vs weekdays, drifting bedtimes, which nights were worst, what changed after a date.

Which sleep tool: · THIS one for any span of time. · get_metric with the sleep series (sleep_minutes, bedtime_minutes, deep_sleep_minutes, awakenings, ...) when you want the arithmetic — weekly/monthly averages computed for you, from these same rows. · get_sleep_detail for one or two nights in depth (stage bands, per-stage vitals).

limit = newest N nights (default 120; pass 400 for everything, and check coverage.more_available). since = "YYYY-MM-DD" keeps every night on or after that day — use it for "last month" rather than guessing a limit; with since, limit applies only if you pass it.

Read legend once. Five flags, comma-separated when a night has several: unworn means sleep was never measured that night (not zero sleep), motion_inferred means phone motion guessed it (leave it out of averages, as get_metric does), daytime means the day's only sleep was a daytime nap (real, but not a night — out of get_metric's nightly series, still a row here), short means a main sleep under 3 h with no stages — a nap or a partly recorded night, left out of get_metric's sleep-duration and clock series — and no_stages means the total is known but the stage columns are null. A null anywhere is "not measured", never zero. Reads YOUR nights; partner=true reads your partner's shared sleep.

Carries a coverage block: quote coverage.days_covered beside any average, and read coverage.more_available before saying how far back the history goes.

get_workoutsA

Decrypt recent workout sessions (type, duration, calories, distance). Reads YOUR workouts; workouts are never shared between partners, so this tool has no partner option.

Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) and coverage.span_days beside any average or trend. coverage.window_satisfied: false alone does NOT mean a short history: it is also false when older days exist beyond your window. 🔴 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_user_profileA

Read YOUR health profile: biological sex, age, date of birth, height.

Use it before anything that depends on sex, age or height — basal metabolic rate, heart-rate zones, VO2 max bands, BMI. It is never shared between partners, so this tool has no partner option.

profile.sex_source says where the sex came from: "chosen" (the person picked it in the app) or "apple_health" (a field in Apple Health they may never have looked at). Every field may be null; profile itself is null when nothing was uploaded, and the reply's note explains why that cannot be narrowed down. Never fill a null in with a guess.

A profile is not a series: its coverage.days_covered is 0 by construction, and the reply carries no upload time — the stored row is replaced in place on every edit, so nothing here says how current the values are.

get_metricA

Read any one-number-per-day health series: steps, resting HR, HRV, weight, …

series takes ONE name, a LIST of names, or nothing (= every series). Names come from list_metric_series, which also says how much data backs each — call it first rather than guessing. Activity is five series (steps, active_energy, exercise_minutes, stand_minutes, distance_meters); pass them as a list to get the whole day in one call.

days = the newest N days THAT HAVE DATA, per series (not N calendar days). For a CALENDAR window — "last month", "this semester", "July" — pass since / until ("YYYY-MM-DD", either or both) instead; days is then ignored and the reply names the range it used.

aggregation is computed here, not by you: avg / sum / min / max / latest over the window, or none (default) for the per-day points only. sum is refused on state measurements (resting HR, weight, VO2max) — it has no meaning there. granularity = day (default), week (ISO weeks) or month; week/month return buckets aggregated with aggregation (avg when none), each with days_with_data beside days_in_period. period_in_progress: true marks the current, unfinished week/month and clipped_by_window: true the oldest one your days cut through — never compare either against a full period as if it were one. granularity = weekday groups the window by day of the week (Mon..Sun) — the direct answer to "weekends vs weekdays" and to social jet lag. Read its weekday_note: sleep is dated by the morning it ends, so Sat is the Friday-night sleep.

Honesty the reply already does for you, so do not undo it: · On cumulative series (steps, energy, water) today is still accruing — it is marked partial: true and excluded from every aggregate. · excluded_days names days dropped because their data was short (basal energy with the Watch off the wrist, TDEE without basal), with reasons. · Missing days are absent, never zero.

Each series' points (or buckets) is a table: columns names the fields once and every entry of rows is one day (or period) in that order — e.g. columns: ["date", "value", "partial"]. The flags above are columns; a day without one has null there.

Each series carries its own coverage block: quote coverage.days_covered and coverage.span_days beside any number. 🔴 Before saying how far back someone's data goes, read coverage.more_available: true means older days exist that your days left behind — ask for more, or quote coverage.oldest_available. Never report a window as the extent of their data.

Reads YOUR data by default. Pass partner=true for your partner's — of these series only sleep, water and weight (with body composition) can be shared, so every other series comes back empty for a partner by design.

Sleep is here too — duration, timing (bedtime_minutes, wake_minutes, sleep_midpoint_minutes), stages, awakenings, heart/breathing rate while asleep; the nights behind those numbers are get_sleep_nights. Richer kinds have their own tools: workouts, strength, food, notes, symptoms, cycle, and the sex / age / height profile (get_user_profile). Samples within a day (HRV spikes) → get_intraday. Trend / period comparison / correlation → get_metric_trend, compare_metric_periods, correlate_metric_series.

get_intradayA

Samples WITHIN a day, for the series that record several per day (HRV).

granularity:

  • "hourly" (default) — one row per hour that HAS samples (UTC hour boundaries, shown in local time), the mean SDNN of that hour and its sample_count. Right for "how did my HRV move through the day".

  • "raw" — one row per sample, newest first.

The Watch measures HRV a handful of times a day, not continuously (more during a Breathe session), so most days have about 8-14 rows either way. limit counts ROWS, not days: the default 168 is roughly two to three weeks, not one. How far back the history goes depends on what this phone uploaded, not on a fixed window — read coverage.oldest_available and coverage.more_available. For one number per day use get_metric. Reads YOUR data only: HRV is never shared between partners.

rows is a table in columns order (local_time, sdnn_ms, plus sample_count for hourly). Carries a coverage block: quote coverage.days_covered (distinct days, not the row count) beside any number.

list_metric_seriesA

Every series, with how much data actually backs each one.

Call this BEFORE guessing a series name, and before concluding a kind is empty. Each row carries rows / first_date / last_date / latest, so "does this person track VO2max at all" is answered here in one call instead of by reading the kind and getting nothing back.

rows = the days that HAVE a value for that series — counted the way get_metric reads it, so rows: 2 means get_metric can return at most 2 points. A kind's series can differ: a partner with 14 weigh-ins may have a BMI on only 2 of those days.

cumulative: true marks the series whose daily value ACCRUES over the day (steps, energy, water). On those the newest value is routinely a partial day and must not be compared against completed ones — that is what makes "steps are down today" wrong at 9am. Measurements of a state (resting HR, VO2max) carry false and need no such care.

Sleep appears here as numbers (duration, timing, stages, awakenings, vitals); read the nights themselves with get_sleep_nights. Kinds with a richer shape (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 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. since / until ("YYYY-MM-DD") fit a calendar window instead.

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. coverage.window_satisfied: false alone does NOT mean a short history — it is also false when older days exist beyond the window; coverage.more_available tells the two apart.

compare_metric_periodsA

Compare two periods of one series: the newest days vs the days before, or two named windows.

To compare CALENDAR periods — "this semester vs the summer", "before and after I started training" — pass period and baseline, each "YYYY-MM-DD..YYYY-MM-DD" (an open end is allowed: "2026-09-01.."). days is then ignored. Without them, the rule below applies.

"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. coverage.window_satisfied: false alone does NOT mean a short history — it is also false when older days exist beyond the window; coverage.more_available tells the two apart.

correlate_metric_seriesA

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

lag_days pairs series_a on each day with series_b that many days LATER (negative = earlier): "does a short night show up in HRV the day after" is sleep vs HRV with lag_days=1. Sleep is already dated by the morning it ends, so lag 0 is "last night vs today". since / until ("YYYY-MM-DD") restrict to a calendar window.

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. coverage.window_satisfied: false alone does NOT mean a short history — it is also false when older days exist beyond the window; coverage.more_available tells the two apart.

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.3/5.0

Scored across 26 tools

Disambiguation5/5

The descriptions go out of their way to disambiguate overlapping tools: get_sleep_detail vs get_sleep_nights vs get_metric sleep series, log_X vs log_X_append (replace vs append), and get_symptoms vs get_notes vs log_symptom are all explicitly contrasted with 'which tool to use' sections. Every tool targets a clearly distinct resource+action, making misselection unlikely.

Naming Consistency4/5

Strong, predictable verb_noun snake_case throughout (get_*, log_*, update_*, delete_*, list_*, compare_*, correlate_*), with append/replace pairs named in a consistent pattern. The only deviation is 'vaultbeat_doctor', which breaks the verb_noun convention but is a single, clearly special-purpose tool.

Tool Count4/5

26 tools is at the upper end, but the domain is genuinely broad (sleep, metrics, symptoms, notes, food, strength, workouts, cycle, profile, plus analysis and diagnostics). Each tool maps to a distinct data kind or operation rather than duplicating surface, so the count is largely earned despite being slightly heavy.

Completeness5/5

The surface covers the full lifecycle: reads for every data kind, writes for symptoms (log/update/delete), notes, food, strength, and weight, plus analysis tools (trend, compare, correlate), discovery (list_metric_series), and diagnostics (doctor). Replace-vs-append pairs, partner reads, and coverage/limit handling leave no obvious dead ends.

Maintenance

ActivityActive
ResponsivenessNo issues