mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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.
69 lines
2.4 KiB
TypeScript
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();
|
|
});
|
|
});
|