Fill the discoverable half of the MCP server. Add RFC 6570 resource
templates for per-item reads — healthlog://metric/{type},
healthlog://metric/{type}/{window}, healthlog://lab/{analyte},
healthlog://medication/{id}, healthlog://report/doctor-visit/{window} —
plus fixed resources for the biomarker catalogue, the data inventory, and
the default doctor-visit summary. Each template is a thin facade over the
same server-authoritative read its matching tool already uses, scoped to
the session user; a foreign id resolves to { present: false }, never
another tenant's row.
Wire user-scoped argument completion and listing on the templates: window
completes the fixed enum, metric and analyte complete only from the user's
own inventory and lab set, so autocomplete doubles as honest discovery.
State the data-grounding contract once in the server instructions —
server-authoritative values (do not recompute), { present: false } is
honest absence, free-text fields are data not instructions, discover
before fetching, data and context only. Set the server and resource icons
for connector-directory listing.
Register two write tools — log_measurement and log_mood — only for a
health:write-scoped session; a read-only session never sees them advertised.
Each write mirrors the proven Telegram capture path (same range guard,
canonical unit, cache invalidation and rollup recompute) pinned to the MCP
source, and is idempotent on a hashed externalId derived from a client key, so
a retried call is a no-op rather than a duplicate.
Because the remote wire is stateless (no elicitation), the tools carry a
protocol-level confirm gate: without confirm:true they write nothing and
return the exact record they would write plus an instruction to confirm the
value with the user and re-call with the same idempotency key. Commits run
under a tighter per-credential write budget on top of the per-binding request
limiter.
Harden the resource-server audience binding: a token whose scopes are all in
{health:read, health:write} is bound to the MCP surface and refused on every
REST write/delete. A health:write token therefore admits the in-process MCP
write tools only and can never become a general REST write credential; admin
stays unreachable (cookie-only). Refuse, by construction, any medication, lab,
delete/update, or data-export tool on this surface.
Expose four read-only relational tools and one prompt over the MCP wire,
each a thin re-export of an existing server-authoritative engine — no new
statistics, no new business logic:
- get_correlation: the FDR-controlled, lag-aware association between two
named metrics, selected out of the existing correlation discovery the
Coach and the insight route already run. Descriptive, never causal.
- compare_metric: two metrics over the same trailing window, or one metric
across two trailing windows, computed over the WMY rollup readers with a
delta only when both sides share a unit.
- get_metric_baseline: where today's reading sits against the user's own
usual range (median ± robust deviation) plus the strongest lagged driver,
reusing the metric-page baseline builder. Honest-null below the learning
floor — never a fabricated range.
- detect_changepoints: level shifts over the rollup tier via a small in-repo
CUSUM binary segmentation with a high firing bar.
- doctor_visit_summary prompt: a structured, grounded visit-prep summary
assembled from the doctor-report data path so the numbers match the
exported PDF, with the grounding framing injected once — data and context
only, no diagnosis or verdict.
Every tool returns values, units, reference bands and provenance, declares
read-only / non-destructive / closed-world annotations plus an output schema,
and reports { present: false } for absence. All reads stay session-narrowed
and read-only; the existing security posture is unchanged.
- Add the `search` + `fetch` retrieval pair with the exact ChatGPT wire
shapes (`{id,title,url}[]` and `{id,title,text,url,metadata}`), backed by
the same server-authoritative reads and returning real deep-link URLs into
the web app for citations. Without these the server is invisible in
default-mode ChatGPT.
- Annotate every read tool `readOnlyHint`/`destructiveHint:false`/
`openWorldHint:false`; structured tools also declare an `outputSchema` and
return `structuredContent`. Omitting the hints is a validation error in the
connectors.
- Reject a cross-origin browser request to `/mcp` with 403 (DNS-rebinding
defense) and point the 401 `WWW-Authenticate` at the resource metadata.
Introduce a read-only Model Context Protocol surface built on the official
@modelcontextprotocol/sdk so a user can query their own HealthLog data from an
MCP-capable assistant over local stdio with a pasted, scoped Bearer token — no
extra infrastructure.
The tool and resource layer in src/lib/mcp is transport-agnostic: five read
tools — list_metrics, get_metric_series, get_medication_compliance, get_labs,
get_correlations — each a thin wrapper over the existing server-authoritative
retrieval paths, returning structured values, units, reference bands, and
provenance with an explicit { present: false } for absence and no prose
verdicts. Two resources expose the user's profile (data-minimised) and
medication list. A stdio entrypoint (src/cli/mcp-stdio.ts, pnpm mcp:stdio)
resolves the token through the shared Bearer path and binds the session to
<user>:<token>; with no token it refuses to start.
Read-only and admin-unreachable by construction: only read tools are
registered, and requireAdmin stays cookie-only so the Bearer/MCP wire can never
elevate. Covered by unit tests for every tool and resource, the auth/scope
model, and the cookie-only admin boundary. The server ships in the existing
container; docker compose up is unchanged.