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.
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
- recommendation: 'No export files were found in the data folder. Export from the MetricBridge iOS app, then check again.',
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.4",
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",
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.4",
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.4",
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.4/health-export.mcpb",
25
- "version": "1.6.4",
26
- "fileSha256": "54e038874281809f6d217eb76a94684951f94718bfe4189b2d6d6171e4bb3c78",
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.4' };
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: {