MDR Evidence Gap Agent: finding unsupported claims with Sanity Context A medical writer built the MDR Evidence Gap Agent, a demo that queries a synthetic Class III device dossier stored in Sanity to surface claims lacking linked clinical evidence and the EU MDR requirements they affect. The agent connects to a Sanity Context MCP endpoint, hands its tools to Gemini, and loops until it answers, using GROQ reference traversal rather than keyword matching to find gaps that only exist as empty reference lists. The developer notes the device, claims, and studies are invented and the requirement texts are paraphrased, so the project is a learning demo rather than regulatory advice. This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content https://dev.to/challenges/sanity-2026-09-16 I'm a medical writer, and I built this project to explore how the clinical evidence requirements of the EU MDR can be modeled as structured content. In a clinical evaluation report, every claim needs supporting clinical evidence, and it is easy to lose track of which claims are still unsupported and which MDR requirements they touch. I have not worked on MDR submissions professionally: this is a learning project built from the public text of the regulation, and I built it with the help of an AI assistant. The result is MDR Evidence Gap Agent : an agent that answers audit questions over a small dossier stored in Sanity, for example "Which claims have no supporting evidence, and which MDR requirements do they affect?" Everything is synthetic. The device VascuSeal, a fictional Class III vascular closure device , its claims and its studies are invented. The requirement entries are my own short paraphrases of the regulation, not the official text. This is a demo, not regulatory advice. A keyword search finds pages that mention things. It cannot find what is missing . In this project the gap is a claim with no linked evidence, which only exists as an empty reference list. The agent finds it by following references with GROQ, not by matching words. Live app: https://mdr-evidence-gap-agent.vercel.app https://mdr-evidence-gap-agent.vercel.app Note: the live demo runs on free quotas and on a Sanity trial that ends around October 19. It may become unavailable after that, depending on what the free plan includes. The sample run below shows what the agent returns. Sample run. The agent called initial context , then wrote its own queries. One of them: php type == "claim" { id, title, text, "requirements": requirements - { id, title, annex, text }, "evidence": evidence - { id, title, studyType, summary }, "evidenceCount": count evidence } Its answer: two claims have no linked evidence. These are the two gaps I planted in the dataset. The agent found both without being told where to look. This is the core agent loop. It connects to the Sanity Context MCP endpoint, hands the endpoint's tools to Gemini and loops until the model answers. The deployed app wraps the same loop in a streaming API route with a rate limit and retries when the model is busy, and keeps all keys on the server. js import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { GoogleGenAI } from "@google/genai"; const { CONTEXT URL, CONTEXT TOKEN, GEMINI API KEY } = process.env; const question = process.argv.slice 2 .join " " || "Which claims in the VascuSeal dossier have no supporting evidence, and which MDR requirements do they affect?"; const SYSTEM = "You are the MDR Evidence Gap Agent.", "You audit a SYNTHETIC Class III device dossier VascuSeal stored in Sanity as three linked document types: requirement, claim and evidence.", "Call initial context first, then use groq query to follow references between claims, requirements and evidence.", "Only report what the linked documents show. Never declare a claim compliant. Requirement texts are paraphrased summaries, not the official MDR text.", "Answer concisely in English and list each gap with the affected requirements.", .join " " ; const mcp = new Client { name: "mdr-evidence-gap-agent", version: "1.0.0" } ; await mcp.connect new StreamableHTTPClientTransport new URL CONTEXT URL , { requestInit: { headers: { Authorization: Bearer ${CONTEXT TOKEN} } }, } ; const { tools } = await mcp.listTools ; const functionDeclarations = tools.map t = { const schema = { ... t.inputSchema || { type: "object", properties: {} } }; delete schema.$schema; return { name: t.name, description: t.description || "", parametersJsonSchema: schema }; } ; const ai = new GoogleGenAI { apiKey: GEMINI API KEY } ; const contents = { role: "user", parts: { text: question } } ; for let step = 0; step < 10; step++ { const res = await ai.models.generateContent { model: "gemini-3.1-flash-lite", contents, config: { systemInstruction: SYSTEM, tools: { functionDeclarations } }, } ; const calls = res.functionCalls || ; if calls.length === 0 { console.log res.text ; break; } contents.push res.candidates 0 .content ; const responses = ; for const call of calls { const r = await mcp.callTool { name: call.name, arguments: call.args || {} } ; const out = r.content || .map c = c.text || "" .join "\n" .slice 0, 20000 ; responses.push { functionResponse: { name: call.name, response: { result: out } } } ; } contents.push { role: "user", parts: responses } ; } await mcp.close ; requirement title, annex, text , evidence title, study type, summary and claim title, text, and two arrays of references: requirements and production dataset. Two claims deliberately have an empty type in "requirement", "claim", "evidence" that limits what the agent can read, and custom instructions. The app connects with a read-only Context Viewer token kept on the server. initial context to learn the schema, then runs groq query to resolve references - , count , references , and reports each claim with no evidence together with the requirements it is tied to. The Dossier and Coverage pages read the same content through the same endpoint. 0hmb0qqg mdr-evidence-gap-agent