personal-health-mcp

personal-health-mcp

Self-hosted MCP server that aggregates personal health data from Google Health, Oura, and Withings into a single, provider-attributed interface with configurable source of truth preferences.

Category
访问服务器

README

Health Insights MCP Server

CI Python 3.12+ MCP Built with FastMCP Docker Ruff Checked with mypy License: MIT

A self-hosted, single-user MCP server that aggregates your personal health data from Google Health, Oura, and Withings behind one normalized, provider-attributed interface — and is built so new vendors drop in with no changes to the core.

It exposes Model Context Protocol tools (over Streamable HTTP, reachable from Claude Desktop or any MCP client on another machine) and a small web UI for managing provider connections and preferences.

Table of contents

What it does

  • One canonical model. Every provider's data is mapped into one canonical unit per dimension (kg, metres, seconds, kcal, bpm, °C). Comparison and resolution happen in canonical units; conversion to your display units happens only at the edge.
  • You choose the source of truth. For any metric (Step Count, Weight, …) pick authority (prefer one provider; fall back to an ordered list if it has no data) or auto (the most recent value across all providers).
  • Provenance, always. Every response names the provider that supplied each value.
  • Your units. Choose kg vs lb, km vs mi, °C vs °F. When a provider can't serve a unit natively, the server converts.
  • Broad coverage. Steps, distance, calories, weight & body composition, heart rate, HRV, SpO₂, sleep stages, readiness/sleep scores, VO₂max, temperature, blood pressure/glucose, and more — including metrics only one provider supplies.
  • Single user, self-hosted. No multi-tenant support by design. The preferences page is your preferences. If someone else wants it, they host their own.

Architecture at a glance

MCP clients (Claude Desktop, …) ──HTTPS──▶ [ Cloudflare Tunnel  OR  Caddy ]
                                                      │  (TLS terminated here)
                                                      ▼  http (internal docker net)
                                            ┌─────────────────────────┐
                                            │  app (uvicorn)          │
                                            │   /mcp  → FastMCP        │  ← bearer token
                                            │   /     → web UI         │  ← session login
                                            │   /oauth/* → callbacks   │
                                            └───────────┬─────────────┘
                                                        ▼
                                            SQLite (/data) — prefs +
                                            ENCRYPTED tokens & secrets
                          ── outbound ──▶ Google Health · Oura · Withings APIs

See docs/ and the source under src/personal_health_mcp/ for the provider abstraction, resolution engine, and unit layer.


Prerequisites

  • Docker and Docker Compose.
  • A domain you own. Both supported hosting options need a stable HTTPS hostname because OAuth redirect URIs must be registered with each provider and cannot change.
  • A developer account / app with each provider you want to use (Google Health, Oura, Withings).

Quick start

git clone https://github.com/adamconde/personal-health-mcp.git
cd personal-health-mcp
cp .env.example .env        # then fill it in (see Configuration)
# pick ONE hosting overlay (see Hosting options):
docker compose --env-file .env -f deploy/docker-compose.yml -f deploy/compose.caddy.yml up -d

Open https://<your-domain>/, log in with WEB_PASSWORD, go to Providers, enter each provider's client id/secret, click Connect, then set your Metrics and Units preferences.

Always pass --env-file .env and run from the repo root. Compose's ${VAR} interpolation (used by the overlays for CF_TUNNEL_TOKEN / CADDY_DOMAIN) loads its .env from the compose file's directory (deploy/), not your shell's working directory — so without --env-file .env those variables resolve empty even though your repo-root .env is correct.


Configuration

Configuration is via environment variables (.env). Generate the secrets:

# MCP bearer token and session secret
python -c "import secrets; print(secrets.token_urlsafe(48))"
# Token/secret encryption key (Fernet)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Variable Required Description
PUBLIC_BASE_URL External HTTPS origin, no trailing slash (e.g. https://health.example.com). Used to build OAuth redirect URIs.
MCP_AUTH_TOKEN Bearer token MCP clients must send (unless GitHub OAuth is configured below).
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET optional Set both to protect /mcp with GitHub OAuth instead of the bearer token.
GITHUB_ALLOWED_USERS optional Comma-separated GitHub logins allowed when GitHub OAuth is on (set it — blank = any account).
WEB_PASSWORD Single-user web UI password (hashed with argon2id at boot; never stored in plaintext).
SESSION_SECRET Signs session cookies.
TOKEN_ENC_KEY Fernet key encrypting tokens & client secrets at rest. Comma-separate multiple keys (newest first) to rotate.
DATABASE_PATH SQLite path (default /data/health.db).
LOG_LEVEL debug/info/warning/error.
CADDY_DOMAIN Caddy Domain Caddy serves + gets a cert for.
CF_TUNNEL_TOKEN Cloudflare Named-tunnel token.
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET optional Headless fallback — normally set in the UI.
OURA_CLIENT_ID / OURA_CLIENT_SECRET optional Headless fallback.
WITHINGS_CLIENT_ID / WITHINGS_CLIENT_SECRET optional Headless fallback.

Provider API credentials are normally entered in the web UI (/providers) and stored encrypted. The *_CLIENT_* env vars are only an optional bootstrap fallback for headless setups; the UI value always wins.

.env and *.db are git-ignored. Never commit secrets.


Provider setup (OAuth apps)

Create an OAuth app with each provider and register the exact redirect URI. Replace health.example.com with your domain.

Provider Developer console Redirect URI Scopes
Google Health Google Cloud Console → APIs & Services → Credentials → OAuth client (Web) https://health.example.com/oauth/google/callback …/googlehealth.activity_and_fitness.readonly, …health_metrics_and_measurements.readonly, …sleep.readonly
Oura https://cloud.ouraring.com → OAuth applications https://health.example.com/oauth/oura/callback personal daily heartrate workout session spo2
Withings https://developer.withings.com → your app https://health.example.com/oauth/withings/callback user.info,user.metrics,user.activity,user.sleepevents

Notes:

  • The redirect URI must match byte-for-byte what the server uses (PUBLIC_BASE_URL + /oauth/<provider>/callback).
  • Google requires consent screen configuration and returns a refresh token only with access_type=offline + prompt=consent (the server requests both).
  • Withings rotates its refresh token on every refresh; the server persists the new one automatically.

Paste each app's client id and client secret into the Providers page and click Connect to run the OAuth flow.


Install: from GHCR or build locally

Pull a published image (set IMAGE and drop the build: section, or just reference it):

docker pull ghcr.io/adamconde/personal-health-mcp:latest

Or build locally (default in the compose files):

docker build -f deploy/Dockerfile -t personal-health-mcp:local .

Sample docker-compose.yml

A complete single-file example (Caddy variant). Adjust the image/domain:

services:
  app:
    image: ghcr.io/adamconde/personal-health-mcp:latest
    env_file: [.env]
    environment:
      DATABASE_PATH: /data/health.db
    expose: ["8000"] # internal only — never publish to the host
    volumes: ["health-data:/data"]
    restart: unless-stopped

  caddy:
    image: caddy:2
    ports: ["80:80", "443:443"]
    environment:
      CADDY_DOMAIN: ${CADDY_DOMAIN}
    volumes:
      - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on: [app]
    restart: unless-stopped

volumes:
  health-data:
  caddy-data:
  caddy-config:

Hosting options (pick one)

Both publish a stable HTTPS hostname (needed for OAuth) and keep the app itself unpublished on the internal Docker network.

Option A — Cloudflare Tunnel (no open ports)

Free on a Cloudflare account; you only need a domain added to Cloudflare.

  1. Add your domain to Cloudflare (free plan is fine).

  2. Create a named tunnel (Zero Trust dashboard → Networks → Tunnels, or cloudflared tunnel create health). Copy the tunnel token.

  3. Route a hostname to the app: in the tunnel's Public Hostname config, map health.example.comhttp://app:8000.

  4. In .env: set CF_TUNNEL_TOKEN=… and PUBLIC_BASE_URL=https://health.example.com.

  5. Launch:

    docker compose --env-file .env -f deploy/docker-compose.yml -f deploy/compose.cloudflared.yml up -d
    

No inbound ports are opened; Cloudflare terminates TLS at its edge.

Option B — Caddy reverse proxy (Let's Encrypt)

For when you can port-forward.

  1. Point a DNS A/AAAA record for health.example.com at your host.

  2. Ensure ports 80 and 443 are reachable from the internet.

  3. In .env: set CADDY_DOMAIN=health.example.com and PUBLIC_BASE_URL=https://health.example.com.

  4. Launch:

    docker compose --env-file .env -f deploy/docker-compose.yml -f deploy/compose.caddy.yml up -d
    

Caddy obtains and renews the certificate automatically.

Option C — LAN / development

You can run the app directly for development:

pip install -e ".[dev]"
make run    # uvicorn on http://localhost:8000

OAuth still requires a stable HTTPS redirect URL, so for real provider connections use Option A or B (or a tunnel to your dev box).


Connecting MCP clients

Point any MCP client at https://<your-domain>/mcp with the bearer token.

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "personal-health": {
      "type": "streamableHttp",
      "url": "https://health.example.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_MCP_AUTH_TOKEN" },
    },
  },
}

If your client lacks native remote Streamable HTTP, bridge over stdio:

{
  "mcpServers": {
    "personal-health": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://health.example.com/mcp",
        "--header",
        "Authorization: Bearer YOUR_MCP_AUTH_TOKEN",
      ],
    },
  },
}

Authenticating with GitHub OAuth (optional)

Instead of the static bearer token, you can protect /mcp with GitHub OAuth — clients do a browser login, and access is restricted to GitHub logins you allow.

1. Create a GitHub OAuth app (GitHub → Settings → Developer settings → OAuth Apps) with Authorization callback URL https://<your-domain>/auth/callback.

2. In .env set the following. When both id+secret are present, /mcp switches from bearer to GitHub OAuth automatically. Set GITHUB_ALLOWED_USERS — otherwise any GitHub account that authorizes the app could reach your data.

GITHUB_CLIENT_ID=Ov23li...
GITHUB_CLIENT_SECRET=...
GITHUB_ALLOWED_USERS=your-github-login   # comma-separated; restricts access

3. Point the client at the URL with no Authorization header — it discovers OAuth and opens a browser login (or use npx mcp-remote https://health.example.com/mcp with no --header):

{
  "mcpServers": {
    "personal-health": { "type": "streamableHttp", "url": "https://health.example.com/mcp" }
  }
}

In this mode the server runs the MCP app at the origin root so OAuth discovery (/.well-known/...) and the callback (/auth/callback) resolve correctly; the web UI continues to serve at /.

Tools

Tool Purpose
health_list_providers Providers and their connection status.
health_provider_auth_status Whether a provider is connected/usable.
health_list_metrics Metrics available from connected providers.
health_get_metric A metric over a date range, resolved (provider named per point).
health_compare_metric A metric from every provider side-by-side (unresolved).
health_get_sleep Composite sleep summary for a night.
health_get_daily_summary Multi-metric summary for a day.
health_set_metric_authority Set a metric's resolution preference.

Example prompts: “What was my weight last week, in pounds?”, “Compare my step count across providers for yesterday.”, “Make Withings the authority for weight, falling back to Google.”

Web UI pages

  • Dashboard — provider status + effective preferences.
  • Providers — enter credentials (secret is write-only), connect/disconnect.
  • Metrics — per-metric authority/auto + fallback order.
  • Units — mass, distance, height, temperature display units.

Operations

  • Backups: back up the health-data volume (/data/health.db*). It holds your preferences and encrypted tokens.
  • Key rotation: prepend a new Fernet key to TOKEN_ENC_KEY (new,old); reads still work with the old key, new writes use the new one. Once everything is re-encrypted you can drop the old key.
  • Upgrades: docker compose … pull && docker compose … up -d.
  • Troubleshooting:
    • 401 from /mcp → check the Authorization: Bearer header / MCP_AUTH_TOKEN.
    • Redirect-URI mismatch → the registered URI must equal PUBLIC_BASE_URL + /oauth/<provider>/callback exactly.
    • Token refresh failed / reconnect prompt → re-connect the provider on the Providers page (e.g. credentials changed or refresh token revoked).

Security

  • /mcp requires a static bearer token (or GitHub OAuth, if configured); the web UI requires a session login (argon2id-hashed password). Three distinct secrets (MCP bearer, session key, encryption key) — never reuse them.
  • OAuth tokens and client secrets are encrypted at rest (Fernet).
  • CSRF protection on all state-changing forms; strict security headers (HSTS, CSP, nosniff, frame-ancestors 'none').
  • The app is never published to the host; only the TLS proxy/tunnel is exposed.
  • Set cookie_secure=true (default) in any internet-facing deployment.

Adding a new provider

  1. Create src/personal_health_mcp/providers/<vendor>.py subclassing HealthProvider; implement capabilities() and fetch_metric() (map the raw response into canonical DataPoints, reusing existing metric keys), set oauth, and decorate with @register.
  2. Import it in providers/__init__.py.
  3. Run the one-time OAuth connect on the Providers page.

No changes to the aggregator, resolution engine, units, tools, or templates — the new provider appears automatically. (See tests/integration/test_extensibility.py.)


Development

make install      # editable install with dev extras
make lint         # ruff
make type         # mypy
make cov          # pytest with coverage
make check        # all of the above
make run          # run locally on :8000

Project layout

src/personal_health_mcp/
  server.py        # ASGI root: mounts /mcp (bearer) + web UI; uvicorn factory
  app.py           # wiring of shared services (AppContext)
  config.py        # settings / secrets
  models.py        # DataPoint, MetricSeries, ResponseEnvelope, Token, enums
  metrics.py       # canonical metric registry + preference groups
  units.py         # unit table + conversion
  display.py       # display-unit resolution
  resolution.py    # authority / fallback / auto engine
  aggregator.py    # fetch -> resolve -> convert -> envelope
  storage.py       # SQLite + encrypted token/secret store + prefs
  crypto.py        # Fernet/MultiFernet helper
  oauth.py         # AuthFlow + TokenManager (lazy refresh)
  tools.py         # MCP tools
  providers/       # base + google/oura/withings
  web/             # routes, templates, security middleware
deploy/            # Dockerfile, compose (+ cloudflared/caddy overlays), Caddyfile
.github/workflows/ # ci.yml, release.yml (GHCR)
tests/             # unit + integration

Version history

  • 1.0.0 — First stable release.
    • Google Health, Oura, and Withings providers behind a canonical, unit-normalized, provider-attributed model with authority/fallback/auto resolution.
    • MCP tools over Streamable HTTP; auth via a static bearer token or optional GitHub OAuth (browser login + GitHub-login allowlist).
    • Material Design 3 web UI (light/dark) for provider connections and metric/unit preferences, with links to each vendor's credential console.
    • Docker with Cloudflare Tunnel / Caddy hosting overlays; CI + GHCR release.
    • Run with docker compose --env-file .env … (so Compose interpolates CF_TUNNEL_TOKEN / CADDY_DOMAIN from the repo-root .env).

License

MIT.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选