Skip to content
verify mcp Beta VerifyMCP is currently in beta. If you notice any issues, get in touch and we’ll put it right.

io.github.Fino-wind/vaultbeat-apple-health

PYPI · VAULTBEAT-APPLE-HEALTH

Your AI agent reads your Apple Health data: sleep, HRV, cycle, workouts. Decrypted on your machine

+18 this week 79 Trust /100

Last scanned (21 hours ago)

Scores higher than 78% of PyPI servers

Category Health and Science

Trust breakdown (7 categories)

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. How we score → Why this is hard to score →

Supply Chain Security99
  • No malware found by supply-chain analysis.Pass
  • No known CVEs affecting this package version or its production dependencies.Pass
  • Runs setuptools.build_meta at install time, a recognised build step with no custom scripting around it. View diagnostics → Pass
  • 5 of 47 dependencies flagged as unhealthy. View diagnostics → Partial
Provenance & Transparency32
  • Source repository is publicly reachable at the declared URL. View diagnostics → Pass
  • Provenance check failed: no build-provenance attestation is published. See how to fix → View diagnostics → Fail
  • Licence could not be checked: the licence is shipped as a file inside the package, not declared as a standard (SPDX) licence identifier, so we cannot tell whether it is OSI-approved.Unverified
  • Actively maintained (last published 21 days ago).Pass
  • Disclosure check failed: no security disclosure policy was found in the source repository. See how to fix → Fail
Schema Quality & AI Usability73
  • 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).Pass
  • AI-judged instruction clarity (excellent).Pass
  • Context-footprint check failed: tool/resource definitions use about 13607 tokens (~469/item across 29 items; 29 tools + 0 resources), over budget; trim descriptions and params. See how to fix → Fail
  • Usage-examples check failed: none of the tools include examples. See how to fix → Fail
Stability & Change Management100
  • No destabilizing schema changes in the last 30 days.Pass
Tool Coverage71
  • 100% of tools have a non-trivial description (not blank, and not just the tool's name).Pass
  • 0% of tool parameters carry a description.Fail
  • Structured output schemas are declared (100% of tools); any adoption earns full credit.Pass
Tool Safety100
  • No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.Pass
  • All 2 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.Pass
  • An AI judge read all 30 captured unit(s) of tool text and found none that tries to manipulate the model reading it.Pass
Capabilities100
  • Implements a current MCP spec version (2026-07-28).Pass
Changelog

Every change we have recorded for this component, newest first. Security-relevant changes are always shown. ▲ marks a change for the better, ▼ a change for the worse; unmarked changes are neutral.

  • 3 Nov 26 +15
    • Malware scan: unverified → pass ▲ security
    • Stability: 0.97 → pass security
  • 2 Nov 26 −14
    • Malware scan: pass → unverified ▼ security
  • 1 Nov 26 +15
    • Malware scan: unverified → pass ▲ security
  • 31 Oct 26 −14
    • Malware scan: pass → unverified ▼ security
  • 29 Oct 26 +16
    • Malware scan: unverified → pass ▲ security
  • 27 Oct 26 +1

    No change was recorded against any check on this day. Stability & Change Management went from 73 to 77. That category is still filling its 30-day observation window: 22 days of observed history at the previous scan, 23 at this one. The score rises as the window fills, whether or not the server changes.

  • 25 Oct 26 +1

    No change was recorded against any check on this day. Stability & Change Management went from 67 to 70. That category is still filling its 30-day observation window: 20 days of observed history at the previous scan, 21 at this one. The score rises as the window fills, whether or not the server changes.

  • 23 Oct 26 +1

    No change was recorded against any check on this day. Stability & Change Management went from 60 to 63. That category is still filling its 30-day observation window: 18 days of observed history at the previous scan, 19 at this one. The score rises as the window fills, whether or not the server changes.

Diagnostics

Diagnostic detail from the automated scan of this channel: what the scanner observed at each step, so you can see exactly where a check passed or failed. It is informational only and never changes the trust score.

Captured 4 Nov 2026 · Analysed pypi/vaultbeat-apple-health@0.9.3

Provenance No attestation

The registry publishes no build provenance for this version, so there is nothing to verify.

Result No attestation
Ecosystem pypi
Reason No attestation published

Background: How many MCP packages publish verified provenance →

Install scripts 1 script
Hook Tier Command
build_backend allowlisted setuptools.build_meta

Background: Why install scripts are a supply-chain risk →

Dependencies 47 packages
Packages resolved 47
Stale 4
No linked repository 1
Tree resolution Complete

Background: SBOMs and build attestations, explained →

MCP tools · 29 exposed · ~12,682 tokens

The tools this component advertises to a client, with an estimated token cost for each. Expand a tool to see its parameters and schema. The per-tool counts are indicative and are not scored directly; the schema's total context footprint is one signal in Schema Quality & AI Usability. A tool's description is untrusted text the model reads on every call, which is what makes this list a security surface and not just an inventory: how tool poisoning works →

Tool Tokens
compare_metric_periods ~301

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.

NameTypeReqDescription
baseline–––
daysinteger––
freshboolean––
partnerboolean––
period–––
seriesstringyes–

Structured output declared, but exposes no named fields.

No examples provided.

correlate_metric_series ~348

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.

NameTypeReqDescription
daysinteger––
freshboolean––
lag_daysinteger––
partnerboolean––
series_astringyes–
series_bstringyes–
since–––
until–––

Structured output declared, but exposes no named fields.

No examples provided.

delete_state_of_mind ~190

Erase one state of mind an AI recorded by mistake (agent write). Only for an entry that should never have existed — a duplicate, the wrong reading of what they said, the wrong person. To record that a feeling changed, log a new one; feelings change and both are true. It disappears from `get_state_of_mind` at once. The person's iPhone, on its next sync, drops it from the app, overwrites its content in the cloud for every reader it was shared with, and removes the sample it wrote into Apple Health, if it wrote one. The result carries `deleted_entry` — the full entry as it was — so a mistaken delete can be logged again. Records with `source: "apple_health"` cannot be deleted here; they are edited in Apple Health.

NameTypeReqDescription
entry_idstringyes–

Structured output declared, but exposes no named fields.

No examples provided.

delete_symptom ~211

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=…)`. It disappears from `get_symptoms` at once, and from the app and every other reader once the person's iPhone next syncs (it then overwrites the entry's own copy in the cloud, and from app 1.3.0 removes the copies earlier `update_symptom` calls left; an older app leaves those sealed there, readable only by the person and the server that wrote them). 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.

NameTypeReqDescription
entry_idstringyes–

Structured output declared, but exposes no named fields.

No examples provided.

get_food_log ~535

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.

NameTypeReqDescription
freshboolean––
limit–––
limit_days–––
since–––
until–––

Structured output declared, but exposes no named fields.

No examples provided.

get_intraday ~305

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.

NameTypeReqDescription
freshboolean––
granularitystring––
limitinteger––
seriesstring––

Structured output declared, but exposes no named fields.

No examples provided.

get_menstrual_cycle ~273

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.

NameTypeReqDescription
freshboolean––
limitinteger––
partnerboolean––

Structured output declared, but exposes no named fields.

No examples provided.

get_metric ~906

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…

NameTypeReqDescription
aggregationstring––
daysinteger––
freshboolean––
granularitystring––
partnerboolean––
series–––
since–––
until–––

Structured output declared, but exposes no named fields.

No examples provided.

get_metric_trend ~261

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.

NameTypeReqDescription
daysinteger––
freshboolean––
partnerboolean––
seriesstringyes–
since–––
until–––

Structured output declared, but exposes no named fields.

No examples provided.

get_notes ~435

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), and "day" is what the person wrote about the day itself in the app's notes list (app 1.3.0+); "general" is agent-authored via `log_note`; "mood" notes are ones an AI wrote before moods moved to `get_state_of_mind` — read them here, write no new ones. 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.

NameTypeReqDescription
freshboolean––
limitinteger––
partnerboolean––
target_kind–––

Structured output declared, but exposes no named fields.

No examples provided.

get_sleep_detail ~774

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…

NameTypeReqDescription
freshboolean––
include_timelineboolean––
limit–––
partnerboolean––
since–––
until–––

Structured output declared, but exposes no named fields.

No examples provided.

get_sleep_nights ~583

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` read…

NameTypeReqDescription
freshboolean––
limit–––
partnerboolean––
since–––

Structured output declared, but exposes no named fields.

No examples provided.

get_state_of_mind ~707

Decrypt recent states of mind (how the person felt) locally — both sources. SENSITIVE. Apple Health's "State of Mind": each record is one feeling with `valence` (-1 very unpleasant … 0 neutral … +1 very pleasant), `valence_classification` (Apple's seven regions, veryUnpleasant … veryPleasant — use these words rather than inventing a grade), `labels` (Apple's feeling words: drained, stressed, calm, grateful…), `associations` (the life area: work, family, partner, health…), `kind` (`momentaryEmotion` = how they felt at that moment, `dailyMood` = how the day went overall), `local_date` / `local_time`. `source` says where it came from: · "apple_health" — logged by the person on their Watch, in the Health app or in another app, and imported by their iPhone (`app` names it); · "ai" — recorded by an AI through `log_state_of_mind` (`entry_id`, `note`). Remove a mistaken one with `delete_state_of_mind`. Each owner also carries `daily` (per day: `count`, `mean_valence`, `lowest_valence`, `highest_valence`, `daily_mood_valence`) and counts per region, label and life area. Describe what was logged; a day with one record says far less than a day with ten — quote `count` beside any average, and never read a day with no record as a neutral day. By default the newest 100 records (about three months of daily moods). `since` / `until` ("YYYY-MM-DD", local days, both inclusive) read a calendar window instead — "how was I in March", or anything older than the default page — and then `limit` applies only if you pass it. A record with its tallies is roughly 450 bytes, so about 130 fit in one result; a longer window comes back as `result_too_large` — read it a month or a quarter at a time. Returns YOUR records by default; `partner=true` returns your partner's — only if they turned on sharing their moods with your AI in their own Vaultbeat app (off by default). Stays on-device, never re-exported. Carries a `coverage` block: quote `coverage.days_covered` (distinct days, not the row…

NameTypeReqDescription
freshboolean––
limit–––
partnerboolean––
since–––
until–––

Structured output declared, but exposes no named fields.

No examples provided.

get_strength_log ~327

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.

NameTypeReqDescription
freshboolean––
limitinteger––
limit_days–––

Structured output declared, but exposes no named fields.

No examples provided.

get_symptoms ~615

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` t…

NameTypeReqDescription
freshboolean––
limitinteger––
partnerboolean––

Structured output declared, but exposes no named fields.

No examples provided.

get_user_profile ~218

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.

NameTypeReqDescription
freshboolean––

Structured output declared, but exposes no named fields.

No examples provided.

get_workouts ~209

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.

NameTypeReqDescription
freshboolean––
limitinteger––

Structured output declared, but exposes no named fields.

No examples provided.

list_metric_series ~348

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.

NameTypeReqDescription
freshboolean––
partnerboolean––

Structured output declared, but exposes no named fields.

No examples provided.

log_food_append ~570

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 c…

NameTypeReqDescription
datestringyes–
mealsarrayyes–

Structured output declared, but exposes no named fields.

No examples provided.

log_food_entry ~585

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…

NameTypeReqDescription
datestringyes–
mealsarrayyes–
mergeboolean––
note–––

Structured output declared, but exposes no named fields.

No examples provided.

log_note ~586

Log a free-text note on the owner's behalf (agent write). For day events that belong next to the metric data instead of in chat history, worth joining against sleep/HRV later (`kind="general"`, the default and the only kind this tool writes). A feeling does NOT go here, nor the story behind it: call `log_state_of_mind`, which keeps it as Apple Health's fields and has its own `note` for the why. This tool refuses `kind="mood"` — one feeling recorded twice, once structured and once as free text, is what that kind produced. (sleep, menstrual and day notes are written in the iOS app — refused too.) ⚠️ 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 (events, how a day went). `date` = LOCAL calendar day "YYYY-MM-DD" (default today). Read back via `get_notes` (optionally `target_kind="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 for you and your own AI machines only (the Vaultbeat app passes it on to your other pa…

NameTypeReqDescription
date–––
kindstring––
mergeboolean––
partnerboolean––
textstringyes–

Structured output declared, but exposes no named fields.

No examples provided.

log_note_append ~449

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"` (the default and the only kind it writes) for day events worth joining against sleep/HRV later. A feeling and the story behind it go to `log_state_of_mind` and its `note`; this tool refuses `kind="mood"`, and sleep, menstrual and day notes are written in the iOS app. `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 for you and your own AI machines only (the Vaultbeat app passes it on to your other paired machines) — never for your partner or their AI. 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.

NameTypeReqDescription
date–––
kindstring––
partnerboolean––
textstringyes–

Structured output declared, but exposes no named fields.

No examples provided.

log_state_of_mind ~762

Record how the person says they feel, as Apple Health's State of Mind (agent write). This is where "今天工作好累,被掏空了" / "刚散步回来,心情特别平静" / "I'm so anxious about tomorrow" goes when they tell you how they feel — people rarely open an app to log a feeling, but they say it to you. One call per feeling. This tool only ever adds; `delete_state_of_mind` removes a mistaken one. `valence`: -1 (very unpleasant) … 0 (neutral) … +1 (very pleasant), YOUR reading of how pleasant it felt FROM THEIR WORDS. Apple splits the range into seven equal regions: about -0.85 very unpleasant, -0.55 unpleasant, -0.3 slightly unpleasant, 0 neutral, +0.3 slightly pleasant, +0.55 pleasant, +0.85 very pleasant. Stay inside what they said; do not dramatise "有点烦" into very unpleasant. `labels`: Apple's feeling words that THEIR words support, usually one to three — amazed, amused, angry, annoyed, anxious, ashamed, brave, calm, confident, content, disappointed, discouraged, disgusted, drained, embarrassed, excited, frustrated, grateful, guilty, happy, hopeful, hopeless, indifferent, irritated, jealous, joyful, lonely, overwhelmed, passionate, peaceful, proud, relieved, sad, satisfied, scared, stressed, surprised, worried. Leave it empty rather than guess when they only said "心情还行". `associations`: what it is about, only if they said — community, current_events, dating, education, family, fitness, friends, health, hobbies, identity, money, partner, self_care, spirituality, tasks, travel, weather, work. `kind`: "momentaryEmotion" (default — how they feel right now or at one moment) or "dailyMood" (how the whole day has been). `logged_at`: the moment, ISO 8601 with their UTC offset, e.g. "2026-10-04T14:20+08:00"; omit it for right now. A `dailyMood` about a past day may give just that day as `date` ("2026-10-03") — do not invent an hour. `note`: a short summary in their words, only if useful later. It stays in Vaultbeat; Apple Health has no place for text. The person's iPhone writes it into Apple Health…

NameTypeReqDescription
associations–––
date–––
kindstring––
labels–––
logged_at–––
note–––
valencenumberyes–

Structured output declared, but exposes no named fields.

No examples provided.

log_strength_append ~331

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.

NameTypeReqDescription
datestringyes–
exercisesarrayyes–

Structured output declared, but exposes no named fields.

No examples provided.

log_strength_entry ~364

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.

NameTypeReqDescription
datestringyes–
exercisesarrayyes–
mergeboolean––
note–––

Structured output declared, but exposes no named fields.

No examples provided.

log_symptom ~604

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.

NameTypeReqDescription
body_location–––
date–––
display_name–––
end_at–––
note–––
onset_at–––
severitystring––
symptom_typestringyes–
triggers–––

Structured output declared, but exposes no named fields.

No examples provided.

log_weight_entry ~288

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. A weight's home is Apple Health: the phone carries this one there on its next sync, through the iOS setting "Allow AI to update Apple Health" (MCP tab; OFF by default). It is also readable here at once (`get_metric` series "weight_kg"). ⚠️ When no iPhone of this account has that setting on (with Weight access granted), the write is REFUSED and nothing is saved — from app 1.3.0 a weight may not live only in the cloud. The error says what to turn on; pass it on and do not retry until the person has done it. Never tell them to re-enter the number by hand in the Vaultbeat weight card instead: logging it in both places makes two entries for one day from different sources and corrupts the trend line.

NameTypeReqDescription
date–––
weight_kgnumberyes–

Structured output declared, but exposes no named fields.

No examples provided.

update_symptom ~265

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.

NameTypeReqDescription
body_location–––
date–––
display_name–––
end_at–––
entry_idstringyes–
note–––
onset_at–––
severity–––
symptom_type–––
triggers–––

Structured output declared, but exposes no named fields.

No examples provided.

vaultbeat_doctor ~332

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.

Input schema present but exposes no named parameters.

Structured output declared, but exposes no named fields.

No examples provided.

Install

How do I install the io.github.Fino-wind/vaultbeat-apple-health MCP server?

io.github.Fino-wind/vaultbeat-apple-health runs locally as a PyPI package, launched with uvx vaultbeat-apple-health. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

pypi · vaultbeat-apple-health

# add to Claude Code
claude mcp add fino-wind-vaultbeat-apple-health -- uvx vaultbeat-apple-health
Common questions

What is the io.github.Fino-wind/vaultbeat-apple-health MCP server?

io.github.Fino-wind/vaultbeat-apple-health is an MCP server listed in the public MCP registry as io.github.Fino-wind/vaultbeat-apple-health. Your AI agent reads your Apple Health data: sleep, HRV, cycle, workouts. Decrypted on your machine. This page covers its PyPI package (vaultbeat-apple-health).

Is the io.github.Fino-wind/vaultbeat-apple-health MCP server safe to use?

io.github.Fino-wind/vaultbeat-apple-health scores 79 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 4 November 2026. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

What tools does the io.github.Fino-wind/vaultbeat-apple-health MCP server expose?

io.github.Fino-wind/vaultbeat-apple-health exposes 29 tools: vaultbeat_doctor, get_symptoms, get_state_of_mind, get_notes, get_strength_log, and 24 more. Their descriptions and schemas cost roughly 12,682 tokens of context every time the server is loaded.

Is the io.github.Fino-wind/vaultbeat-apple-health MCP server still maintained?

io.github.Fino-wind/vaultbeat-apple-health is still listed as active in the MCP registry. We last reached this channel on 4 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.