From 93fb0f68d89651d7e8ec2d2a36db1c0a15f9c183 Mon Sep 17 00:00:00 2001 From: Usama Muhammad <46465291+muhdusama@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:17:59 +1100 Subject: [PATCH] fix(mcp): declare labs availability metadata (cherry picked from commit c1a5861fdcbfe374fcbb5362eed3717a5bd9b15d) --- src/lib/mcp/__tests__/tools.test.ts | 46 +++++++++++++++++++++++++++++ src/lib/mcp/tools.ts | 7 ++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/lib/mcp/__tests__/tools.test.ts b/src/lib/mcp/__tests__/tools.test.ts index 5b9d9a8c1..3de4475d4 100644 --- a/src/lib/mcp/__tests__/tools.test.ts +++ b/src/lib/mcp/__tests__/tools.test.ts @@ -510,6 +510,52 @@ describe("get_illness_recovery — condition body site", () => { }); describe("get_labs", () => { + it.each([{}, { analyte: "LDL" }])( + "declares empty-read availability for ordinary and filtered reads: %j", + async (args) => { + const { z } = await import("zod/v4"); + const empty = { + present: false, + reason: "out_of_window", + searchedWindow: "lastYear", + available: { + count: 2, + firstDate: "2020-01-01", + lastDate: "2020-01-02", + reachableWithWindow: null, + }, + }; + vi.mocked(executeCoachTool).mockResolvedValue(empty); + const def = tool("get_labs"); + const result = await def.run(CTX, args); + expect(executeCoachTool).toHaveBeenCalledWith({ + userId: CTX.userId, + name: "get_labs", + rawArguments: JSON.stringify(args), + }); + expect(z.object(def.outputShape!).strict().parse(result)).toEqual(empty); + }, + ); + + it("keeps output validation closed and availability typed", async () => { + const { z } = await import("zod/v4"); + const schema = z.object(tool("get_labs").outputShape!).strict(); + expect(z.toJSONSchema(schema).additionalProperties).toBe(false); + expect(schema.safeParse({ present: false, unexpected: true }).success).toBe( + false, + ); + expect( + schema.safeParse({ present: false, searchedWindow: 7 }).success, + ).toBe(false); + expect( + schema.safeParse({ present: false, available: { count: "2" } }).success, + ).toBe(false); + expect(schema.parse({ present: false, reason: "no_data" })).toEqual({ + present: false, + reason: "no_data", + }); + }); + it("forwards the optional analyte filter and returns readings with units + bands", async () => { vi.mocked(executeCoachTool).mockResolvedValue({ present: true, diff --git a/src/lib/mcp/tools.ts b/src/lib/mcp/tools.ts index 2c9813c30..c26fc1daf 100644 --- a/src/lib/mcp/tools.ts +++ b/src/lib/mcp/tools.ts @@ -1201,11 +1201,8 @@ const listMetricsOutput: z.ZodRawShape = { /** Output schema for `get_labs` — latest-per-biomarker OR a paginated history. */ const getLabsOutput: z.ZodRawShape = { - present: z.boolean(), - reason: z.string().optional(), - // Latest-mode payload (the Coach labs section, carried opaque). - data: z.unknown().optional(), - grounding: z.string().optional(), + // Latest-mode payload, including typed empty-read availability metadata. + ...coachReadOutput, // History-mode payload (per-analyte trajectory + pagination). analyte: z.string().optional(), readings: z