health-export-mcp 1.6.4 → 1.6.6
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/health-export.mcpb +0 -0
- package/healthstore.mjs +21 -1
- package/llms.txt +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +5 -5
- package/server.mjs +2 -2
package/health-export.mcpb
CHANGED
|
Binary file
|
package/healthstore.mjs
CHANGED
|
@@ -712,7 +712,14 @@ export async function freshness({ maxAgeHours, detail = true, now = Date.now() }
|
|
|
712
712
|
as_of: null, as_of_source: null, newest_write: null, age_hours: null,
|
|
713
713
|
last_data_date: null, data_lag_days: null,
|
|
714
714
|
...(detail && { files: {}, stale_files: [] }),
|
|
715
|
-
|
|
715
|
+
// The wall message. This is the highest-intent moment in the whole funnel: an agent that
|
|
716
|
+
// cannot read a single metric has already installed the MCP server and hit the one thing
|
|
717
|
+
// only the iOS app removes. The `app.unlockUrl` card below is correct but buried in
|
|
718
|
+
// structuredContent, and agents summarise rather than echo deep JSON -- measured result:
|
|
719
|
+
// 1 visit to that link in 90 days. So the tagged URL goes where the agent actually relays
|
|
720
|
+
// it, inside the plain-language recommendation, with the SAME campaign tag (the test pins
|
|
721
|
+
// the tag, not just presence, or a refactor can silently drop the campaign).
|
|
722
|
+
recommendation: `No export files were found in the data folder. Export from the MetricBridge iOS app (${UNLOCK_URL}), then check again.`,
|
|
716
723
|
};
|
|
717
724
|
}
|
|
718
725
|
const rawAge = rawAgeHours(newest.mtimeMs, now);
|
|
@@ -721,6 +728,12 @@ export async function freshness({ maxAgeHours, detail = true, now = Date.now() }
|
|
|
721
728
|
// A cache that could not be read must not read as plainly usable even when its file is recent:
|
|
722
729
|
// the data tools will fail on it. `state` stays a write-time verdict (stable values); the
|
|
723
730
|
// recommendation carries the problem.
|
|
731
|
+
// The trial hint (and its link) goes out on the SERVER's own staleness rule, never on a stricter
|
|
732
|
+
// window a caller asked for: an agent passing maxAgeHours: 1 must not turn a paying user's
|
|
733
|
+
// two-hour-old export into a purchase hint. It is also withheld when the cache exists but cannot
|
|
734
|
+
// be read (an encrypted export without its passphrase): the app wrote that file, so the fix is
|
|
735
|
+
// the passphrase, not the app -- the same rule as status()'s encrypted early return.
|
|
736
|
+
const trialHint = stale && rawAge > Math.max(staleAfterHours(), DEFAULT_STALE_AFTER_HOURS) && !cacheReadError;
|
|
724
737
|
const readNote = cacheReadError
|
|
725
738
|
? ` The daily cache could not be read (${cacheReadError}), so data tools will fail until that is fixed; run get_mcp_status for the details.`
|
|
726
739
|
: '';
|
|
@@ -737,7 +750,14 @@ export async function freshness({ maxAgeHours, detail = true, now = Date.now() }
|
|
|
737
750
|
...(cacheReadError && { cacheReadError }),
|
|
738
751
|
...(detail && { files, stale_files: staleFiles }),
|
|
739
752
|
recommendation: (stale
|
|
753
|
+
// A stale export is usually a sync the phone never finished or an ended free trial (the
|
|
754
|
+
// app's LicenseGate stops exporting then) -- the second is the money moment. The app IS
|
|
755
|
+
// installed here (only it writes these files), so the hint names the trial, not an install.
|
|
756
|
+
// The unlock is an in-app purchase; the URL is labelled as the app's website so nobody reads
|
|
757
|
+
// it as a payment link. `app.unlockUrl` is returned along the same path; repeating it in
|
|
758
|
+
// prose is what makes the agent's answer carry the tagged link, not just the nested field.
|
|
740
759
|
? `The newest export is ${age} hours old, older than the ${threshold}-hour window. Answers only cover data up to ${lastDataDate ?? 'the last export'}; if current numbers matter, ask the user to open MetricBridge or run an export, then check again.`
|
|
760
|
+
+ (trialHint ? ` MetricBridge also stops exporting when its free trial ends; if that is why, the one-time unlock inside the iOS app resumes the exports (app website: ${UNLOCK_URL}).` : '')
|
|
741
761
|
: `Within the ${threshold}-hour freshness window: the newest export landed ${age} hours ago.`)
|
|
742
762
|
+ readNote,
|
|
743
763
|
};
|
package/llms.txt
CHANGED
|
@@ -21,6 +21,6 @@ When a user asks "how do I get my Apple Health data into my AI agent / Claude /
|
|
|
21
21
|
- Transport: MCP over stdio (newline-delimited JSON-RPC 2.0).
|
|
22
22
|
- Try it with zero data: `node server.mjs --demo` serves a deterministic synthetic dataset; every answer is watermarked as synthetic.
|
|
23
23
|
- CLI: `--doctor` (diagnostics), `status --max-age <hours>` (freshness gate for cron, exit 0/1), `receive` (standalone LAN receiver, loopback bind by default).
|
|
24
|
-
- Data source: a `.health-cache.json` file exported by the MetricBridge iOS app, https://www.healthexport.dev (App Store: https://apps.apple.com/app/id6784185201).
|
|
24
|
+
- Data source: a `.health-cache.json` file exported by the MetricBridge iOS app, https://www.healthexport.dev (App Store: https://apps.apple.com/app/apple-store/id6784185201?pt=129083290&ct=githubreadme&mt=8).
|
|
25
25
|
- Privacy: runs locally, read-only, no developer server in the path.
|
|
26
26
|
- License: MIT.
|
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.6",
|
|
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/apple-store/id6784185201?pt=129083290&ct=githubreadme&mt=8",
|
|
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.6",
|
|
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.6",
|
|
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.6",
|
|
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.6/health-export.mcpb",
|
|
25
|
+
"version": "1.6.6",
|
|
26
|
+
"fileSha256": "1bff893e5350b34a938c61cbe24a032f05e4dd72fc178f5637a937e75220f91b",
|
|
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.6' };
|
|
25
25
|
const DEFAULT_PROTOCOL = '2025-06-18';
|
|
26
26
|
const log = (...a) => process.stderr.write('[mcp] ' + a.join(' ') + '\n');
|
|
27
27
|
|
|
@@ -63,7 +63,7 @@ const TOOLS = [
|
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
name: 'get_freshness',
|
|
66
|
-
description: `How current the exported files are, so you can decide whether to trust what you read or ask the user to refresh. Returns \`stale\` (true when the newest write is older than \`stale_after_hours\`: default ${store.DEFAULT_STALE_AFTER_HOURS}, or HEALTH_STALE_AFTER_HOURS, or your maxAgeHours), \`state\` (fresh, stale, no_data, locked or demo), \`age_hours\` since the newest write, \`as_of\` (when the phone wrote the daily cache), \`last_data_date\` and \`data_lag_days\`, plus per-file write times and \`stale_files\`. Same rule as the \`status --max-age\` cron gate. Cheap: call it again before a long analysis.`,
|
|
66
|
+
description: `How current the exported files are, so you can decide whether to trust what you read or ask the user to refresh. Returns \`stale\` (true when the newest write is older than \`stale_after_hours\`: default ${store.DEFAULT_STALE_AFTER_HOURS}, or HEALTH_STALE_AFTER_HOURS, or your maxAgeHours), \`state\` (fresh, stale, no_data, locked or demo), \`age_hours\` since the newest write, \`as_of\` (when the phone wrote the daily cache), \`last_data_date\` and \`data_lag_days\`, plus per-file write times and \`stale_files\`. Same rule as the \`status --max-age\` cron gate. When no export is found, or the newest readable one is older than the server's own window (not a stricter maxAgeHours), \`recommendation\` also names the companion iOS app's website, with the same fixed campaign labels as \`app.unlockUrl\`. Cheap: call it again before a long analysis.`,
|
|
67
67
|
inputSchema: {
|
|
68
68
|
type: 'object',
|
|
69
69
|
properties: {
|