health-export-mcp 1.2.2
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/.dockerignore +5 -0
- package/.mcp.json +11 -0
- package/AGENTS.md +85 -0
- package/Dockerfile +8 -0
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/apply-mcp-config.mjs +215 -0
- package/gen-deeplinks.mjs +37 -0
- package/glama.json +46 -0
- package/health-export.mcpb +0 -0
- package/healthstore.mjs +613 -0
- package/llms.txt +18 -0
- package/manifest.json +51 -0
- package/minisign.pub +2 -0
- package/package.json +64 -0
- package/receiver.mjs +238 -0
- package/server.json +21 -0
- package/server.mjs +222 -0
- package/smithery.yaml +29 -0
- package/test/integration.mjs +118 -0
package/.dockerignore
ADDED
package/.mcp.json
ADDED
package/AGENTS.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Using health-export-mcp with your AI client
|
|
2
|
+
|
|
3
|
+
Copy-paste setup for every supported agent. The server is a single Node script (`server.mjs`,
|
|
4
|
+
zero dependencies). Point `HEALTH_DATA_DIR` at the folder that holds your exported
|
|
5
|
+
`.health-cache.json` — by default the [Health Export AI](https://www.healthexport.dev) iOS app
|
|
6
|
+
([App Store](https://apps.apple.com/app/id6784185201)) writes it to your iCloud Drive container:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
~/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Tip:** get the absolute path to `server.mjs` with `node -e "console.log(process.cwd()+'/server.mjs')"` (run inside the repo), or let `node apply-mcp-config.mjs` write the config for you.
|
|
13
|
+
|
|
14
|
+
After configuring, restart the client and ask: **"Use health-export: what's my HRV trend this week?"**
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Claude Desktop
|
|
19
|
+
|
|
20
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"health-export": {
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": ["/ABSOLUTE/PATH/TO/health-export-mcp/server.mjs"],
|
|
28
|
+
"env": { "HEALTH_DATA_DIR": "~/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or drag `health-export.mcpb` into **Claude Desktop → Settings → Extensions** (no JSON).
|
|
35
|
+
|
|
36
|
+
## Cursor
|
|
37
|
+
|
|
38
|
+
`~/.cursor/mcp.json` (same `mcpServers` shape as above), or run `node gen-deeplinks.mjs` for a one-click install link.
|
|
39
|
+
|
|
40
|
+
## VS Code (Copilot / Continue)
|
|
41
|
+
|
|
42
|
+
Run `node gen-deeplinks.mjs` and open the generated VS Code link, or add the server under your MCP settings.
|
|
43
|
+
|
|
44
|
+
## opencode
|
|
45
|
+
|
|
46
|
+
`opencode.json` (project root) or `~/.config/opencode/opencode.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"$schema": "https://opencode.ai/config.json",
|
|
51
|
+
"mcp": {
|
|
52
|
+
"health-export": {
|
|
53
|
+
"type": "local",
|
|
54
|
+
"command": ["node", "/ABSOLUTE/PATH/TO/health-export-mcp/server.mjs"],
|
|
55
|
+
"enabled": true,
|
|
56
|
+
"environment": { "HEALTH_DATA_DIR": "~/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## OpenClaw
|
|
63
|
+
|
|
64
|
+
Add the `health-export` block (same shape as Claude Desktop's `mcpServers` entry) to your OpenClaw MCP servers config, then reload.
|
|
65
|
+
|
|
66
|
+
## Hermes
|
|
67
|
+
|
|
68
|
+
Add the `health-export` block to your Hermes agent's MCP servers config, then restart the daemon.
|
|
69
|
+
|
|
70
|
+
## Non-MCP clients — ChatGPT · Gemini · Grok · n8n · Home Assistant
|
|
71
|
+
|
|
72
|
+
These don't speak MCP, so they don't use this server. Instead, the [Health Export AI](https://www.healthexport.dev) app **POSTs your data to a webhook** (or your own endpoint) and your automation/agent reads that JSON. Every delivery is token-authenticated.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Auto-configure
|
|
77
|
+
|
|
78
|
+
`node apply-mcp-config.mjs` detects installed clients and writes the config for you.
|
|
79
|
+
|
|
80
|
+
## Example prompts
|
|
81
|
+
|
|
82
|
+
- "What's my average resting heart rate over the last 7 days?"
|
|
83
|
+
- "Compare my deep sleep this week vs last week."
|
|
84
|
+
- "Is my VO₂ max trending up this quarter?"
|
|
85
|
+
- "Export HRV, RHR and sleep as JSON for the last 14 days."
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# health-export-mcp — zero-dependency MCP server (stdio JSON-RPC 2.0).
|
|
2
|
+
# Used by directories (e.g. Glama) to build the server and run MCP introspection
|
|
3
|
+
# (initialize + tools/list). No real Apple Health data is required for introspection.
|
|
4
|
+
FROM node:20-alpine
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
COPY . .
|
|
7
|
+
# Zero dependencies — nothing to install.
|
|
8
|
+
ENTRYPOINT ["node", "server.mjs"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philip D'Souza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# health-export-mcp — Apple Health MCP server for AI agents
|
|
2
|
+
|
|
3
|
+
**Query your Apple Health data from Claude, ChatGPT, Cursor, OpenClaw, Hermes — and any other AI agent.**
|
|
4
|
+
|
|
5
|
+
`health-export-mcp` is an open-source, **zero-dependency** [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that lets any MCP-compatible AI agent query your **Apple Health / HealthKit** data — **190 metrics** as clean JSON — in plain language. Local-first, read-only, no accounts, and no developer server in the path. It's the open-source server for the [Health Export AI](https://www.healthexport.dev) iOS app — **[available on the App Store](https://apps.apple.com/app/id6784185201)**.
|
|
6
|
+
|
|
7
|
+
<p align="center"><a href="https://apps.apple.com/app/id6784185201"><img alt="Download Health Export AI on the App Store" src="https://img.shields.io/badge/Download_on_the-App_Store-0D96F6?style=for-the-badge&logo=apple&logoColor=white" height="38" /></a></p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://glama.ai/mcp/servers/PhilipAD/health-export-mcp"><img alt="Glama MCP Server" src="https://glama.ai/mcp/servers/PhilipAD/health-export-mcp/badges/score.svg" /></a>
|
|
11
|
+
<img alt="MCP" src="https://img.shields.io/badge/Model_Context_Protocol-server-6E56CF" />
|
|
12
|
+
<img alt="zero dependencies" src="https://img.shields.io/badge/dependencies-0-2ea44f" />
|
|
13
|
+
<img alt="Node ≥18" src="https://img.shields.io/badge/node-%E2%89%A518-339933?logo=node.js&logoColor=white" />
|
|
14
|
+
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue" />
|
|
15
|
+
<img alt="works with Claude, Cursor, ChatGPT, OpenClaw, Hermes" src="https://img.shields.io/badge/works_with-Claude_·_Cursor_·_ChatGPT_·_OpenClaw_·_Hermes-111" />
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="assets/architecture.svg" alt="Apple Health exports to iCloud, a folder, or your LAN; health-export-mcp reads it and serves 7 query tools to your AI agent" width="100%" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
> Ask your agent: *"Compare my HRV this week vs last week and tell me if I'm recovering."* — it calls the tools and answers from your **actual numbers**.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What is health-export-mcp?
|
|
27
|
+
|
|
28
|
+
It's an MCP server that turns your Apple Health export into a tool your AI agent can query in natural language — HRV, sleep, resting heart rate, steps, workouts, VO₂ max, and 180+ more.
|
|
29
|
+
|
|
30
|
+
- **Who it's for:** anyone who wants their AI to reason over their *real* health data instead of a stale CSV.
|
|
31
|
+
- **What it isn't:** a cloud service. There's no developer server in the path — your data goes only where *you* point it.
|
|
32
|
+
- **Setup:** point the server at your exported data and add it to your AI client.
|
|
33
|
+
|
|
34
|
+
> **Try it with no iPhone needed:** `git clone https://github.com/PhilipAD/health-export-mcp.git && cd health-export-mcp && npm test` writes a 14-day sample cache and exercises every tool.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Connect Apple Health to your AI agent — Quickstart
|
|
39
|
+
|
|
40
|
+
### 1. Get your Apple Health data flowing
|
|
41
|
+
|
|
42
|
+
The companion iOS app **[Health Export AI](https://www.healthexport.dev)** exports your Apple Health data — read-only, automatic, private. For this MCP server, export to a destination it can read:
|
|
43
|
+
|
|
44
|
+
| Destination | Notes |
|
|
45
|
+
|---|---|
|
|
46
|
+
| **iCloud Drive** (default) | Your Mac reads the synced folder automatically |
|
|
47
|
+
| **Local folder** | Any folder that syncs to your Mac (Dropbox, Google Drive, OneDrive, …) |
|
|
48
|
+
| **LAN** (HTTP / WebSocket) | Direct push to the server — great over Tailscale |
|
|
49
|
+
|
|
50
|
+
> Using a non-MCP tool (ChatGPT, n8n, Home Assistant)? The app can also POST to a **webhook** those tools read directly — see [Works with](#works-with-claude-cursor-chatgpt-openclaw-hermes).
|
|
51
|
+
|
|
52
|
+
### 2. Add the server to your agent
|
|
53
|
+
|
|
54
|
+
**Fastest — auto-configure:**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/PhilipAD/health-export-mcp.git
|
|
58
|
+
cd health-export-mcp
|
|
59
|
+
node apply-mcp-config.mjs # detects installed clients and writes the config for you
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Manual — Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"health-export": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["-y", "health-export-mcp"],
|
|
70
|
+
"env": { "HEALTH_DATA_DIR": "~/Library/Mobile Documents/iCloud~ai~healthexport~app/Documents" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> `npx` fetches the published package at run time (npm verifies package integrity) — it runs anywhere, no clone or absolute path needed. Prefer to pin a vetted local checkout instead? Use `"command": "node", "args": ["REPLACE_WITH_ABSOLUTE_PATH/server.mjs"]` — get the path with `node -e "console.log(process.cwd()+'/server.mjs')"` inside the repo.
|
|
77
|
+
> Or skip JSON entirely — drag **`health-export.mcpb`** into **Claude Desktop → Settings → Extensions**.
|
|
78
|
+
|
|
79
|
+
**Cursor / VS Code:** `node gen-deeplinks.mjs` prints one-click install links.
|
|
80
|
+
**opencode / OpenClaw / Hermes:** see **[AGENTS.md](AGENTS.md)** for the exact block — same shape, one per client.
|
|
81
|
+
|
|
82
|
+
### 3. Ask your agent
|
|
83
|
+
|
|
84
|
+
Restart the client and try:
|
|
85
|
+
|
|
86
|
+
> *"Use health-export: what's my average HRV this week vs last week?"*
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Works with Claude, Cursor, ChatGPT, OpenClaw, Hermes
|
|
91
|
+
|
|
92
|
+
| Client / Agent | Integration | How |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| **Claude Desktop** | Native MCP | `.mcpb` bundle, or `mcpServers` block |
|
|
95
|
+
| **Cursor** | Native MCP | One-click deeplink, or `~/.cursor/mcp.json` |
|
|
96
|
+
| **opencode** | Native MCP | `opencode.json` `mcp` block |
|
|
97
|
+
| **OpenClaw** | Native MCP | Add the server block to your MCP config |
|
|
98
|
+
| **Hermes** | Native MCP | Add the server block to your agent's MCP config |
|
|
99
|
+
| **VS Code** (Copilot / Continue) | Native MCP | One-click deeplink |
|
|
100
|
+
| **ChatGPT · Gemini · Grok** | Webhook* | Consume the app's webhook export |
|
|
101
|
+
| **n8n · Home Assistant** | Webhook* | Trigger automations on the exported JSON |
|
|
102
|
+
|
|
103
|
+
<sub>*MCP clients query this server directly over stdio. ChatGPT / Gemini / Grok / n8n / Home Assistant don't speak MCP — they consume the same Apple Health data via the iOS app's token-authenticated **webhook** export.</sub>
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## The 7 MCP tools
|
|
108
|
+
|
|
109
|
+
| Tool | What it does |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `get_mcp_status` | Health check — source, metric/workout counts, latest data date. **Call first.** |
|
|
112
|
+
| `list_metrics` | Every available metric with unit, day count, and date range. |
|
|
113
|
+
| `get_health_metrics` | Daily values for a metric (or all) over a date range + an aggregate (avg/sum/min/max/latest). |
|
|
114
|
+
| `get_trends` | Recent N-day window vs the prior N days — change, % change, direction. |
|
|
115
|
+
| `compare_periods` | A metric across two arbitrary date periods (A vs B). |
|
|
116
|
+
| `get_structured_export` | Clean JSON for chosen metrics/range — drop straight into context. |
|
|
117
|
+
| `query_health_data` | Natural-language convenience: *"average HRV last month"* → routed structured results. |
|
|
118
|
+
|
|
119
|
+
**Coverage:** 190 Apple Health metrics across activity, heart, HRV, mobility, respiratory, body, sleep, hearing, and nutrition — plus workouts.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Example AI queries
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
"What has my resting heart rate done over the last 30 days?"
|
|
127
|
+
"Compare my deep sleep this week vs last week."
|
|
128
|
+
"Is my VO₂ max trending up or down this quarter?"
|
|
129
|
+
"Give me a clean JSON export of HRV, RHR and sleep for the last 14 days."
|
|
130
|
+
"Correlate my step count with my sleep duration this month."
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Use cases
|
|
136
|
+
|
|
137
|
+
- **AI health coach** — let an agent reason over your real HRV, resting heart rate, and sleep to suggest when to push and when to recover, grounded in your actual numbers instead of generic advice.
|
|
138
|
+
- **Training-load analysis** — pull workouts, VO₂ max, and heart-rate trends so your agent can flag overreaching, plot fitness progression, and pace a training block.
|
|
139
|
+
- **Sleep correlations** — have your agent correlate deep-sleep duration against steps, caffeine, late workouts, or screen time to find what actually moves your sleep quality.
|
|
140
|
+
- **Quantified-self dashboards** — feed clean JSON for any metric set and date range straight into a notebook, spreadsheet, or LLM-built dashboard for your own self-tracking.
|
|
141
|
+
- **Personal research & experiments** — run n-of-1 experiments (supplement, routine, or protocol changes) and let an agent compare before/after periods across 190 metrics to see what changed.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## The app that feeds it
|
|
146
|
+
|
|
147
|
+
<p align="center">
|
|
148
|
+
<img src="assets/screenshot-dashboard.png" alt="Health Export AI dashboard" width="240" />
|
|
149
|
+
|
|
150
|
+
<img src="assets/screenshot-metrics.png" alt="190 Apple Health metrics" width="240" />
|
|
151
|
+
|
|
152
|
+
<img src="assets/screenshot-destinations.png" alt="Export to iCloud, folder, LAN, or webhook" width="240" />
|
|
153
|
+
</p>
|
|
154
|
+
|
|
155
|
+
<p align="center"><sub><a href="https://www.healthexport.dev">Health Export AI</a> — exports your Apple Health to your agent, automatically.</sub></p>
|
|
156
|
+
|
|
157
|
+
<p align="center"><a href="https://apps.apple.com/app/id6784185201"><b>Download on the App Store →</b></a></p>
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Privacy & security
|
|
162
|
+
|
|
163
|
+
- **Read-only.** The server only reads your exported data — it never touches HealthKit and never writes back.
|
|
164
|
+
- **Local-first.** It runs on your machine over stdio. There is **no developer server** in the path.
|
|
165
|
+
- **Optional pairing.** Set `PAIRING_SECRET` to the code the iOS app shows (Settings → Agent pairing) to gate access.
|
|
166
|
+
- **Auditable.** Zero dependencies and a few hundred lines of readable JavaScript — read every line.
|
|
167
|
+
- **Signed releases.** Hosted artifacts are minisign-signed and checksummed — see [Verifying releases](#verifying-releases).
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Verifying releases
|
|
172
|
+
|
|
173
|
+
The server artifacts hosted at `healthexport.dev/mcp/` (used by the [setup skill](https://healthexport.dev/SKILL.md)) are **minisign-signed**, and every download is **SHA-256 checksummed**. The signing public key is published **in this repo** (`minisign.pub`) and in `SKILL.md` — **pin it from here, not only from the website**, so a compromise of the website alone cannot swap both an artifact and its key.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
PUBKEY='RWS6TxVWSKUblYkx7Db6ZpmvHALwHpznZpjaED/FlZj+PpxSlel0MxHZ' # = minisign.pub in this repo
|
|
177
|
+
curl -fsSL https://healthexport.dev/mcp/SHA256SUMS -o SHA256SUMS
|
|
178
|
+
curl -fsSL https://healthexport.dev/mcp/SHA256SUMS.minisig -o SHA256SUMS.minisig
|
|
179
|
+
minisign -Vm SHA256SUMS -P "$PUBKEY" || { echo "signature INVALID — do not run"; exit 1; } # fail closed
|
|
180
|
+
curl -fsSL https://healthexport.dev/mcp/server.mjs -o server.mjs
|
|
181
|
+
shasum -a 256 --ignore-missing -c SHA256SUMS || { echo "checksum mismatch — do not run"; exit 1; }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The pinned, checksum-verified download above is the locked-down path. `npx health-export-mcp` instead resolves the latest version published to npm at run time (npm provides its own package integrity). Full security overview: <https://healthexport.dev/security>. Report vulnerabilities to **security@healthexport.dev**.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Requirements
|
|
189
|
+
|
|
190
|
+
- **Node.js ≥ 18** (`node -v`).
|
|
191
|
+
- A folder containing a `.health-cache.json` exported by [Health Export AI](https://www.healthexport.dev) — or run `npm test` to generate a sample one.
|
|
192
|
+
- An MCP-compatible client (Claude Desktop, Cursor, opencode, OpenClaw, Hermes, VS Code) — or any tool that can read the webhook export.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## FAQ
|
|
197
|
+
|
|
198
|
+
**How do I get my Apple Health data into Claude / ChatGPT / my AI agent?**
|
|
199
|
+
Install `health-export-mcp`, export your Apple Health data with the [Health Export AI](https://www.healthexport.dev) iOS app (to iCloud, a folder, or your LAN), then add the MCP server to your AI client. Your agent can then query your Apple Health metrics in natural language. (Non-MCP tools like ChatGPT read the app's webhook export instead.)
|
|
200
|
+
|
|
201
|
+
**Is my health data sent to a server?**
|
|
202
|
+
Not to us. The MCP server runs locally and reads only the export files or endpoints you configure — there's no developer server in the path. Where your iOS export is delivered (iCloud, your LAN, a webhook) is entirely your choice.
|
|
203
|
+
|
|
204
|
+
**Which agents are supported?**
|
|
205
|
+
Any MCP client — Claude Desktop, Cursor, opencode, OpenClaw, Hermes, VS Code — natively. ChatGPT, Gemini, Grok, n8n, and Home Assistant consume the same data via webhook.
|
|
206
|
+
|
|
207
|
+
**Do I need the iOS app?**
|
|
208
|
+
The app is the easiest way to get Apple Health data off your iPhone in the format this server reads. You can also point `HEALTH_DATA_DIR` at any folder containing a compatible `.health-cache.json`.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
- **"No metrics found"** — confirm `HEALTH_DATA_DIR` points at the folder containing `.health-cache.json`, and that the app has exported at least once. Run `get_mcp_status` to see the resolved source and latest date.
|
|
215
|
+
- **Server not visible in the client** — use an **absolute** path to `server.mjs`, ensure Node ≥18, and fully restart the client.
|
|
216
|
+
- **Locked data error** — the export file is protected until first unlock after reboot; unlock your device once.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Run it locally
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Node ≥18, no install needed
|
|
224
|
+
HEALTH_DATA_DIR=~/Library/Mobile\ Documents/iCloud~ai~healthexport~app/Documents node server.mjs
|
|
225
|
+
|
|
226
|
+
# integration test — writes a 14-day sample cache and exercises every tool
|
|
227
|
+
npm test
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
stdio transport (newline-delimited JSON-RPC 2.0) — the universal MCP transport. Optionally set `HEALTH_LISTEN=1` to also accept LAN pushes from the iOS app in the same process (see [`receiver.mjs`](receiver.mjs)).
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## How it works
|
|
235
|
+
|
|
236
|
+
The iOS app reads Apple Health (read-only) and writes a compact `.health-cache.json` to the destination you choose. This server reads that file and exposes the 7 tools above over MCP. No bridge, no Docker, no database — just a file and stdio.
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
Apple Health → Health Export AI (iOS) → .health-cache.json → health-export-mcp → MCP client → you
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Related projects
|
|
245
|
+
|
|
246
|
+
Other Apple Health MCP servers in the ecosystem — worth a look depending on your setup:
|
|
247
|
+
|
|
248
|
+
- **[neiltron/apple-health-mcp](https://github.com/neiltron/apple-health-mcp)** — an MCP server that runs SQL-style queries over an Apple Health export.
|
|
249
|
+
- **[the-momentum/apple-health-mcp-server](https://github.com/the-momentum/apple-health-mcp-server)** — an MCP server for analyzing Apple Health data exported from the Health app.
|
|
250
|
+
- **[HealthyApps/health-auto-export-mcp-server](https://github.com/HealthyApps/health-auto-export-mcp-server)** — an MCP server for the Health Auto Export app's data.
|
|
251
|
+
|
|
252
|
+
**How `health-export-mcp` differs:** zero dependencies, clean structured JSON, 190 Apple Health metrics, and the widest agent support (Claude, Cursor, opencode, OpenClaw, Hermes, VS Code natively — plus ChatGPT/Gemini/Grok/n8n/Home Assistant via webhook).
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Related
|
|
257
|
+
|
|
258
|
+
- **Companion iOS app:** [Health Export AI](https://www.healthexport.dev) — exports 190 Apple Health metrics to your agent, automatically.
|
|
259
|
+
- **Model Context Protocol:** [modelcontextprotocol.io](https://modelcontextprotocol.io)
|
|
260
|
+
- **Per-agent setup:** [AGENTS.md](AGENTS.md)
|
|
261
|
+
|
|
262
|
+
If this helps your setup, a ⭐ makes it easier for others to find.
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT — see [LICENSE](LICENSE). Contributions welcome.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
<sub>Apple Health MCP server · export Apple Health to AI · HealthKit MCP server · query Apple Health with Claude / ChatGPT / Cursor · Model Context Protocol health server · Apple Health to LLM · HRV, sleep & heart rate for AI agents.</sub>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Health Export AI — safe, idempotent MCP config writer.
|
|
3
|
+
//
|
|
4
|
+
// Adds (or updates) ONE "health-export" server entry in the config file of a
|
|
5
|
+
// given MCP client, preserving everything else. Backs up the file first, writes
|
|
6
|
+
// atomically, and NEVER prints the pairing secret.
|
|
7
|
+
//
|
|
8
|
+
// node apply-mcp-config.mjs --client <id> --server-path <abs> --data-dir <abs> [opts]
|
|
9
|
+
//
|
|
10
|
+
// --client claude-desktop | claude-code | cursor | vscode | opencode (required)
|
|
11
|
+
// --server-path absolute path to server.mjs (required)
|
|
12
|
+
// --data-dir absolute path to the folder holding .health-cache.json (required)
|
|
13
|
+
// --pairing-secret-env read the secret from $HEALTH_EXPORT_PAIRING_SECRET
|
|
14
|
+
// --clear-pairing-secret remove any existing pairing secret from the entry
|
|
15
|
+
// --env KEY=VALUE extra env var for the entry (repeatable). For the LAN
|
|
16
|
+
// listen path: --env HEALTH_LISTEN=1 --env HEALTH_LISTEN_HOST=0.0.0.0
|
|
17
|
+
// --listen-token-env set HEALTH_LISTEN_TOKEN from $HEALTH_EXPORT_LISTEN_TOKEN
|
|
18
|
+
// (the iOS pairing code) — keeps it off argv, like the secret.
|
|
19
|
+
// --name <id> server entry name (default: health-export)
|
|
20
|
+
// --config-path <path> override the client's default config file
|
|
21
|
+
// --dry-run show what would change; write nothing
|
|
22
|
+
//
|
|
23
|
+
// Idempotent re-runs: if you don't pass --pairing-secret-env, an existing secret
|
|
24
|
+
// (or VS Code ${input:…} reference) is PRESERVED, not dropped. Use
|
|
25
|
+
// --clear-pairing-secret to remove it on purpose.
|
|
26
|
+
//
|
|
27
|
+
// Secrets: pass the pairing code via the env var, never on argv (argv is visible
|
|
28
|
+
// to other processes). For VS Code the secret is NEVER written to disk — the
|
|
29
|
+
// config references ${input:...} so VS Code prompts for it.
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
|
|
35
|
+
// ---- args ----------------------------------------------------------------
|
|
36
|
+
const argv = process.argv.slice(2);
|
|
37
|
+
const opt = (k) => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : undefined; };
|
|
38
|
+
const flag = (k) => argv.includes(k);
|
|
39
|
+
|
|
40
|
+
const client = opt('--client');
|
|
41
|
+
const name = opt('--name') || 'health-export';
|
|
42
|
+
const dryRun = flag('--dry-run');
|
|
43
|
+
const useSecretEnv = flag('--pairing-secret-env');
|
|
44
|
+
const clearSecret = flag('--clear-pairing-secret');
|
|
45
|
+
|
|
46
|
+
const expand = (p) => (p && p.startsWith('~')) ? path.join(os.homedir(), p.slice(1)) : p;
|
|
47
|
+
const serverPath = expand(opt('--server-path'));
|
|
48
|
+
const dataDir = expand(opt('--data-dir'));
|
|
49
|
+
const secret = useSecretEnv ? (process.env.HEALTH_EXPORT_PAIRING_SECRET || '') : '';
|
|
50
|
+
const VSCODE_INPUT = '${input:health_pairing_secret}';
|
|
51
|
+
|
|
52
|
+
// repeatable --env KEY=VALUE (e.g. the LAN listen keys)
|
|
53
|
+
const extraEnv = {};
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
if (argv[i] === '--env' && argv[i + 1]) {
|
|
56
|
+
const kv = argv[i + 1]; const eq = kv.indexOf('=');
|
|
57
|
+
if (eq > 0) extraEnv[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// LAN token via env (keeps it off argv, like the pairing secret). The listen token
|
|
61
|
+
// and the pairing secret are the same iOS code, so fall back to the secret's env var —
|
|
62
|
+
// one `read -rs HEALTH_EXPORT_PAIRING_SECRET` capture then covers both roles.
|
|
63
|
+
const useListenTokenEnv = flag('--listen-token-env');
|
|
64
|
+
const listenToken = useListenTokenEnv
|
|
65
|
+
? (process.env.HEALTH_EXPORT_LISTEN_TOKEN || process.env.HEALTH_EXPORT_PAIRING_SECRET || '')
|
|
66
|
+
: '';
|
|
67
|
+
|
|
68
|
+
const die = (msg) => { console.error('error: ' + msg); process.exit(1); };
|
|
69
|
+
|
|
70
|
+
const CLIENTS = {
|
|
71
|
+
'claude-desktop': { topKey: 'mcpServers', shape: 'standard', def: claudeDesktopPath },
|
|
72
|
+
'claude-code': { topKey: 'mcpServers', shape: 'standard', def: () => path.join(process.cwd(), '.mcp.json') },
|
|
73
|
+
'cursor': { topKey: 'mcpServers', shape: 'standard', def: () => path.join(os.homedir(), '.cursor', 'mcp.json') },
|
|
74
|
+
'vscode': { topKey: 'servers', shape: 'vscode', def: () => path.join(process.cwd(), '.vscode', 'mcp.json') },
|
|
75
|
+
'opencode': { topKey: 'mcp', shape: 'opencode', def: opencodePath },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (!client || !CLIENTS[client]) die(`--client must be one of: ${Object.keys(CLIENTS).join(', ')}`);
|
|
79
|
+
if (!serverPath) die('--server-path is required (absolute path to server.mjs)');
|
|
80
|
+
if (!dataDir) die('--data-dir is required (absolute path to the export folder)');
|
|
81
|
+
if (!path.isAbsolute(serverPath)) die('--server-path must be absolute: ' + serverPath);
|
|
82
|
+
if (!path.isAbsolute(dataDir)) die('--data-dir must be absolute: ' + dataDir);
|
|
83
|
+
|
|
84
|
+
function claudeDesktopPath() {
|
|
85
|
+
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
86
|
+
if (process.platform === 'win32') return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData/Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
87
|
+
return path.join(os.homedir(), '.config/Claude/claude_desktop_config.json');
|
|
88
|
+
}
|
|
89
|
+
function opencodePath() {
|
|
90
|
+
const local = path.join(process.cwd(), 'opencode.json');
|
|
91
|
+
if (fs.existsSync(local)) return local;
|
|
92
|
+
return path.join(os.homedir(), '.config/opencode/opencode.json');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const spec = CLIENTS[client];
|
|
96
|
+
const file = expand(opt('--config-path')) || spec.def();
|
|
97
|
+
|
|
98
|
+
// ---- read existing config first (so we can preserve secrets + other keys) -
|
|
99
|
+
let root = {};
|
|
100
|
+
let existed = false;
|
|
101
|
+
if (fs.existsSync(file)) {
|
|
102
|
+
existed = true;
|
|
103
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
104
|
+
if (raw) {
|
|
105
|
+
try { root = JSON.parse(raw); }
|
|
106
|
+
catch { die(`${file} is not plain JSON (it may use comments). Edit it by hand using the recipe in SKILL.md.`); }
|
|
107
|
+
}
|
|
108
|
+
if (typeof root !== 'object' || Array.isArray(root)) die(`${file} is not a JSON object.`);
|
|
109
|
+
}
|
|
110
|
+
const prev = (root[spec.topKey] && root[spec.topKey][name]) || null;
|
|
111
|
+
const prevEnv = prev ? (prev.env || prev.environment) : null;
|
|
112
|
+
const prevSecret = prevEnv ? prevEnv.PAIRING_SECRET : undefined;
|
|
113
|
+
|
|
114
|
+
// ---- decide what the secret should be after this run ----------------------
|
|
115
|
+
// kinds: {kind:'value',value} write a literal · {kind:'input'} VS Code ref · {kind:'none'}
|
|
116
|
+
function resolveSecret() {
|
|
117
|
+
if (clearSecret) return { kind: 'none', why: 'cleared' };
|
|
118
|
+
if (spec.shape === 'vscode') {
|
|
119
|
+
if (useSecretEnv) return { kind: 'input', why: 'prompted by VS Code (${input:…}), not stored' };
|
|
120
|
+
if (prevSecret === VSCODE_INPUT) return { kind: 'input', why: 'preserved existing input ref' };
|
|
121
|
+
return { kind: 'none', why: 'none' };
|
|
122
|
+
}
|
|
123
|
+
if (useSecretEnv && secret) return { kind: 'value', value: secret, why: 'set from $HEALTH_EXPORT_PAIRING_SECRET' };
|
|
124
|
+
// --pairing-secret-env but the env var is empty: don't wipe an existing secret.
|
|
125
|
+
if (useSecretEnv && !secret) {
|
|
126
|
+
if (prevSecret !== undefined && prevSecret !== '') return { kind: 'value', value: prevSecret, why: '$HEALTH_EXPORT_PAIRING_SECRET empty — preserved existing' };
|
|
127
|
+
return { kind: 'none', why: '$HEALTH_EXPORT_PAIRING_SECRET empty — left unset' };
|
|
128
|
+
}
|
|
129
|
+
if (prevSecret !== undefined && prevSecret !== '') return { kind: 'value', value: prevSecret, why: 'preserved existing' };
|
|
130
|
+
return { kind: 'none', why: 'none' };
|
|
131
|
+
}
|
|
132
|
+
const sec = resolveSecret();
|
|
133
|
+
|
|
134
|
+
// ---- build the server entry for this client's shape ----------------------
|
|
135
|
+
// Preserve any prior env keys (e.g. HEALTH_LISTEN*), update HEALTH_DATA_DIR, merge
|
|
136
|
+
// --env passthrough, then resolve the secret.
|
|
137
|
+
function mergedEnv() {
|
|
138
|
+
const env = { ...(prevEnv || {}) };
|
|
139
|
+
env.HEALTH_DATA_DIR = dataDir;
|
|
140
|
+
Object.assign(env, extraEnv);
|
|
141
|
+
if (listenToken) env.HEALTH_LISTEN_TOKEN = listenToken; // empty → prior value preserved
|
|
142
|
+
delete env.PAIRING_SECRET;
|
|
143
|
+
if (sec.kind === 'value') env.PAIRING_SECRET = sec.value;
|
|
144
|
+
else if (sec.kind === 'input') env.PAIRING_SECRET = VSCODE_INPUT;
|
|
145
|
+
// VS Code: never persist a secret/token literal — route it through a prompted ${input:…}.
|
|
146
|
+
// (.vscode/mcp.json is often committed; literals would leak the pairing code at rest.)
|
|
147
|
+
if (spec.shape === 'vscode') {
|
|
148
|
+
for (const k of Object.keys(env)) {
|
|
149
|
+
if (/secret|token/i.test(k) && typeof env[k] === 'string' && env[k] && !env[k].startsWith('${')) {
|
|
150
|
+
env[k] = '${input:' + k.toLowerCase() + '}';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return env;
|
|
155
|
+
}
|
|
156
|
+
function buildEntry() {
|
|
157
|
+
if (spec.shape === 'opencode') return { type: 'local', command: ['node', serverPath], enabled: true, environment: mergedEnv() };
|
|
158
|
+
if (spec.shape === 'vscode') return { type: 'stdio', command: 'node', args: [serverPath], env: mergedEnv() };
|
|
159
|
+
// standard (Claude Desktop / Claude Code / Cursor)
|
|
160
|
+
return { command: 'node', args: [serverPath], env: mergedEnv() };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const wasPresent = !!prev;
|
|
164
|
+
root[spec.topKey] = root[spec.topKey] || {};
|
|
165
|
+
root[spec.topKey][name] = buildEntry();
|
|
166
|
+
|
|
167
|
+
// VS Code: declare a password promptString for every ${input:id} the entry references,
|
|
168
|
+
// and drop our managed inputs no longer referenced. Secrets stay off disk.
|
|
169
|
+
if (spec.shape === 'vscode') {
|
|
170
|
+
const entryEnv = root[spec.topKey][name].env || {};
|
|
171
|
+
const used = new Set();
|
|
172
|
+
for (const v of Object.values(entryEnv)) {
|
|
173
|
+
const m = typeof v === 'string' && v.match(/^\$\{input:([A-Za-z0-9_]+)\}$/);
|
|
174
|
+
if (m) used.add(m[1]);
|
|
175
|
+
}
|
|
176
|
+
const DESC = { health_pairing_secret: 'Health Export pairing code', health_listen_token: 'Health Export LAN token (iOS pairing code)' };
|
|
177
|
+
root.inputs = Array.isArray(root.inputs) ? root.inputs : [];
|
|
178
|
+
for (const id of used) {
|
|
179
|
+
if (!root.inputs.some((i) => i && i.id === id)) {
|
|
180
|
+
root.inputs.push({ type: 'promptString', id, description: DESC[id] || 'Health Export secret', password: true });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
root.inputs = root.inputs.filter((i) => !(i && /^health_(pairing_secret|listen_token)$/.test(i.id) && !used.has(i.id)));
|
|
184
|
+
if (root.inputs.length === 0) delete root.inputs;
|
|
185
|
+
}
|
|
186
|
+
if (spec.shape === 'opencode' && !root.$schema) root.$schema = 'https://opencode.ai/config.json';
|
|
187
|
+
|
|
188
|
+
// ---- redacted summary (never print the secret) ---------------------------
|
|
189
|
+
const redacted = JSON.parse(JSON.stringify(root[spec.topKey][name]));
|
|
190
|
+
const scrub = (o) => { for (const k of Object.keys(o || {})) { if (/secret|token/i.test(k) && o[k] && !String(o[k]).startsWith('${')) o[k] = '********'; } };
|
|
191
|
+
scrub(redacted.env); scrub(redacted.environment);
|
|
192
|
+
|
|
193
|
+
console.log(`client: ${client}`);
|
|
194
|
+
console.log(`config file: ${file}${existed ? '' : ' (will be created)'}`);
|
|
195
|
+
console.log(`action: ${wasPresent ? 'UPDATE existing' : 'ADD'} "${name}" under "${spec.topKey}"`);
|
|
196
|
+
console.log(`secret: ${sec.why}`);
|
|
197
|
+
console.log(`entry: ${JSON.stringify(redacted)}`);
|
|
198
|
+
|
|
199
|
+
if (dryRun) { console.log('\ndry run — nothing written.'); process.exit(0); }
|
|
200
|
+
|
|
201
|
+
// ---- back up + atomic write ----------------------------------------------
|
|
202
|
+
// If a literal secret is being written (non-VS-Code clients), lock files to owner-only 0600.
|
|
203
|
+
const hasLiteralSecret = sec.kind === 'value';
|
|
204
|
+
const mode = hasLiteralSecret ? 0o600 : 0o644;
|
|
205
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
206
|
+
if (existed && !fs.existsSync(file + '.bak')) { // don't clobber an existing backup
|
|
207
|
+
fs.copyFileSync(file, file + '.bak');
|
|
208
|
+
if (hasLiteralSecret) { try { fs.chmodSync(file + '.bak', 0o600); } catch {} }
|
|
209
|
+
console.log(`backup: ${file}.bak`);
|
|
210
|
+
}
|
|
211
|
+
const tmp = file + '.tmp-' + process.pid;
|
|
212
|
+
fs.writeFileSync(tmp, JSON.stringify(root, null, 2) + '\n', { mode });
|
|
213
|
+
fs.renameSync(tmp, file);
|
|
214
|
+
try { fs.chmodSync(file, mode); } catch {}
|
|
215
|
+
console.log('\nwritten. Restart/reload the client, then call get_mcp_status to verify.');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generate one-click MCP install deeplinks for Cursor and VS Code that point at
|
|
3
|
+
// the local Health Export stdio server. No bridge, no Docker — just node + stdio.
|
|
4
|
+
//
|
|
5
|
+
// node gen-deeplinks.mjs [serverPath] [healthDataDir]
|
|
6
|
+
//
|
|
7
|
+
// Defaults assume the installed bundle layout + the iCloud Drive export folder.
|
|
8
|
+
//
|
|
9
|
+
// NOTE the two clients use DIFFERENT config encodings (verified 2026):
|
|
10
|
+
// • Cursor — config = base64(JSON of the inner server object)
|
|
11
|
+
// • VS Code — the whole server JSON is URL-encoded (NOT base64), and it
|
|
12
|
+
// includes "name" + "type" inline.
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
|
|
16
|
+
const serverPath = process.argv[2] || path.join(process.cwd(), 'server.mjs');
|
|
17
|
+
const dataDir = process.argv[3] ||
|
|
18
|
+
path.join(os.homedir(), 'Library/Mobile Documents/iCloud~ai~healthexport~app/Documents');
|
|
19
|
+
|
|
20
|
+
const name = 'health-export';
|
|
21
|
+
const env = { HEALTH_DATA_DIR: dataDir };
|
|
22
|
+
|
|
23
|
+
// Cursor: inner server object, base64-encoded, passed as ?config=
|
|
24
|
+
const cursorCfg = { command: 'node', args: [serverPath], env };
|
|
25
|
+
const cursorB64 = encodeURIComponent(Buffer.from(JSON.stringify(cursorCfg)).toString('base64'));
|
|
26
|
+
|
|
27
|
+
// VS Code: full server object (with name + type), URL-encoded JSON — no base64.
|
|
28
|
+
const vscodeCfg = { name, type: 'stdio', command: 'node', args: [serverPath], env };
|
|
29
|
+
const vscodeEnc = encodeURIComponent(JSON.stringify(vscodeCfg));
|
|
30
|
+
// Web redirect form takes name + config separately (config = URL-encoded JSON of the inner object).
|
|
31
|
+
const vscodeWebCfg = encodeURIComponent(JSON.stringify({ type: 'stdio', command: 'node', args: [serverPath], env }));
|
|
32
|
+
|
|
33
|
+
console.log('Cursor config:', JSON.stringify(cursorCfg));
|
|
34
|
+
console.log('VS Code config:', JSON.stringify(vscodeCfg));
|
|
35
|
+
console.log('\nCursor: cursor://anysphere.cursor-deeplink/mcp/install?name=' + name + '&config=' + cursorB64);
|
|
36
|
+
console.log('VS Code: vscode:mcp/install?' + vscodeEnc);
|
|
37
|
+
console.log('VS Code web: https://insiders.vscode.dev/redirect/mcp/install?name=' + name + '&config=' + vscodeWebCfg);
|