cd /news/ai-agents/mdr-evidence-gap-agent-finding-unsup… · home topics ai-agents article
[ARTICLE · art-135661] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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.

by read4 min views1 publishedSep 21, 2026

This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content

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

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:

*[_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.

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

── more in #ai-agents 4 stories · sorted by recency
── more on @sanity 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mdr-evidence-gap-age…] indexed:0 read:4min 2026-09-21 ·