# 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](../api/mcp-capabilities.md); for building a connector or skill on top of it, see [building skills](../api/mcp-skills.md). > **Read this before enabling writes.** A remote, write-capable connector > reached over OAuth is a powerful combination. Read the > [trust model](#trust-model-read-this-before-enabling-writes) 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 `/mcp` endpoint goes live for your account and the card shows your endpoint URL (`/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**: 1. 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. 2. Name the token and create it. 3. **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. 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 `/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) 1. In Claude.ai, add a custom connector and paste `/mcp`. 2. Claude.ai discovers the OAuth flow automatically: the `401` from `/mcp` points 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. 3. 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) 1. Add `/mcp` as a connector. The same OAuth discovery applies. 2. In ChatGPT's **default** (non-Developer) mode, the only tools it will call are **`search`** and **`fetch`** — 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`: ```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 a `health:write` MCP 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](../api/mcp-capabilities.md#security-and-write-model).