health-export-mcp 1.4.3 → 1.4.4
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/apply-mcp-config.mjs +2 -2
- package/health-export.mcpb +0 -0
- package/healthstore.mjs +2 -2
- package/manifest.json +1 -1
- package/package.json +1 -2
- package/receiver.mjs +40 -1
- package/server.json +2 -2
- package/server.mjs +1 -1
- package/smithery.yaml +0 -29
package/apply-mcp-config.mjs
CHANGED
|
@@ -173,11 +173,11 @@ if (spec.shape === 'vscode') {
|
|
|
173
173
|
const m = typeof v === 'string' && v.match(/^\$\{input:([A-Za-z0-9_]+)\}$/);
|
|
174
174
|
if (m) used.add(m[1]);
|
|
175
175
|
}
|
|
176
|
-
const DESC = { health_pairing_secret: '
|
|
176
|
+
const DESC = { health_pairing_secret: 'MetricBridge pairing code', health_listen_token: 'MetricBridge LAN token (iOS pairing code)' };
|
|
177
177
|
root.inputs = Array.isArray(root.inputs) ? root.inputs : [];
|
|
178
178
|
for (const id of used) {
|
|
179
179
|
if (!root.inputs.some((i) => i && i.id === id)) {
|
|
180
|
-
root.inputs.push({ type: 'promptString', id, description: DESC[id] || '
|
|
180
|
+
root.inputs.push({ type: 'promptString', id, description: DESC[id] || 'MetricBridge secret', password: true });
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
root.inputs = root.inputs.filter((i) => !(i && /^health_(pairing_secret|listen_token)$/.test(i.id) && !used.has(i.id)));
|
package/health-export.mcpb
CHANGED
|
Binary file
|
package/healthstore.mjs
CHANGED
|
@@ -121,11 +121,11 @@ export async function loadMetrics() {
|
|
|
121
121
|
const meta = raw._meta;
|
|
122
122
|
if (meta && Number(meta.schema) > SUPPORTED_SCHEMA) {
|
|
123
123
|
throw new Error(
|
|
124
|
-
`This health cache was written by
|
|
124
|
+
`This health cache was written by MetricBridge ${meta.app ?? 'a newer version'} using ` +
|
|
125
125
|
`cache schema ${meta.schema}, but this MCP server only understands ${SUPPORTED_SCHEMA}. ` +
|
|
126
126
|
// These are the two channels the site and SKILL.md actually document. (npm publishes
|
|
127
127
|
// health-export-mcp too, but the pinned artifacts below are the checksum-verified path.)
|
|
128
|
-
`Update the
|
|
128
|
+
`Update the health-export-mcp server: reinstall the bundle from https://www.healthexport.dev/health-export.mcpb, or re-download https://www.healthexport.dev/mcp/healthstore.mjs. Reading it anyway ` +
|
|
129
129
|
`could report wrong numbers.`);
|
|
130
130
|
}
|
|
131
131
|
// NULL PROTOTYPE. Every existence check here is `metrics[name]`, so inherited keys answered for
|
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.4.
|
|
5
|
+
"version": "1.4.4",
|
|
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. 14 tools: get_mcp_status, 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. 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.4.
|
|
4
|
+
"version": "1.4.4",
|
|
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": [
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"gen-deeplinks.mjs",
|
|
43
43
|
"manifest.json",
|
|
44
44
|
"server.json",
|
|
45
|
-
"smithery.yaml",
|
|
46
45
|
"glama.json",
|
|
47
46
|
".mcp.json",
|
|
48
47
|
"health-export.mcpb",
|
package/receiver.mjs
CHANGED
|
@@ -67,11 +67,50 @@ function sanitize(incoming) {
|
|
|
67
67
|
return out;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Validate the `_authoritative` window the app sends alongside a push (AppModel LAN branch).
|
|
71
|
+
// Same distrust as `sanitize`: strict day format, ordered range, allowlisted metric names.
|
|
72
|
+
const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
73
|
+
function sanitizeAuthoritative(incoming) {
|
|
74
|
+
const a = incoming && typeof incoming === 'object' ? incoming._authoritative : null;
|
|
75
|
+
if (!a || typeof a !== 'object') return null;
|
|
76
|
+
const { start, end } = a;
|
|
77
|
+
if (typeof start !== 'string' || typeof end !== 'string' || !DAY_RE.test(start) || !DAY_RE.test(end) || start > end) return null;
|
|
78
|
+
if (!Array.isArray(a.metrics)) return null;
|
|
79
|
+
const metrics = new Set(a.metrics.slice(0, MAX_METRICS).filter((n) => typeof n === 'string' && /^[A-Za-z0-9_]{1,64}$/.test(n)));
|
|
80
|
+
return { start, end, metrics };
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
// Merge a pushed (sanitised) summary into the cache — union by metric+day, last-write-wins.
|
|
84
|
+
// When the push carries `_authoritative` {start, end, metrics}, the app just re-read those days for
|
|
85
|
+
// those metrics, so inside that window the push IS the truth: any cached day it did not produce was
|
|
86
|
+
// deleted in Apple Health (or is a ghost key from a previous time zone) and is pruned. Mirrors
|
|
87
|
+
// `HealthCache.merge(_:_:authoritativeDays:authoritativeMetrics:)`; without it a LAN-only cache
|
|
88
|
+
// kept deletions and TZ ghost days forever. Metrics not listed (e.g. sensitive types excluded from
|
|
89
|
+
// the run) are never pruned. `yyyy-MM-dd` is fixed-width, so string order IS chronological.
|
|
71
90
|
function mergeCache(existing, incoming) {
|
|
72
91
|
const merged = { ...(existing || {}) };
|
|
73
|
-
|
|
92
|
+
const cleaned = sanitize(incoming);
|
|
93
|
+
const auth = sanitizeAuthoritative(incoming);
|
|
94
|
+
const inRange = (d) => !!auth && d >= auth.start && d <= auth.end;
|
|
95
|
+
|
|
96
|
+
if (auth) {
|
|
97
|
+
for (const name of auth.metrics) {
|
|
98
|
+
if (cleaned[name] || !Object.hasOwn(merged, name)) continue;
|
|
99
|
+
const prev = Array.isArray(merged[name]?.daily) ? merged[name].daily : [];
|
|
100
|
+
const kept = prev.filter((p) => !inRange(p?.d));
|
|
101
|
+
if (kept.length === prev.length) continue;
|
|
102
|
+
// A full wipe removes the metric, not a present-but-empty shell agents read as "known".
|
|
103
|
+
if (kept.length === 0) delete merged[name];
|
|
104
|
+
else merged[name] = { ...merged[name], daily: kept };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const [name, m] of Object.entries(cleaned)) {
|
|
74
109
|
const byDay = new Map((merged[name]?.daily || []).map((p) => [p.d, p.v]));
|
|
110
|
+
if (auth && auth.metrics.has(name)) {
|
|
111
|
+
const incomingDays = new Set(m.daily.map((p) => p.d));
|
|
112
|
+
for (const d of [...byDay.keys()]) if (inRange(d) && !incomingDays.has(d)) byDay.delete(d);
|
|
113
|
+
}
|
|
75
114
|
for (const p of m.daily) byDay.set(p.d, p.v);
|
|
76
115
|
merged[name] = { unit: m.unit || merged[name]?.unit || '', cumulative: !!m.cumulative,
|
|
77
116
|
daily: [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([d, v]) => ({ d, v })) };
|
package/server.json
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
"url": "https://github.com/PhilipAD/health-export-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.4.
|
|
9
|
+
"version": "1.4.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
14
14
|
"identifier": "health-export-mcp",
|
|
15
|
-
"version": "1.4.
|
|
15
|
+
"version": "1.4.4",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
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.4.
|
|
24
|
+
const SERVER = { name: 'health-export-ai', version: '1.4.4' };
|
|
25
25
|
const DEFAULT_PROTOCOL = '2025-06-18';
|
|
26
26
|
const log = (...a) => process.stderr.write('[mcp] ' + a.join(' ') + '\n');
|
|
27
27
|
|
package/smithery.yaml
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# Smithery deployment configuration for health-export-mcp
|
|
2
|
-
# https://smithery.ai/docs/deploy
|
|
3
|
-
build:
|
|
4
|
-
dockerfile: Dockerfile
|
|
5
|
-
dockerBuildPath: .
|
|
6
|
-
startCommand:
|
|
7
|
-
type: stdio
|
|
8
|
-
configSchema:
|
|
9
|
-
type: object
|
|
10
|
-
properties:
|
|
11
|
-
healthDataDir:
|
|
12
|
-
type: string
|
|
13
|
-
description: "Path to Apple Health export directory containing export.xml (local path on user machine)"
|
|
14
|
-
default: ""
|
|
15
|
-
healthListen:
|
|
16
|
-
type: string
|
|
17
|
-
description: "Set to '1' to enable LAN push from iOS Health Export app"
|
|
18
|
-
default: ""
|
|
19
|
-
required: []
|
|
20
|
-
commandFunction:
|
|
21
|
-
|-
|
|
22
|
-
(config) => ({
|
|
23
|
-
command: 'node',
|
|
24
|
-
args: ['server.mjs'],
|
|
25
|
-
env: {
|
|
26
|
-
HEALTH_DATA_DIR: config.healthDataDir || '',
|
|
27
|
-
HEALTH_LISTEN: config.healthListen || ''
|
|
28
|
-
}
|
|
29
|
-
})
|