Skip to main content
Glama

health_mcp

Personal health data for Claude: Garmin Connect and Technogym mywellness, served as one MCP endpoint that runs on a Windows machine, starts with it, and is reachable from claude.ai anywhere through a Cloudflare Tunnel.

claude.ai ─HTTPS─▶ Cloudflare Tunnel ─▶ cloudflared (service)
                                           │  http://127.0.0.1:18080/s/<secret>/mcp
                                           ▼
                                     health-caddy  (Caddy, loopback :18080)
                                           │  strips /s/<secret>/, 404s everything else
                                           ▼
                                     health-mcp    (aggregator, loopback :18001)
                                       ├─ garmin_*     ─▶ garmin-mcp     (loopback :18000)
                                       └─ technogym_*  ─▶ technogym-mcp  (loopback :<port>/<secret>/mcp)

Everything is a Windows service running as LOCAL SYSTEM. No inbound ports are opened; cloudflared dials out. The only thing between the internet and your data is the secret path, so the URL is treated like a password.

Repository layout

Path

What

src/health_mcp/

The aggregator. FastMCP 4 ProxyProviders expose upstream tools as garmin_* and technogym_*. GET /healthz checks both upstreams.

src/health_mcp/combined.py

The combined_* tools: one workout record merged from both sources, matched by time overlap (see below).

technogym/

technogym-mcp: MCP server for Technogym mywellness (own pyproject.toml, .env, tests). Merged from the former technogym_mcp repo with history.

deploy/install.ps1

One idempotent installer for all four services.

deploy/install-cloudflared.ps1

Installs the Cloudflare Tunnel connector as a service (token based).

deploy/Caddyfile.template

The secret-path gate.

deploy/enabled_tools.txt

Garmin tool allowlist (read-only tools only).

deploy/list_garmin_tools.py

Lists every tool the installed garmin-mcp offers.

scripts/list_tools.py

Lists the tools health-mcp exposes.

Garmin's MCP server itself is upstream Taxuspt/garmin_mcp, installed as a uv tool; nothing of it is vendored here.

Related MCP server: garmin-mcp

Machine layout

Path

Purpose

C:\health-mcp\secret.txt

URL secret. Treat like a password.

C:\health-mcp\Caddyfile

Rendered gate config (contains the secret).

C:\health-mcp\enabled_tools.txt

Active Garmin allowlist.

C:\health-mcp\logs\*.log

One rotated log per service.

%USERPROFILE%\.garminconnect

Garmin OAuth tokens.

%USERPROFILE%\.local\bin\garmin-mcp.exe

Garmin server binary.

technogym\.env

Technogym credentials, port and secret path (never committed).

.env

Upstream URLs for the aggregator, HEALTH_MCP_STATELESS=1 (never committed).

Services: garmin-mcp, technogym-mcp, health-mcp (depends on both), health-caddy (depends on health-mcp), cloudflared.

Setup from scratch

1. Tools

winget install --id Git.Git
winget install --id astral-sh.uv
winget install --id CaddyServer.Caddy
winget install --id Cloudflare.cloudflared
winget install --id NSSM.NSSM

Reopen PowerShell, then:

uv tool install --python 3.12 git+https://github.com/Taxuspt/garmin_mcp
garmin-mcp-auth            # interactive Garmin login (email, password, MFA). Once.
garmin-mcp-auth --verify

2. Configure

git clone https://github.com/ErikAnkerKilbergSkallevold/health_mcp
cd health_mcp
copy technogym\.env.example technogym\.env   # mywellness email+password, a port (e.g. 38472),
                                             # and TECHNOGYM_MCP_PATH=/<32 hex>/mcp
copy .env.example .env                       # TECHNOGYM_MCP_URL = http://127.0.0.1:<port><that path>

3. Install the services

Elevated PowerShell:

.\deploy\install.ps1

Generates the secret on first run, renders the Caddyfile, syncs both Python environments, (re)creates the four services and prints probe results plus the connector path. Re-run it after changing deploy\enabled_tools.txt or either .env.

Expected:

garmin    http://127.0.0.1:18000/healthz            -> 200 ok
technogym http://127.0.0.1:38472/healthz            -> 200 ok
health    http://127.0.0.1:18001/healthz            -> 200 {"status":"ok","upstreams":{"garmin":"ok","technogym":"ok"}}
gated     http://127.0.0.1:18080/s/<secret>/healthz -> 200
nosecret  http://127.0.0.1:18080/healthz            -> 404

4. Cloudflare Tunnel

Needs a Cloudflare account (Free) and a domain whose nameservers point at Cloudflare. For a .no domain from Domeneshop: buy it with the free DNS option, add the domain in Cloudflare, then set the two Cloudflare nameservers under "Egne navnetjenere". Norid publishes the change within about an hour.

  1. Zero Trust → Networks → Tunnels → Create a tunnel → Cloudflared. Name it after the machine.

  2. Pick Windows and copy the token from the shown service install command.

  3. Published application route: subdomain health, your domain, HTTP, localhost:18080, Path empty.

  4. On the machine, elevated: .\deploy\install-cloudflared.ps1 -Token <token>

One tunnel per machine. More apps later are just more routes on the same tunnel.

In the zone's AI Crawl Control, set Agent to Allow, or claude.ai's requests may be blocked.

5. claude.ai

Settings → Connectors → Yours → Add custom connector. URL:

https://health.<domain>/s/<contents of C:\health-mcp\secret.txt>/mcp

No OAuth fields. Custom connectors are per account: on Team/Enterprise plans only Owners can add them, a Free account gets one, Pro/Max unlimited.

Combined tools (combined_*)

Garmin and Technogym record the same workout from different angles and know nothing of each other's IDs. The combined_* tools pair them by time overlap and merge them:

Tool

What it returns

combined_get_workouts(from_date, to_date?)

Gym workouts from both sides in a range, paired by overlap: matched, technogym_only or garmin_only, with confidence, the clock offset between watch and console, and the IDs for get_workout.

combined_get_workout(date, technogym_session_id?, garmin_activity_id?, include_timeseries=false, sample_every_seconds=10, from_sec=0, to_sec=0)

Runs: Technogym intervals and laps each enriched with Garmin HR, power and cadence for that window; a merged summary where every metric names its source and carries the other side's value as alt; HR zones, training effect/load, Garmin splits, power-duration curve; optional joined per-second series. Strength: Technogym exercises and sets (reps, weight) paired in order with Garmin's timed sets, so each set gets start time, duration and HR; per-exercise HR; free-weight sets Garmin saw but Technogym did not; total volume, rest times, zones, training effect/load.

Merge rule: Technogym is authoritative for what the machine did (speed, grade, belt distance, mill-measured cadence and running power, sets / reps / weight); Garmin for what the body did (heart rate, zones, training effect and load, calories, recovery). Nothing is discarded: where both report a metric the chosen value is marked with source and the other kept under alt. Alignment is by wall clock, so the 10-20 s between pressing start on the watch and on the console is accounted for (clock_offset_sec).

Tools

deploy\enabled_tools.txt limits Garmin to read-only tools (every get_*/search_* plus count_activities, 106 of 150). Write tools such as uploading workouts, editing activities or logging food are excluded because the URL alone guards the account. Trim it further to save context per chat and re-run the installer. Technogym is read-only by design.

uv run python scripts\list_tools.py                                  # what the connector exposes
& "$env:APPDATA\uv\tools\garmin-mcp\Scripts\python.exe" deploy\list_garmin_tools.py "$env:USERPROFILE\.local\bin\garmin-mcp.exe"

Operations

Task

Command

Status

Get-Service garmin-mcp, technogym-mcp, health-mcp, health-caddy, cloudflared

Logs

Get-Content C:\health-mcp\logs\health-mcp.log -Tail 50 -Wait

Restart everything

Restart-Service garmin-mcp -Force (dependents restart with it)

Upgrade garmin-mcp

uv tool upgrade garmin-mcp; Restart-Service garmin-mcp -Force

Update this repo

git pull; .\deploy\install.ps1

Re-auth Garmin

garmin-mcp-auth --force-reauth; Restart-Service garmin-mcp -Force

Rotate the secret

delete C:\health-mcp\secret.txt, re-run installer, update the connector URL

Uninstall

foreach ($s in 'health-caddy','health-mcp','technogym-mcp','garmin-mcp') { nssm remove $s confirm }; cloudflared service uninstall

Troubleshooting

Symptom

Cause / fix

421 Misdirected Request from /mcp

An upstream rejects non-local Host headers. Caddy sets header_up Host {upstream_hostport}; check the rendered Caddyfile.

Phone/claude.ai says the connector failed; log shows POST /mcp 400 right after Created new transport

Client sent calls without a session id. health-mcp must run stateless (HEALTH_MCP_STATELESS=1, the default); check the "listening ... (stateless)" line in health-mcp.log.

/healthz says "technogym": "down"

Get-Service technogym-mcp, then C:\health-mcp\logs\technogym-mcp.log. Usually a wrong port or path in .env.

Cloudflare shows "Invalid nameservers" on a new domain

Registry publishing lag. Wait, then "Check nameservers".

Connector gets 403 from Cloudflare

AI Crawl Control → Agent → Allow; Security → Settings → "Block AI bots" off.

Service stuck in StopPending

The installer kills the nssm host after 15 s; manually: sc queryex <name> then taskkill /F /T /PID <pid>.

Tokens invalid after a while

Garmin tokens expire eventually: garmin-mcp-auth --force-reauth.

Related MCP Connectors

Related MCP Servers