health-export-mcp 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/health-export.mcpb +0 -0
- package/healthstore.mjs +35 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +5 -5
- package/server.mjs +2 -2
package/README.md
CHANGED
|
@@ -231,6 +231,7 @@ node server.mjs --help
|
|
|
231
231
|
- **Local-first.** It runs on your machine over stdio. There is **no developer server** in the path.
|
|
232
232
|
- **Optional pairing.** Set `PAIRING_SECRET` to the code the iOS app shows (Settings → Agent pairing) to gate access.
|
|
233
233
|
- **Encrypted exports (optional).** If you switch on Export encryption in the iOS app, set `HEALTH_EXPORT_PASSPHRASE` (or `HEALTH_EXPORT_PASSPHRASE_FILE`) to the same passphrase and the server decrypts on your machine. See [Encrypted exports](#encrypted-exports).
|
|
234
|
+
- **No telemetry.** The server makes no network requests. When it finds no usable export (none yet, a stale one, or `--demo`), `get_mcp_status` includes one static link to the companion app's website. That link carries fixed campaign labels (`utm_source=mcp`, `utm_medium=agent-tool`, `utm_campaign=mcp-unlock`) that are identical for every user and contain nothing about you or your data; they only count if you choose to open it.
|
|
234
235
|
- **Auditable.** Zero dependencies and a few hundred lines of readable JavaScript, read every line.
|
|
235
236
|
- **Signed releases.** Hosted artifacts are minisign-signed and checksummed, see [Verifying releases](#verifying-releases).
|
|
236
237
|
|
package/health-export.mcpb
CHANGED
|
Binary file
|
package/healthstore.mjs
CHANGED
|
@@ -44,6 +44,32 @@ const DATA_DIR = (RAW_DIR === '~' || RAW_DIR.startsWith('~/'))
|
|
|
44
44
|
* elsewhere would silently diverge the moment tilde expansion or env handling changed here. */
|
|
45
45
|
export const dataPath = (file) => path.join(DATA_DIR, file);
|
|
46
46
|
|
|
47
|
+
// THE INSTALL-INTENT -> UNLOCK MIDDLE, MADE VISIBLE (2026-10-13).
|
|
48
|
+
// The companion iOS app writes the export this server reads, and the funnel into it used to be
|
|
49
|
+
// unmeasurable: install intent runs orders of magnitude above unlocks, and no tool answer carried
|
|
50
|
+
// an attributable link to the app. This is that link. It is TEXT ONLY -- adding it changes no
|
|
51
|
+
// network behaviour, so the server still makes zero runtime calls and the offline privacy promise
|
|
52
|
+
// is untouched.
|
|
53
|
+
//
|
|
54
|
+
// `utm_campaign=mcp-unlock` names THIS surface, so a visit lands under one campaign in the site's
|
|
55
|
+
// first-party analytics (Umami) instead of the undifferentiated direct bucket. site/app.js then
|
|
56
|
+
// stamps the same value onto the App Store link's Apple campaign token (ct) on the way through, so
|
|
57
|
+
// App Store Connect attributes the DOWNLOAD, not just the click on our site. `utm_source=mcp` and
|
|
58
|
+
// `utm_medium=agent-tool` keep the surface separable from paid and organic traffic.
|
|
59
|
+
export const UNLOCK_URL = 'https://www.healthexport.dev/?utm_source=mcp&utm_medium=agent-tool&utm_campaign=mcp-unlock';
|
|
60
|
+
|
|
61
|
+
/** The user-facing app card. get_mcp_status (the tool agents call first) returns it ONLY when this
|
|
62
|
+
* machine has no usable export: none at all, a stale one (a paused trial stops the writes), or the
|
|
63
|
+
* synthetic demo dataset. A user with a working export already has the app, and a link on every
|
|
64
|
+
* answer would be promotion rather than a description of what is missing, which is what MCP
|
|
65
|
+
* directory review rules object to. Exported so the tests read the same string. */
|
|
66
|
+
export const APP_INFO = {
|
|
67
|
+
name: 'MetricBridge',
|
|
68
|
+
unlockUrl: UNLOCK_URL,
|
|
69
|
+
note: 'The companion iOS app writes the export this server reads. unlockUrl is its website and carries fixed campaign labels (utm_source, utm_medium, utm_campaign) that are the same for every user. One-time unlock, no subscription.',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
47
73
|
// Guard against reading something pathological, but sized for a real full-history cache.
|
|
48
74
|
// Measured on this cache's exact shape (189 metrics x 3,653 days): 22.0 MB compact. The old
|
|
49
75
|
// 64 MB ceiling was within reach of a long history once workouts were included -- and worse, the
|
|
@@ -459,6 +485,8 @@ export async function status() {
|
|
|
459
485
|
// Write times are still readable when the contents are not.
|
|
460
486
|
freshness: await freshness({ detail: false }),
|
|
461
487
|
metrics: [],
|
|
488
|
+
// No `app` block here: an export that exists but cannot be decrypted was written by the app,
|
|
489
|
+
// so the fix is the passphrase in `note`, not a link to the app.
|
|
462
490
|
};
|
|
463
491
|
}
|
|
464
492
|
const names = Object.keys(metrics);
|
|
@@ -467,9 +495,15 @@ export async function status() {
|
|
|
467
495
|
const d = m?.daily?.[m.daily.length - 1]?.d;
|
|
468
496
|
if (d && (!lastDate || d > lastDate)) lastDate = d;
|
|
469
497
|
}
|
|
498
|
+
const fresh = await freshness({ detail: false });
|
|
499
|
+
// Locked is neither case: a pairing gate file exists only because the app wrote it.
|
|
500
|
+
const noUsableExport = DEMO || (p.ok && (names.length === 0 || fresh.stale === true));
|
|
470
501
|
return {
|
|
471
502
|
ok: names.length > 0 && p.ok,
|
|
472
503
|
source: sourceLabel(),
|
|
504
|
+
// The campaign-tagged companion-app link, only when there is no usable export (see APP_INFO).
|
|
505
|
+
// Additive and text only: no runtime network call is made.
|
|
506
|
+
...(noUsableExport && { app: APP_INFO }),
|
|
473
507
|
// Demo mode is reported LOUDLY and first-class here, not only in the per-answer watermark:
|
|
474
508
|
// get_mcp_status is the tool agents are told to call first, so this is where a human debugging
|
|
475
509
|
// "why do my numbers look wrong" finds out they are not their numbers at all.
|
|
@@ -489,7 +523,7 @@ export async function status() {
|
|
|
489
523
|
lastDataDate: lastDate,
|
|
490
524
|
// Additive: the compact staleness signal (get_freshness has the per-file breakdown), so an
|
|
491
525
|
// agent's first call already tells it whether to trust today's numbers.
|
|
492
|
-
freshness:
|
|
526
|
+
freshness: fresh,
|
|
493
527
|
// Late-arrival recompute (cache schema minor 1). `supported: false` means the app that wrote
|
|
494
528
|
// this cache predates it: past days are only as fresh as the run that last covered them.
|
|
495
529
|
recompute: (() => {
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "health-export-ai",
|
|
4
4
|
"display_name": "MetricBridge",
|
|
5
|
-
"version": "1.6.
|
|
5
|
+
"version": "1.6.3",
|
|
6
6
|
"description": "Query your Apple Health data, exported by the MetricBridge iOS app to iCloud Drive, from any MCP agent. Local-first, read-only, zero dependencies.",
|
|
7
7
|
"long_description": "A zero-dependency MCP server that exposes 190 Apple Health metrics (exported by the MetricBridge iOS app into an iCloud Drive folder) to any MCP-compatible agent: Claude Desktop, opencode, Cursor, OpenClaw, Hermes. No bridge, no Docker, no HTTP receiver: the server reads the synced cache file directly over stdio. 16 tools: get_mcp_status, get_freshness, list_metrics, get_health_metrics, get_trends, compare_periods, get_structured_export, get_intraday, query_health_data, list_events, get_profile, get_workouts, get_sleep_sessions, get_cycle_context, correlate_metrics, resolve_metric. Plus 22 MCP prompts and a --demo mode with a synthetic dataset. Get the companion iOS app on the App Store: https://apps.apple.com/app/id6784185201",
|
|
8
8
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "health-export-mcp",
|
|
3
3
|
"mcpName": "io.github.PhilipAD/health-export-mcp",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Open-source, zero-dependency Model Context Protocol (MCP) server to query Apple Health / HealthKit data (190 metrics) from Claude, ChatGPT, Cursor, OpenClaw, Hermes and any MCP agent.",
|
|
7
7
|
"keywords": [
|
package/server.json
CHANGED
|
@@ -8,22 +8,22 @@
|
|
|
8
8
|
"url": "https://github.com/PhilipAD/health-export-mcp",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
-
"version": "1.6.
|
|
11
|
+
"version": "1.6.3",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "health-export-mcp",
|
|
17
|
-
"version": "1.6.
|
|
17
|
+
"version": "1.6.3",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"registryType": "mcpb",
|
|
24
|
-
"identifier": "https://github.com/PhilipAD/health-export-mcp/releases/download/v1.6.
|
|
25
|
-
"version": "1.6.
|
|
26
|
-
"fileSha256": "
|
|
24
|
+
"identifier": "https://github.com/PhilipAD/health-export-mcp/releases/download/v1.6.3/health-export.mcpb",
|
|
25
|
+
"version": "1.6.3",
|
|
26
|
+
"fileSha256": "c6f08a4db4ffafcf14d3fde0f86f2f4331a9f6f2aab59c7b8b5ce5b80d8c77a7",
|
|
27
27
|
"transport": {
|
|
28
28
|
"type": "stdio"
|
|
29
29
|
}
|
package/server.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import * as ev from './events.mjs';
|
|
|
21
21
|
import { PROMPTS } from './prompts.mjs';
|
|
22
22
|
import { DEMO } from './demo.mjs';
|
|
23
23
|
|
|
24
|
-
const SERVER = { name: 'health-export-ai', version: '1.6.
|
|
24
|
+
const SERVER = { name: 'health-export-ai', version: '1.6.3' };
|
|
25
25
|
const DEFAULT_PROTOCOL = '2025-06-18';
|
|
26
26
|
const log = (...a) => process.stderr.write('[mcp] ' + a.join(' ') + '\n');
|
|
27
27
|
|
|
@@ -57,7 +57,7 @@ const ANNOTATIONS = { readOnlyHint: true, idempotentHint: true, openWorldHint: f
|
|
|
57
57
|
const TOOLS = [
|
|
58
58
|
{
|
|
59
59
|
name: 'get_mcp_status',
|
|
60
|
-
description: 'Health check: data source, how many metrics/workouts are available, which optional context files exist, the most recent data date, and a `recompute` summary of past days rebuilt after late-arriving samples (with any days too old to rebuild). Call this first to confirm the bridge is connected.',
|
|
60
|
+
description: 'Health check: data source, how many metrics/workouts are available, which optional context files exist, the most recent data date, and a `recompute` summary of past days rebuilt after late-arriving samples (with any days too old to rebuild). Call this first to confirm the bridge is connected. When there is no usable export (none found, a stale one, or demo data), the result also includes `app.unlockUrl`, the website of the companion iOS app that writes the export; the link carries fixed campaign attribution labels.',
|
|
61
61
|
inputSchema: { type: 'object', properties: {} },
|
|
62
62
|
handler: () => store.status(),
|
|
63
63
|
},
|