Files
HealthLog/src/lib/mcp/__tests__/server-resources.test.ts
T
Marc-André Bombeck acc8f1cfc6 feat(mcp): add the browseable resource surface — templates, completion, instructions, icons
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.
2026-06-28 01:07:00 +02:00

69 lines
2.4 KiB
TypeScript

/**
* Guards for the browseable surface wired in `createMcpServer`: the resource
* templates are registered, and the top-level data-grounding contract is stated
* in the server instructions.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/lib/db", () => ({ prisma: {} }));
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createMcpServer, MCP_SERVER_INSTRUCTIONS } from "../server";
import { MCP_RESOURCE_TEMPLATES, MCP_RESOURCES } from "../resources";
import type { McpAuthContext } from "../auth";
const CTX: McpAuthContext = {
userId: "u-1",
tokenId: "t-1",
scopes: ["health:read"],
binding: "u-1:t-1",
canRead: true,
canWrite: false,
};
let registeredResources: string[];
beforeEach(() => {
registeredResources = [];
vi.spyOn(McpServer.prototype, "registerTool").mockReturnValue({} as never);
vi.spyOn(McpServer.prototype, "registerPrompt").mockReturnValue({} as never);
vi.spyOn(McpServer.prototype, "registerResource").mockImplementation(
function (this: unknown, name: string) {
registeredResources.push(name);
return {} as never;
},
);
});
describe("createMcpServer browseable surface", () => {
it("registers every fixed resource and every resource template", () => {
createMcpServer(CTX);
for (const r of MCP_RESOURCES) {
expect(registeredResources).toContain(r.name);
}
for (const t of MCP_RESOURCE_TEMPLATES) {
expect(registeredResources).toContain(t.name);
}
});
});
describe("server instructions", () => {
it("states the data-grounding + safety contract", () => {
expect(MCP_SERVER_INSTRUCTIONS.length).toBeGreaterThan(0);
// The four load-bearing properties of the whole surface.
expect(MCP_SERVER_INSTRUCTIONS).toMatch(/server-side/i);
expect(MCP_SERVER_INSTRUCTIONS).toMatch(/present: false/);
expect(MCP_SERVER_INSTRUCTIONS).toMatch(/never instructions/i);
expect(MCP_SERVER_INSTRUCTIONS).toMatch(/list_metrics|inventory/i);
// No diagnosis / verdict — the read-only clinical-safety floor.
expect(MCP_SERVER_INSTRUCTIONS).toMatch(/no diagnosis|never a diagnosis/i);
});
it("is passed to the underlying server instance", () => {
const server = createMcpServer(CTX);
// The SDK stores the instructions on the underlying Server; expose check
// via the public server handle without depending on private fields shape.
expect(server.server).toBeDefined();
});
});