mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +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.
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
/**
|
|
* Local stdio entrypoint for the HealthLog MCP server.
|
|
*
|
|
* A power-user runs this in a plain Node process and points an MCP client
|
|
* (Claude Desktop, etc.) at it. Authentication is a pasted, scoped `hlk_<hex>`
|
|
* Bearer token — no OAuth infrastructure (REQ-T1). The token is read from the
|
|
* `HEALTHLOG_MCP_TOKEN` environment variable (preferred — keeps the secret out
|
|
* of the process argument list) or, as a fallback, the first CLI argument.
|
|
*
|
|
* Off by default: with no token the server refuses to start. Running it with a
|
|
* minted token IS the opt-in.
|
|
*
|
|
* Run it with tsx (the standalone production image strips tsx, so this is a
|
|
* local-only tool):
|
|
*
|
|
* HEALTHLOG_MCP_TOKEN=hlk_... \
|
|
* API_TOKEN_HMAC_KEY=... DATABASE_URL=... ENCRYPTION_KEYS=... \
|
|
* pnpm dlx tsx src/cli/mcp-stdio.ts
|
|
*
|
|
* stdout is the JSON-RPC channel — every diagnostic goes to stderr so it can
|
|
* never corrupt the protocol stream.
|
|
*/
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
|
|
import { createMcpServer } from "@/lib/mcp/server";
|
|
import { resolveMcpAuthContext } from "@/lib/mcp/auth";
|
|
|
|
function readToken(): string | null {
|
|
const fromEnv = process.env.HEALTHLOG_MCP_TOKEN?.trim();
|
|
if (fromEnv) return fromEnv;
|
|
const fromArg = process.argv[2]?.trim();
|
|
if (fromArg) return fromArg;
|
|
return null;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const token = readToken();
|
|
if (!token) {
|
|
console.error(
|
|
"HealthLog MCP: no token. Set HEALTHLOG_MCP_TOKEN (a scoped hlk_ Bearer token) or pass it as the first argument.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
let ctx;
|
|
try {
|
|
ctx = await resolveMcpAuthContext(token);
|
|
} catch {
|
|
// Do not echo the token or the underlying reason to stderr.
|
|
console.error("HealthLog MCP: token rejected.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const server = createMcpServer(ctx);
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error(
|
|
`HealthLog MCP server running on stdio (session ${ctx.binding}).`,
|
|
);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("HealthLog MCP: fatal error during startup.", error);
|
|
process.exit(1);
|
|
});
|