mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(mcp): expose medication schedule, integration status, and preventive-care reads
Add three grounded read tools, each a thin façade over an existing
server-authoritative engine (no new analytics, userId from the session):
- get_medication_schedule: next-due / overdue per active medication via
the canonical recurrence engine (computeDisplayDue), with as-needed
flagged and no scheduled due time.
- get_integration_status: per-provider sync health (connected, last sync,
reauth-required/failing) from the integration-status ledger, so a user
can see why their data is stale. No secrets or tokens.
- get_preventive_care: the user's already-configured Vorsorge reminders
with their persisted next-due dates and an overdue flag; it surfaces
configured reminders only and invents no screening logic.
Each declares an output schema, read-only annotations, and {present:false}
on absence. Page the search tool across the full assembled result set
(the prior 200-row scan made later items unreachable even through the
cursor) and give each result a per-item deep link where an id exists.
This commit is contained in:
1 parent
1d9cc4b3d2
commit
b9c164c4bb
6 files changed
+611
-8
No files matched your search
@@ -18,9 +18,28 @@ process.env.ENCRYPTION_KEY ??=
|
||||
|
||||
import { logMcpBloodPressure, logMcpMeasurement } from "@/lib/mcp/writes";
|
||||
import { compareMetric } from "@/lib/mcp/rich-reads";
|
||||
import { MCP_TOOLS } from "@/lib/mcp/tools";
|
||||
import type { McpAuthContext } from "@/lib/mcp/auth";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
function readCtx(userId: string): McpAuthContext {
|
||||
return {
|
||||
userId,
|
||||
tokenId: "token-int",
|
||||
scopes: ["health:read"],
|
||||
binding: `${userId}:token-int`,
|
||||
canRead: true,
|
||||
canWrite: false,
|
||||
};
|
||||
}
|
||||
|
||||
function readTool(name: string) {
|
||||
const def = MCP_TOOLS.find((t) => t.name === name);
|
||||
if (!def) throw new Error(`tool ${name} not registered`);
|
||||
return def;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
});
|
||||
@@ -107,6 +126,101 @@ describe("logMcpBloodPressure — real DB", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("measuredAt instant bound — real DB", () => {
|
||||
it("refuses a far-future / ancient measuredAt and persists nothing (measurement + BP)", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await makeUser("instant");
|
||||
|
||||
const future = await logMcpMeasurement({
|
||||
userId: user.id,
|
||||
type: "WEIGHT",
|
||||
value: 80,
|
||||
measuredAt: new Date("9999-01-01T00:00:00Z"),
|
||||
idempotencyKey: "instant-future",
|
||||
});
|
||||
expect(future.status).toBe("out_of_range");
|
||||
|
||||
const ancient = await logMcpMeasurement({
|
||||
userId: user.id,
|
||||
type: "WEIGHT",
|
||||
value: 80,
|
||||
measuredAt: new Date("1000-01-01T00:00:00Z"),
|
||||
idempotencyKey: "instant-ancient",
|
||||
});
|
||||
expect(ancient.status).toBe("out_of_range");
|
||||
|
||||
const bpFuture = await logMcpBloodPressure({
|
||||
userId: user.id,
|
||||
systolic: 120,
|
||||
diastolic: 80,
|
||||
measuredAt: new Date("9999-01-01T00:00:00Z"),
|
||||
idempotencyKey: "instant-bp-future",
|
||||
});
|
||||
expect(bpFuture.status).toBe("out_of_range");
|
||||
|
||||
expect(await prisma.measurement.count({ where: { userId: user.id } })).toBe(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a now-ish measuredAt", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await makeUser("instant-ok");
|
||||
|
||||
const result = await logMcpMeasurement({
|
||||
userId: user.id,
|
||||
type: "WEIGHT",
|
||||
value: 80,
|
||||
measuredAt: new Date(),
|
||||
idempotencyKey: "instant-now",
|
||||
});
|
||||
expect(result.status).toBe("written");
|
||||
expect(await prisma.measurement.count({ where: { userId: user.id } })).toBe(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get_preventive_care — real DB", () => {
|
||||
it("surfaces the user's configured reminders with overdue flags, {present:false} when none", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await makeUser("vorsorge");
|
||||
|
||||
const empty = (await readTool("get_preventive_care").run(
|
||||
readCtx(user.id),
|
||||
{},
|
||||
)) as { present: boolean };
|
||||
expect(empty.present).toBe(false);
|
||||
|
||||
await prisma.measurementReminder.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
label: "Blood pressure check",
|
||||
measurementType: "BLOOD_PRESSURE_SYS",
|
||||
intervalDays: 30,
|
||||
notifyHour: 9,
|
||||
enabled: true,
|
||||
// A past due date so the read flags it overdue.
|
||||
nextDueAt: new Date("2000-01-01T00:00:00Z"),
|
||||
},
|
||||
});
|
||||
|
||||
const result = (await readTool("get_preventive_care").run(
|
||||
readCtx(user.id),
|
||||
{},
|
||||
)) as {
|
||||
present: boolean;
|
||||
checkups: Array<{ label: string; overdue: boolean }>;
|
||||
};
|
||||
expect(result.present).toBe(true);
|
||||
expect(result.checkups).toHaveLength(1);
|
||||
expect(result.checkups[0]).toMatchObject({
|
||||
label: "Blood pressure check",
|
||||
overdue: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareMetric — explicit {from,to} range over real rollups", () => {
|
||||
it("filters each side's buckets to its range and computes a delta", async () => {
|
||||
const user = await makeUser("range");
|
||||
|
||||
Reference in new issue
Block a user