- define explicit MCP read scope boundaries and upgrade handling - document Nightscout private-origin pinning and reset revocation
9.1 KiB
MCP connector — enable and connect
HealthLog can expose one user's own health record to an external assistant (Claude.ai, ChatGPT, Claude Desktop) through a Model Context Protocol (MCP) server. The assistant reads your figures — vitals, sleep, labs, medications, discovered drivers — and, if you opt in, can log a self-reported reading back. Everything stays on your instance; the assistant only sees what it asks for, one record at a time.
The surface is off by default. It does nothing until you (1) pin a
canonical origin, (2) turn the mcp module on for your account, and
(3) mint a token. A self-hoster who never opts in gets zero new exposure
— the /mcp endpoint answers 404 until all three are in place.
This guide covers turning it on and pointing a real client at it. For what the server can do, see the capabilities reference; for building a connector or skill on top of it, see building skills.
Read this before enabling writes. A remote, write-capable connector reached over OAuth is a powerful combination. Read the trust model at the end of this page first.
Prerequisites
Three things must be true before /mcp will answer:
| Requirement | Why |
|---|---|
APP_URL is set |
The endpoint fails closed without a pinned canonical origin. APP_URL (or NEXT_PUBLIC_APP_URL) is the origin every OAuth document and audience check is anchored to — Host is never trusted. Without it the surface stays invisible (404). |
| The global API is on | The operator's instance-wide API switch must be enabled (it is by default). With it off, every token path — including /mcp — returns 404. |
The mcp module is on |
A per-account opt-in. It is the only module that ships dark; you turn it on in Settings. Until then /mcp answers 404 for that account. |
APP_URL is the same origin you already use for passkeys and invite
links — for example https://health.example. It must be reachable by the
client you are connecting (Claude.ai and ChatGPT connect from their own
servers, so the origin has to be a real public hostname for those; a
LAN-only origin works for Claude Desktop over stdio).
1. Enable the module
In the app: Settings → MCP. The card has three parts:
- Enable — the opt-in switch. Toggle it on; the
/mcpendpoint goes live for your account and the card shows your endpoint URL (<origin>/mcp). - Connections — cloud connectors that completed the OAuth flow appear here; revoke any of them.
- Tokens — manual connector tokens (see below).
2. Mint a connector token
Still in Settings → MCP → Tokens:
- Choose the scope with the write toggle:
- off → a read-only token (
health:read). This is the default and the right choice unless you actually want the assistant to log readings. - on → a read and write token (
health:read health:write). The write scope only ever admits the three confirmed, append-only write tools (below) and is bound to/mcp— it can never write over the regular REST API.
- off → a read-only token (
- Name the token and create it.
- Copy the
hlk_…value now — it is shown exactly once and cannot be retrieved again. Tokens default to a 90-day lifetime.
The token never carries the * wildcard and never a medication-ingest
scope — it is the least-privilege MCP scope and nothing more, and it is
bound to the MCP surface: it is refused on every REST route. The same
list shows last-used time and lets you revoke any token.
MCP reads now require an explicit MCP read capability. A token must carry
health:read (or the legacy/native * wildcard) before the server registers
or invokes any read tool. health:write, fhir:read,
medication:ingest, notification scopes, and every other unrelated scope do
not imply health:read; presenting one of those narrow tokens to /mcp is
rejected before health data is accessed. Likewise, health:read does not
grant a regular REST read or any write. When upgrading, check any manually
provisioned connector token and mint a replacement with health:read instead
of broadening an unrelated credential.
Cloud connectors that use OAuth (next section) mint the same
health:read scope automatically, so for Claude.ai / ChatGPT you do not
need to mint a token by hand — the manual token is for the stdio / power-
user path and for clients you wire up with a static Bearer.
3. Connect a client
The endpoint is <APP_URL>/mcp for every remote client. A non-browser
client (Claude.ai / ChatGPT server-side) sends no Origin, so it
connects directly; a browser Origin that does not match APP_URL is
refused (403) as a DNS-rebinding defence.
Claude.ai (remote + OAuth)
- In Claude.ai, add a custom connector and paste
<APP_URL>/mcp. - Claude.ai discovers the OAuth flow automatically: the
401from/mcppoints at/.well-known/oauth-protected-resource, which names the authorization server, and the client walks/authorize→/token. PKCE (S256) is mandatory; the built-in bridge accepts client-id-metadata-document clients with dynamic registration as a fallback. - Approve the consent screen. The connection then appears under Settings → MCP → Connections, where you can revoke it.
No token to paste — OAuth mints and rotates a short-lived health:read
access token (60 minutes, with a refresh token) behind the scenes.
ChatGPT (remote)
- Add
<APP_URL>/mcpas a connector. The same OAuth discovery applies. - In ChatGPT's default (non-Developer) mode, the only tools it will
call are
searchandfetch— these are HealthLog's two-tool retrieval façade over the same server-authoritative reads. So in default ChatGPT you ask in natural language ("what's my recent LDL?") and it searches your record and fetches the matching item with a citation deep-link back into the app. Developer mode exposes the full tool catalogue.
Claude Desktop (stdio)
Claude Desktop runs a local process and speaks the protocol over stdio —
no OAuth, no public hostname. Mint a token (step 2), then add an entry to
claude_desktop_config.json:
{
"mcpServers": {
"healthlog": {
"command": "pnpm",
"args": ["dlx", "tsx", "src/cli/mcp-stdio.ts"],
"cwd": "/path/to/HealthLog",
"env": {
"HEALTHLOG_MCP_TOKEN": "hlk_…",
"DATABASE_URL": "postgres://…",
"API_TOKEN_HMAC_KEY": "…",
"ENCRYPTION_KEYS": "…"
}
}
}
}
The stdio entrypoint reads the token from HEALTHLOG_MCP_TOKEN
(preferred — keeps the secret out of the process argument list) or as the
first CLI argument. With no token it refuses to start; running it with a
minted token is the opt-in. It needs database access, so it runs from
a checkout with the same DATABASE_URL / API_TOKEN_HMAC_KEY /
ENCRYPTION_KEYS your instance uses. Use tsx via pnpm dlx — the
production standalone image strips tsx, so this is a local-only tool.
stdout is the JSON-RPC channel; diagnostics go to stderr.
Trust model — read this before enabling writes
The combination of a remote endpoint, write capability, and OAuth access is exactly the high-leverage surface to be careful with. HealthLog ships several structural limits, but the operator's choices still matter:
- Only enable the connector for clients you trust. Anything that holds a valid token or a live OAuth connection reads your health record. Revoke connections and tokens you no longer use (Settings → MCP).
- Use a read-only token unless you need writes. The default. A read-only session never even sees the write tools advertised.
- Writes are narrow by construction. The only writes are
append-only, confirmed inserts of a single self-reported reading
(
log_measurement,log_mood,log_blood_pressure). There is no medication, lab, schedule, delete, update, or data-export tool, and ahealth:writeMCP token is refused on every REST write — it can never become a general write credential. - Admin is unreachable over MCP.
requireAdmin()is cookie-only and the MCP wire carries no cookie, so no token — not even a*wildcard — can reach an admin endpoint over this surface.
The structural guarantees are detailed in the capabilities reference.