health-mcp
v0.1.3
Published
Self-hosted MCP server for personal nutrition, biomarkers, and wearables data
Downloads
408
Maintainers
Readme
health-mcp
Your personal health database. Your agent does the typing.
A local-first server that stores your nutrition, biomarker, and wearable data. The whole thing is exposed as Model Context Protocol tools, so any MCP-aware agent (Hermes, OpenClaw) can read and write it. A web dashboard ships in the same process for when you want to look at the data instead of talk to it.
Everything runs on your machine. One SQLite file. No accounts, no SaaS, no telemetry.
Instead of building another photo-CV calorie app or a freeform-text parser, you let the agent handle the squishy parts ("I had two eggs and toast", "log this lab PDF", "what affects my sleep score?") and let this server handle the durable parts: typed schema, atomic transactions, range queries, a capability-gated tool surface, and a UI that doesn't lie about the data underneath.
What it tracks
Nutrition. Foods (USDA, Open Food Facts, manual entries), meals composed of food / recipe-serving / batch / custom components, hydration, weight, body measurements, macro goals as {min, max} bounds, and daily / weekly rollups.
Recipes and cooked batches. Recipes scale to per-serving macros. A batch is a cooked instance that depletes as you eat against it, with atomic decrements inside log_meal and refunds on delete.
Remembered meals. Label your usual breakfast once, relog it in a single tool call. Holds either pre-resolved components (deterministic) or canonical free text (the agent re-estimates on each call).
Biomarkers and labs. About 60 curated biomarkers seeded with LOINC codes, default units, and reference + optimal ranges. Lab panels insert atomically with all their results. A three-tier range walk decides each result's status: per-result lab snapshot → per-biomarker default → curated optimal. Unit conversion for the common dual-unit pairs (mg/dL ↔ mmol/L, ng/mL ↔ nmol/L, etc.). Trend and "latest per marker" queries.
Wearables. Whoop and Oura over OAuth2 today. Each provider mirror keeps the raw payload (raw_json per row) so a future migration can promote any field to a normalized column without re-syncing. Normalized tables (wearable_sleep, wearable_activity, wearable_readiness, wearable_daily) let you read across vendors without caring which one is connected. Refresh tokens rotate; concurrent 401s can't double-spend because the auth store is mutex-guarded per provider.
Insights. correlate runs Pearson or Spearman over any two metric series, bucketed by day / week / month. Signed lag buckets shift one series in time. Forward-fill carries the last value through gaps so sparse lab data correlates cleanly against daily wearable scores. The tool stays hidden in the agent's catalog until there's enough data to be meaningful.
Quick start
npx
Node ≥ 20, no clone or build:
npx health-mcp # http://127.0.0.1:7777, opens the dashboard
npx health-mcp --stdio # headless MCP server over stdioState lives in ~/.health-mcp/ (one SQLite file). npx health-mcp --help lists every flag; npx health-mcp doctor runs a self-check.
Docker
git clone https://github.com/lukaisailovic/health-mcp.git
cd health-mcp
cp .env.example .env
# Put a strong token in .env (required to bind off-loopback)
openssl rand -hex 32
docker compose up -d
open http://127.0.0.1:7777Data persists in the health-mcp-data named volume. If you'd rather see the files on disk, swap the volume mapping in docker-compose.yml for ./.health-mcp-data:/data.
docker compose down stops the container; data survives across restarts.
Prefer the prebuilt image over building locally? Point docker-compose.yml at the published image and drop its build: block:
image: ghcr.io/lukaisailovic/health-mcp:latest # or pin :0.1.0 / :0.1Each release pushes :X.Y.Z, :X.Y, and :latest; :main tracks the newest commit. All images carry a build-provenance attestation. See Releasing.
From source
Node ≥ 20 and pnpm.
git clone https://github.com/lukaisailovic/health-mcp.git
cd health-mcp
pnpm install
pnpm build
pnpm start # http://127.0.0.1:7777, browser opens automaticallyFor development with hot reload on the dashboard, shared types, and the server, run all three in watch mode:
pnpm dev
# server on :7777, dashboard dev on :5173 (proxies /api/* to :7777)Pass --no-open to keep the browser shut, or --no-dashboard to run as a headless MCP / REST server.
Subcommands
pnpm start -- migrate # apply pending DB migrations and exit
pnpm start -- doctor # self-check (DB pragmas, file modes, token entropy)
pnpm start -- export /tmp/dump.jsonl # JSONL dump; raw_json redacted unless --include-raw
pnpm start -- import-usda dump.json # ingest a USDA FoodData Central bulk JSONConnect an MCP agent
Hermes / OpenClaw (stdio)
Both use the standard MCP config, so the setup is identical: add health-mcp to the agent's mcpServers. No clone or build needed; point it at the published package:
{
"mcpServers": {
"health": {
"command": "npx",
"args": ["-y", "health-mcp", "--stdio"]
}
}
}Where that block lives differs by agent; check its MCP settings for the path.
Then ask your agent:
- "Log eggs and toast for breakfast" →
log_meal - "How's my fasting glucose trending?" →
biomarker_trend - "Does my protein intake correlate with Whoop recovery the next day?" →
correlatewithlag_buckets: 1
Running from a local checkout instead? Use "command": "node" with "args": ["/path/to/health-mcp/apps/server/dist/index.js", "--stdio"] after pnpm build, or "args": ["--import", "tsx", "/path/to/health-mcp/apps/server/src/index.ts", "--stdio"] to skip the build.
MCP Inspector
cd apps/server
pnpm inspectOpens the MCP Inspector against a stdio child for poking at tools by hand.
HTTP / custom client
The Streamable-HTTP transport is mounted at POST /mcp on the same port as the dashboard. Point any HTTP-aware MCP client at http://127.0.0.1:7777/mcp and send Authorization: Bearer <HEALTH_MCP_TOKEN> when a token is set.
The first OAuth link to a wearable provider needs the HTTP server running so the callback route can receive the redirect. Once linked, refresh tokens persist in auth.json and stdio mode can sync from there indefinitely.
The dashboard
Served at / from the same process. Pages today:
- Today — meals for the day, totals against goals, hydration, weight
- Log — add meals, hydration, weight, body measurements
- Foods, Recipes, Batches — the food graph
- Goals — macro bounds, weight target
- Labs — panels, results, trends, per-biomarker About card
- Trends — weekly rollups
- Wearables — provider status, sleep / activity / readiness / daily reads
- Insights —
correlateUI - Settings — token, timezone, theme
Built on TanStack Router + Query, Kumo UI over Tailwind v4, and Recharts. Dark mode follows your OS by default; you can pin it in Settings.
Configuration
Precedence: CLI flag > env var > JSON config file > default.
| Env var | Purpose | Default |
|---|---|---|
| HEALTH_MCP_TOKEN | Bearer token. Required to bind off-loopback. | unset (loopback-only) |
| HEALTH_MCP_PORT | HTTP port | 7777 |
| HEALTH_MCP_HOST | Bind host | 127.0.0.1 |
| HEALTH_MCP_DATA_DIR | Storage dir for data.db and auth.json | ~/.health-mcp |
| HEALTH_MCP_TZ | IANA timezone for day-bucket queries | system TZ |
| HEALTH_MCP_WHOOP_CLIENT_ID / _SECRET | Whoop OAuth app credentials | — |
| HEALTH_MCP_OURA_CLIENT_ID / _SECRET | Oura OAuth app credentials | — |
| HEALTH_MCP_USDA_API_KEY | Enables USDA FoodData Central remote search | local search only |
| HEALTH_MCP_DASHBOARD | Serve the dashboard at / | true |
| HEALTH_MCP_LOG_LEVEL | debug / info / warn / error | info |
Every flag, every env var, the JSON config-file schema, and the security invariants enforced at startup are in docs/CONFIGURATION.md.
Privacy and security
The server is built to fail closed.
- Loopback is the only safe default. To bind anywhere else you must set
HEALTH_MCP_TOKENto a 32+ character high-entropy string (openssl rand -hex 32); the server refuses to start otherwise, with no soft fallback. data.dbandauth.jsonare created0600inside a0700parent. Looser modes refuse to open unless you pass--allow-insecure-db/--allow-insecure-auth.- Wearable OAuth credentials live in
~/.health-mcp/auth.json, separate fromdata.db, sohealth-mcp exportcan ship the database without leaking provider tokens. - Providers like Whoop rotate refresh tokens on every use. The auth store serializes refresh per provider so two concurrent 401s can't both spend the same token and lock you out.
- The OAuth callback uses an HMAC-signed state payload with a 10-minute expiry and a single-use nonce persisted in SQLite. No replay, no forgery.
What this protects against, what it doesn't, and how to safely expose the server beyond localhost (TLS-terminating tunnel; check the doctor output) are covered in docs/SECURITY.md.
How it's put together
One Node process. A Hono app mounts the MCP Streamable-HTTP transport at /mcp, the REST mirror at /api/*, the wearable OAuth callback at /auth/wearable/callback, and the dashboard SPA at /. Storage is SQLite via better-sqlite3 with journal_mode=WAL and foreign_keys=ON. All business logic lives in apps/server/src/services/*.ts; MCP tool handlers and REST routes are thin Zod-validated wrappers that delegate. Wearable data flows through a WearableProvider interface that writes raw per-vendor mirrors and normalized cross-vendor tables in one transaction per sync page.
In HTTP mode a cron job (*/30 * * * * by default, configurable) calls syncWearables() for every linked provider. Stdio mode skips the scheduler; agents call sync_wearables on demand.
The service-by-service breakdown, transport plumbing, and capability-gating mechanics are in docs/ARCHITECTURE.md.
Tools surface
About 60 tools. discover_capabilities returns the live catalog grouped by area, with the current enable flag, so agents can call it first instead of guessing what's available.
ping, discover_capabilities
# food
search_food, search_foods, lookup_barcode, get_food
create_custom_food, bulk_upsert_custom_foods, update_custom_food, delete_custom_food
# meals
log_meal, list_meals, get_meal, update_meal, delete_meal, undo_last_meal,
add_meal_component, update_meal_component, remove_meal_component
# recipes + batches
create_recipe, update_recipe, delete_recipe, list_recipes, get_recipe
create_batch, list_batches, get_batch, archive_batch, delete_batch
# remembered meals (read tools hidden until you save one)
remember_meal, list_remembered_meals, get_remembered_meal,
update_remembered_meal, forget_meal, log_remembered_meal
# simple logs
log_hydration, list_hydration, delete_hydration
log_weight, list_weight, delete_weight
log_measurement, list_measurements, delete_measurement
get_goals, set_goals
# summaries
daily_summary, weekly_summary, range_summary
# biomarkers + labs
search_biomarker, get_biomarker, create_custom_biomarker, update_biomarker, set_optimal_range
log_lab_panel, log_lab_result, list_lab_results, latest_biomarkers, biomarker_trend
list_lab_panels, get_lab_panel, delete_lab_result, delete_lab_panel
# insights (hidden until ≥7 days intake AND (≥1 wearable_daily row OR ≥3 lab_results))
correlate, list_correlate_metrics
# wearables (most hidden until a provider is linked)
wearables_list_providers, wearables_status,
wearable_connect_url, wearable_disconnect, sync_wearables,
wearable_sleep, wearable_activity, wearable_readiness, wearable_daily, wearable_metric_minutes,
set_activity_type_map
# whoop (hidden until linked)
whoop_recovery, whoop_cycles, whoop_sleep_raw, whoop_workouts_raw,
whoop_profile, whoop_body_measurementCapability gating hides tools the agent can't currently use, so the surface stays small. Wearable reads stay invisible until a provider is linked; correlate stays invisible until there's data worth correlating. The full catalog with parameters, return shapes, and gating rules is in docs/MCP.md.
Documentation
| Doc | What it covers |
|---|---|
| Architecture | Process layout, transports, service layer, scheduler |
| Configuration | Flags, env vars, JSON config, subcommands, startup invariants |
| MCP tools | Tool catalog, capability gates, item shapes, agent-client wiring |
| REST API | /api/* mirror used by the dashboard |
| Data model | SQLite schema, indexes, raw-vs-normalized wearable split |
| Biomarkers | Three-tier range model, status walk, unit-conversion table |
| Wearables | Provider interface, OAuth flow, refresh rotation, provider matrix |
| Security | Bearer auth, loopback rule, file modes, OAuth state, threat model |
| Releasing | Version bump → tag → npm (OIDC) + GHCR, all from one Actions run |
Contributing
Issues and PRs welcome.
pnpm install
pnpm typecheck && pnpm lint && pnpm testA few ground rules:
- Business logic lives in
apps/server/src/services/. MCP tools (src/mcp/tools/) and REST routes (src/rest/) are thin wrappers around it. Don't put logic in handlers. - Migrations are checked-in TypeScript modules under
apps/server/src/db/sql/000N-*.ts. Forward-only. - Shared Zod schemas live in
packages/shared. The server and dashboard agree there. - New services want a Vitest case (
*.test.ts) or coverage through the integration suite (apps/server/src/integration.test.ts). pnpm lint:fixbefore pushing — Biome.
Adding a new wearable provider is a self-contained job: create apps/server/src/wearables/providers/<id>/, add a migration with the raw mirror tables, register in the wearable registry. The normalized read tools pick it up automatically. Walkthrough in docs/WEARABLES.md.
Cutting a release is a one-click GitHub Actions run — bump, tag, npm publish, and GHCR tags in one go. See docs/RELEASING.md.
Status
Solo project, actively developed. The data model is stable for nutrition, biomarkers, and Whoop / Oura sync; migrations are forward-only and run on boot. Expect breaking changes to tool parameters and dashboard routes until a 1.0 tag. File an issue if something stops you cold.
This is a personal-use tool, not medical advice or a medical device. The values, ranges, and correlations it surfaces are for self-quantification, not diagnosis.
Tech
Node ≥ 20 · pnpm · TypeScript (ESM, strict) · Hono · @modelcontextprotocol/sdk v1 · better-sqlite3 · Zod · croner · Vitest · Biome.
Dashboard: Vite · React 18 · TanStack Router + Query · Tailwind v4 · Kumo UI · Recharts.
License
MIT.
