mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
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.
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
/**
|
|
* MCP scope model.
|
|
*
|
|
* The MCP wire reuses the existing `hlk_<hex>` Bearer tokens as its auth
|
|
* substrate (no parallel identity system). Two scope handles frame the
|
|
* least-privilege contract:
|
|
*
|
|
* - `health:read` — read the user's own health data. The default MCP posture.
|
|
* - `health:write` — mutate (log a measurement / intake / mood). Reserved for
|
|
* the write tools, which are NOT registered in this milestone phase.
|
|
*
|
|
* Read tools impose no scope requirement of their own: every underlying read
|
|
* path is already an unscoped authenticated route, so a narrow-scope token works
|
|
* and a wildcard token is never required (REQ-SEC-5). The strong "read-only by
|
|
* default" guarantee (REQ-SEC-1 / ADR-003) is structural — only read tools are
|
|
* registered — not a runtime flag a future token could flip. Write registration
|
|
* (a later phase) gates on `health:write` via `tokenAllowsWrite`.
|
|
*/
|
|
|
|
/** Wildcard scope — grants every capability the token model can express. */
|
|
export const SCOPE_WILDCARD = "*";
|
|
|
|
/** Read the authenticated user's own health data over MCP. */
|
|
export const SCOPE_HEALTH_READ = "health:read";
|
|
|
|
/** Mutate the authenticated user's data over MCP (reserved; no write tools yet). */
|
|
export const SCOPE_HEALTH_WRITE = "health:write";
|
|
|
|
/**
|
|
* Whether a token's scopes admit the read surface. Any valid token qualifies —
|
|
* the underlying reads are unscoped — so this is permissive by design and exists
|
|
* to document the contract and to give a single place to tighten later.
|
|
*/
|
|
export function tokenAllowsRead(): boolean {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether a token's scopes admit the write surface. A token must carry the
|
|
* explicit `health:write` scope (or the wildcard). Used to decide whether write
|
|
* tools are registered for a session.
|
|
*/
|
|
export function tokenAllowsWrite(permissions: readonly string[]): boolean {
|
|
return (
|
|
permissions.includes(SCOPE_WILDCARD) ||
|
|
permissions.includes(SCOPE_HEALTH_WRITE)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The `<user_id>:<token_id>` binding (REQ-SEC-11). Identifies a session by the
|
|
* user AND the specific token presented — never the user alone — so audit,
|
|
* annotation, and (later phases) rate-limit buckets are keyed to the credential
|
|
* actually in use.
|
|
*/
|
|
export function sessionBinding(userId: string, tokenId: string): string {
|
|
return `${userId}:${tokenId}`;
|
|
}
|