# MDR Evidence Gap Agent: finding unsupported claims with Sanity Context

> Source: <https://dev.to/ileanamazilu1/mdr-evidence-gap-agent-finding-unsupported-claims-with-sanity-context-nio>
> Published: 2026-09-21 06:45:55+00:00

*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`
