Content
# Health Collar
`health collar` is a personal Apple Health / Apple Watch data synchronization tool.
Its goal is simple: to synchronize data written to HealthKit on Apple Watch to your own server with a short and stable delay, organize it into regular files, and provide it to AI agents through MCP (with summary status and details view).
The project does not rely on a database. The server-side data layer is implemented using Python standard libraries; the Watch side is responsible for collection and reporting; and the MCP side only reads data that has been written to disk.
> This is not a medical device, nor does it diagnose health data. It is more suitable as a data entry point for personal records, automation, and AI context.
## Why Not Use Health Auto Export as the Main Link
[Health Auto Export](https://www.healthexport.app/) is a complete Health data export tool, and this project is not intended to replace it. The two solve different problems.
I initially used HAE, but this project focuses on "how long does it take for newly generated data to arrive at my server". In actual use, HAE's automation is affected by iOS background scheduling and Health data access conditions, and the delay is not always suitable for near-real-time use; its official documentation also states that automation can only run when the iPhone has access to health data, and device locking, low power mode, and system background resource availability will affect execution timing.
Therefore, `health collar` puts the main link on Apple Watch:
```text
Apple Watch / HealthKit
↓
CollarWatch
↓ HTTPS POST
Your ingest endpoint
↓
health_store.py
↓
Regular JSON / JSONL files
↓
MCP / Other readers
```
HAE can still be used as a compatible input source, migration tool, or backup solution when there is no Watch collection end. The server-side retains support for parsing HAE REST payload.
## Actual Synchronization Performance
Currently, the Watch side schedules the next background refresh about 15 minutes later and provides a complication on the watch face. Apple uses a budget-based scheduling for watchOS background refresh: apps with active complications usually get a higher background refresh budget, but the system only guarantees "not earlier than the preferred date", not precise minute-by-minute wake-up.
In actual use, the performance is:
- **Can achieve relatively stable automatic data upload, with a new report appearing about every 10-20 minutes in normal use, including during sleep.** This is a measured range, not a real-time SLA; watchOS may delay or postpone tasks due to power, system load, or background budget. (It is highly recommended to add Collar as a widget to the watch face, which can make automatic upload more stable)
- **Immediate supplement when recovering to the foreground.** After Collar enters the `.active` state, it will directly execute a collection and upload, so opening the app, clicking on the complication, or clicking "Report Now" can actively urge refresh.
- **Wrist raise as a light prompt.** When the system raises the wrist and recovers to the Collar app, the scene will re-active, triggering a report; if the Collar app is cut off or replaced by another app, it will not work. Clicking to open Collar will also directly trigger upload.
- **No advancement of HealthKit anchor when disconnected.** The anchor for numerical samples is only submitted when the server returns 2xx; after a failure, the next round will still read from the old anchor, so the server-side idempotence can retransmit.
Apple's documentation on watchOS background tasks:
- <https://developer.apple.com/documentation/watchkit/using-background-tasks>
- <https://developer.apple.com/documentation/watchkit/wkapplicationrefreshbackgroundtask>
## How Data is Organized in the Background
The server-side does not simply stack each POST. `server/health_store.py` will normalize the Watch custom payload and HAE REST payload into the same sample structure, and then store and summarize them.
Default files:
```text
data/health/
├── latest.json # Latest value for each indicator
├── samples-YYYYMMDD.jsonl # Original samples, split by local day
├── wrist_baseline.json # Wrist temperature baseline status
└── sleep_history.jsonl # Sleep history, retained for about 30 days
```
Current organization logic includes:
- Idempotent deduplication by indicator and time;
- Saving the latest sample for each indicator;
- Generating "today's total" for cumulative indicators such as step count, distance, active energy, and exercise time;
- Combining recent sleep stages into core / deep / REM / awake and timeline;
- Calculating summaries such as heart rate, HRV, and breathing during sleep;
- Saving recent sleep history for MCP query;
- Sampling long sampling windows returned to agents to avoid too much context at once.
The original sample query window is designed for 48 hours; current file cleanup is executed by "local day file", so the oldest file on disk may slightly exceed a strict 48 hours in boundary cases. MCP's detailed indicator query is still limited to the recent original window.
## Currently Supported Data
### Watch Side Direct Collection
| Indicator | Internal Name | Unit |
|---|---|---|
| Heart Rate | `heart_rate` | count/min |
| HRV (SDNN) | `heart_rate_variability` | ms |
| Resting Heart Rate | `resting_heart_rate` | count/min |
| Respiratory Rate | `respiratory_rate` | count/min |
| Sleep Wrist Temperature | `apple_sleeping_wrist_temperature` | °C |
| Sleep Stage | `sleep_analysis` | Aggregate structure |
| Step Count | `step_count` | count |
| Walking/Running Distance | `walking_running_distance` | mi |
| Flights Climbed | `flights_climbed` | count |
| Active Energy Burned | `active_energy_burned` | kcal |
| Exercise Time | `apple_exercise_time` | min |
Data types can be added according to demand - but whether a specific data type can be obtained depends on the Apple Watch model, region, system version, HealthKit permissions, and whether the device actually generated such samples.
### Server-Side Additional Compatibility
The server-side allow-list also includes:
- `blood_oxygen_saturation`
But **the current `watch/` collection end does not request or query blood oxygen type**. If using HAE or other collection ends to report blood oxygen, the server-side can receive it; if you want CollarWatch to collect it, you need to add the corresponding HealthKit type in `HealthCollector.swift` and consider device/region availability.
You can override the server-side allowed indicator set with `HEALTH_ALLOWED_TYPES`.
## Watch Side Installation
Requires:
- Xcode
- [XcodeGen](https://github.com/yonaskolb/XcodeGen)
- watchOS 10 or later
- An Apple Watch for installation
### 1. Configure Endpoint and Token
Copy local token template:
```bash
cp watch/Sources/Config.local.swift.example watch/Sources/Config.local.swift
```
Fill in the token:
```swift
enum ConfigLocal {
static let token = "YOUR-LONG-RANDOM-TOKEN"
}
```
Then modify `watch/Sources/Config.swift`:
```swift
static let endpoint = URL(string: "https://your-server.example.com/api/health")!
```
It is recommended to use HTTPS only. Health data and ingest token should not be sent over plain HTTP.
### 2. Configure Signing
Modify `watch/project.yml`:
- `DEVELOPMENT_TEAM`: Your Apple Team ID;
- `PRODUCT_BUNDLE_IDENTIFIER`: Replace with your unique Bundle ID;
- Also modify the Bundle ID for the widget target.
### 3. Generate and Install
```bash
cd watch
xcodegen generate
open CollarWatch.xcodeproj
```
In Xcode, select your Apple Watch and run.
After the first launch, complete HealthKit authorization on the Watch. Adding Collar complication to the currently used watch face helps obtain a more stable background refresh budget.
When using a free personal signature installation, the development build usually needs to be re-signed regularly; using an Apple Developer Program's formal development signature is not limited by a 7-day personal signature cycle.
## Server-Side Access
`server/health_store.py` is the **data layer**, not a complete web service. The repository does not decide on FastAPI, Flask, reverse proxy, domain name, or authentication method for you.
You need to complete two things in your HTTPS ingest endpoint:
1. Verify `X-Health-Token`;
2. Pass the JSON body to `normalize_payload()` / `store_samples()`.
Example:
```python
from health_store import ALLOWED_TYPES, normalize_payload, store_samples
# Verify X-Health-Token in your web layer.
# After verification:
samples = normalize_payload(body)
samples = [s for s in samples if s["type"] in ALLOWED_TYPES]
stored, deduped = store_samples(samples)
```
**Note: The current repository does not have an HTTP ingest server and will not automatically read request headers.** `.env.example` `HEALTH_INGEST_TOKEN` is an agreement for your own entry layer. Do not expose an unauthenticated write interface to the public internet.
## MCP
MCP server requires Python 3.10 or later:
```bash
pip install -r requirements.txt
```
Configuration example:
```json
{
"mcpServers": {
"health-collar": {
"command": "python",
"args": ["/absolute/path/to/health-collar/mcp_server/server.py"],
"env": {
"HEALTH_DATA_DIR": "/absolute/path/to/health-collar/data/health",
"HEALTH_TZ_OFFSET_HOURS": "8"
}
}
}
}
```
Provides two tools:
### `health_now`
A compact snapshot for agents, suitable for first looking at "what's the current state". Currently, it will organize:
- Latest heart rate, resting heart rate, HRV, breathing;
- Recent sleep and stages;
- Heart rate / HRV / breathing summary during sleep;
- Wrist temperature;
- Today's step count, distance, active energy, exercise time, flights climbed.
### `health_detail`
For further inquiry:
- Heart rate / HRV / breathing: recent up to 2 hours of point-by-point samples and min / max / avg;
- Sleep: specified date sleep stage timeline, sleep period vitals, and recent 7-day average sleep duration.
Connecting MCP to any agent means that the agent can read these health data when calling tools. Please configure MCP and server permissions according to your own trust boundary.
## Configuration Items
| Environment Variable | Default Value | Description |
|---|---:|---|
| `HEALTH_DATA_DIR` | `./data/health` | Health data file directory |
| `HEALTH_TZ_OFFSET_HOURS` | `0` | Fixed UTC offset, used for "today" and display time |
| `HEALTH_ALLOWED_TYPES` | Built-in list | Comma-separated indicator allow-list |
| `HEALTH_INGEST_TOKEN` | None | Agreement for your own HTTP ingest layer; core data layer will not automatically verify |
`HEALTH_TZ_OFFSET_HOURS` is a fixed offset, not an IANA timezone. When in daylight saving time, you need to adjust it yourself; otherwise, the boundary of "today" and sleep date may be off by one hour.
## Known Limitations
### watchOS Scheduling is Not a Timer
15 minutes is a request to the system "do not wake up earlier than this time", not a strong timing. Active complications can improve the background budget, but watchOS may still delay or skip a background refresh.
### Watch Must Have Available Network
Upload requires the Apple Watch to have a network connection at that time, such as:
- Direct Wi-Fi;
- Cellular network;
- Or obtain network capability through a connection to an iPhone.
Without a network, this round of upload will fail; because the HealthKit anchor will not be submitted on failure, the next successful run will re-read unconfirmed data.
### "Wrist Raise Prompt" Depends on App Re-activation
The code does not use private API to listen to wrist raise actions. It listens to SwiftUI `scenePhase == .active`. So the most stable active refresh method is still to open Collar, click on the watch face complication, or use "Report Now".
### Watch Collection Does Not Equal Complete Apple Health
The current collection end only reads HealthKit types listed in the code. Data generated on iPhone by other devices or apps but not synchronized or exposed to Watch will not appear automatically; segments where the watch is not worn may also produce gaps.
### Cumulative Indicators Are Summed from Original Samples
Currently, "today's total" for step count, distance, active energy, etc. is summed from samples received by the server. For a single Watch data source, this is direct; if multiple HealthKit sources with overlapping intervals are mixed, duplicate counting may still occur. Do not let multiple collection links report the same batch of cumulative indicators for a long time unless you have explicitly handled source priority.
### Original Data is Not Encrypted by Default
The server-side stores regular JSON / JSONL files. Disk encryption, backup strategy, HTTPS, reverse proxy, and access control are responsible for the deployment environment.
## HAE Compatibility Mode
If you do not install the Watch app for now, you can still let Health Auto Export POST to the same ingest endpoint. `normalize_payload()` can recognize its REST metrics format.
Note: HAE and Watch reporting the same cumulative indicators for a long time may cause duplicate counting. A more cautious approach during migration is to select a primary collection source and retain another as a temporary backfill or backup.
HAE's official documentation on automation limitations:
- <https://help.healthyapps.dev/en/health-auto-export/automations/>
- <https://help.healthyapps.dev/en/health-auto-export/troubleshooting/>
## Project Structure
```text
health-collar/
├── server/
│ └── health_store.py # Normalization, storage, summarization, query
├── mcp_server/
│ └── server.py # health_now / health_detail
├── watch/
│ ├── Sources/ # Watch app
│ ├── Widget/ # Watch face complication
│ └── project.yml # XcodeGen configuration
├── .env.example
├── requirements.txt
└── LICENSE
```
## License
MIT. See [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available