# MedgroundAI

> Open-source, MCP-native RAG for biomedical research. Every answer cites a real, verifiable source, with a grounding check that rejects hallucinated citations.

- **Type:** MCP server
- **Install:** `agentstack add mcp-arnaudturn-pro-medgroundai`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ArnaudTurn-pro](https://agentstack.voostack.com/s/arnaudturn-pro)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ArnaudTurn-pro](https://github.com/ArnaudTurn-pro)
- **Source:** https://github.com/ArnaudTurn-pro/medgroundAI
- **Website:** https://www.medground.ai

## Install

```sh
agentstack add mcp-arnaudturn-pro-medgroundai
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# medground

**Grounded Graph-RAG over the biomedical research literature. No claim ships without a real, retrievable source.**

medground gives Claude a finite, citable corpus of biomedical research and a **deterministic
grounding gate**, so every answer is backed by a paper you can open. Retrieval is hybrid (dense
vectors, BM25, and a MeSH knowledge graph), and each answer must pass a provenance check before it
reaches you: every claim has to cite a `paper_id` the system can actually find. A confident,
unsupported sentence is treated as a bug, not a stylistic choice.

It runs as an [MCP](https://modelcontextprotocol.io) server, so it plugs straight into Claude
Desktop, Claude Code, or any MCP-aware client as a research tool.

> ⚕️ **This is research synthesis and decision *support*, not medical advice.** It tells you what
> the literature *says*, grounded in citations. It does not prescribe, dose, or replace a
> clinician or tumor board.

- **Website:** [medground.ai](https://medground.ai) · **New here? → [`HOWTOUSE.md`](HOWTOUSE.md):** a 5-minute guide to asking Claude grounded questions (no tool names required). One-command setup: **[`./install.sh`](install.sh)** · copy-paste prompts: **[`EXAMPLES.md`](EXAMPLES.md)**.
- **Status:** early development (`0.0.x`) · Python 3.11+ · 24 tests green · [MIT](LICENSE)-licensed
- **Scale** (snapshot, 2026-06-01, grows continuously; run `medground stats` for live counts):
  ~12,400 grounded documents (~26,900 chunks), of which 11,240 are CIViC biomarker→therapy items;
  two retrieval-ready sources (PubMed + CIViC)
- **Scope:** the retrieval pipeline is domain-agnostic, and grows with your corpus. PubMed ingestion covers any biomedical field; the curated CIViC layer is one structured source (oncology biomarkers) among more to come. The repo ships the *pipeline*, not the corpus: you build your own with `medground ingest`, so no third-party article text is redistributed.
- **Architecture deep-dive:** [`docs/decisions/ARCHITECTURE.md`](docs/decisions/ARCHITECTURE.md) and the
  [ADRs](docs/decisions/) (18 decision records)

---

## Why this exists

Large language models hallucinate, and for research questions a fluent, wrong, *uncited* answer
is worse than no answer. medground's thesis is that an LLM is only trustworthy on these questions
when it is **forced to retrieve, forced to cite, and mechanically checked**. The system provides the
retrieval substrate and the enforcement; the LLM does the reasoning on top.

The contract is non-negotiable and **structural, not aspirational**:

1. **Retrieve** evidence from the local corpus (hybrid search returns citation metadata + an
   `allowed_paper_ids` envelope).
2. **Draft** the answer as discrete claims, each citing only `paper_id`s that were actually
   retrieved.
3. **Gate** the draft through `check_grounding(claims, allowed_paper_ids)`, a deterministic, no-LLM
   verifier that flags every uncited, fabricated, or out-of-envelope citation.
4. **Repair** every violation and re-check. Only a `grounded=true` answer is presented.

See [ADR-0007](docs/decisions/0007-groundedness-and-provenance.md) and
[ADR-0013](docs/decisions/0013-grounding-verifier-tool.md).

---

## How it works

```
                        ┌──────────────────────────┐
                        │  Agent (Claude / client) │
                        └────────────┬─────────────┘
                                     │ MCP (stdio) · 17 tools
                        ┌────────────▼─────────────┐
                        │  medground MCP server │
                        └────────────┬─────────────┘
                                     │
              ┌──────────────────────┼──────────────────────┐
       ┌──────▼──────┐        ┌──────▼──────┐        ┌──────▼──────┐
       │ Vector ANN  │        │   BM25 FTS  │        │  Graph hop  │
       │ (DuckDB-VSS)│        │  (DuckDB)   │        │  (KuzuDB)   │
       │ dense /     │        │ exact tokens│        │ MeSH 1-hop  │
       │ semantic    │        │ genes·drugs·│        │ co-occur-   │
       │             │        │ NCT·dosages │        │ rence       │
       └──────┬──────┘        └──────┬──────┘        └──────┬──────┘
              └──────────────────────┼──────────────────────┘
                  each channel degrades independently
                                     │
                          ┌──────────▼──────────┐
                          │ Reciprocal Rank     │   no tuning weights;
                          │ Fusion (RRF)        │   rank-only, library-grade
                          └──────────┬──────────┘
                                     │
                          ┌──────────▼───────────┐
                          │ Cited hits ──────────┼──► check_grounding gate
                          └──────────────────────┘    (deterministic provenance)

   Ingestion (PubMed E-utilities · CIViC GraphQL):
     fetch → chunk → embed → DuckDB (docs + vectors + FTS) + KuzuDB (MeSH graph)
                            ↑ OpenAI text-embedding-3-large (default) / fastembed / Voyage
```

- **Hybrid retrieval, RRF-fused.** Three channels: dense vector (semantic), BM25 (verbatim
  gene/drug/trial/dosage tokens), and a 1-hop MeSH graph expansion, merged by
  [Reciprocal Rank Fusion](docs/decisions/0006-hybrid-retrieval-rrf.md). Each channel degrades
  independently: a missing embedding key or empty vector table just drops that channel; the
  lexical channel needs no API key, so the corpus is searchable the moment papers are ingested.
- **MeSH-first knowledge graph.** Concepts and `MENTIONS` edges come straight from PubMed's MeSH
  terms (free, authoritative); demographic/species "check tags" are stop-listed so real signal
  isn't buried. Co-occurrence is computed on the live edges. ([ADR-0004](docs/decisions/0004-graph-rag-design.md),
  [ADR-0005](docs/decisions/0005-mesh-first-entities.md))
- **Embedded, single-file storage.** Everything (papers, chunks, vectors, FTS index, CIViC
  evidence) lives in **one DuckDB file**; the graph in **one KuzuDB file**. No servers, one backup,
  ACID. ([ADR-0002](docs/decisions/0002-embedded-storage-stack.md),
  [ADR-0011](docs/decisions/0011-collapse-vectors-into-duckdb.md))
- **Single-owner concurrency.** The running process is the sole owner of the data dir: one
  connection per store + one re-entrant lock serialize every touch. Network and embedding work
  stays *outside* the lock by construction. ([ADR-0014](docs/decisions/0014-concurrency-single-owner.md))

---

## Quickstart

> **In a hurry?** One command does everything below, installs medground, writes your `.env` (no
> hand-editing), offers a starter corpus, connects it to Claude, and installs the `/doc` skills:
>
> ```bash
> git clone https://github.com/ArnaudTurn-pro/medgroundAI medground && cd medground
> ./install.sh
> ```
>
> It's safe to re-run anytime to change a setting. The manual steps below are the same thing, broken out.

### 1. Install

Requires [uv](https://docs.astral.sh/uv/) and Python 3.11+.

```bash
git clone https://github.com/ArnaudTurn-pro/medgroundAI medground && cd medground
uv sync                      # creates .venv and installs from uv.lock
```

### 2. Configure

```bash
cp .env.example .env
```

Then edit `.env`. The minimum for the default (best-quality) setup is an OpenAI key and an
absolute data path:

```dotenv
MG_DATA_DIR=/absolute/path/to/medground/data
OPENAI_API_KEY=sk-...
MG_NCBI_EMAIL=you@example.com   # courtesy to NCBI; raises nothing but is requested
```

> **No API key?** Switch to the free, offline local embedder, set
> `MG_EMBED_PROVIDER=fastembed`, `MG_EMBED_MODEL=BAAI/bge-small-en-v1.5`, `MG_EMBED_DIM=384`.
> See [Embeddings](#embeddings).

`.env` is loaded from the project root regardless of the working directory, so the same file feeds
both the CLI and an MCP server that Claude spawns from elsewhere. Real environment variables always
win over the file. `.env` is gitignored, never commit secrets.

### 3. Build a corpus

```bash
# Pull literature from PubMed
uv run medground ingest pubmed -q "BRCA1 olaparib maintenance ovarian" -n 50

# Pull curated biomarker→therapy evidence from CIViC (0 = the whole knowledgebase, ~11k items)
uv run medground ingest civic -n 200

uv run medground stats     # what's in the local stores
```

### 4. Search (sanity check)

```bash
uv run medground search "PARP inhibitor resistance mechanisms" -k 8
```

### 5. Wire it into an MCP client

The server speaks MCP over stdio. For **Claude Code**:

```bash
claude mcp add medground -- uv run --directory /absolute/path/to/medground medground-mcp
```

For **Claude Desktop** (`claude_desktop_config.json`) or any JSON-configured client:

```json
{
  "mcpServers": {
    "medground": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/medground", "medground-mcp"]
    }
  }
}
```

Keys and `MG_DATA_DIR` are picked up from the project-root `.env` automatically; alternatively pass
them in an `"env": { ... }` block. The server self-describes its grounded workflow to the client on
connect.

> **Multiple clients / agents at once?** The stdio command above spawns **one server per client**,
> and the embedded stores are **single-writer**, so two terminals (or Warp panes, or an IDE) would
> collide. To share one corpus across many clients, run **one** shared HTTP server and point every
> client at its URL:
>
> ```bash
> # one process owns the DB → http://127.0.0.1:8765/mcp, leave it running
> uv run --directory /absolute/path/to/medground medground serve
> claude mcp add --transport http medground http://127.0.0.1:8765/mcp   # run in each client
> ```
>
> (Drop the `uv run --directory …` prefix only if the venv is active and you're inside the
> project dir. The `serve` process must stay up; clients only connect to it.)
>
> One writer, many clients, no lock conflict; a startup lock makes a stray second server exit
> cleanly. See [ADR-0018](docs/decisions/0018-http-transport-and-single-owner-lock.md).

---

## Skills: the `/doc` profile (optional, recommended)

medground bundles a set of **[Claude Code](https://docs.claude.com/en/docs/claude-code) skills** in
[`skills/`](skills/) that turn the grounded workflow into slash commands, `/doc-evidence`,
`/doc-case`, `/doc-treatment-map`, `/doc-biomarker-match`, and more. They're optional (plain-English
questions already work), but they make the flows first-class and consistent. Install them with one
command from the repo root:

```bash
./install-skills.sh          # copies them into ~/.claude/skills
```

Restart Claude Code and type `/doc`. Full list in [`skills/README.md`](skills/README.md); copy-paste
prompts in [`EXAMPLES.md`](EXAMPLES.md). (`./install.sh` installs these for you at the end.)

---

## The MCP toolset (17 tools)

The agent surface. Source: [`src/medground/mcp/server.py`](src/medground/mcp/server.py).

### Retrieve & ground: the core loop

| Tool | What it does |
|---|---|
| `search_papers(query, k=8)` | Hybrid retrieval (vector + BM25 + graph, RRF-fused). Returns hits with `chunk_id`, `paper_id`, score, section, title, year, journal, url, text. Cap `k`=50. |
| `summarize_evidence(question, k_per_facet=5, facets?)` | Decomposes a clinical question into facets (efficacy / safety / biomarkers / mechanism / comparators) and retrieves per facet. Returns an evidence pack + `allowed_paper_ids` + a suggested answer structure. **No LLM call**, the agent synthesizes. |
| `evaluate_plan(plan_text, k_per_claim=5)` | Splits a treatment plan into claims and retrieves evidence per claim. Returns a per-claim pack + `allowed_paper_ids` + a verdict schema (supported / contested / unsupported / unknown). **No LLM call.** |
| `check_grounding(claims, allowed_paper_ids?)` | **The enforcement gate.** Deterministically classifies each drafted claim: `grounded` / `uncited` / `phantom_citation` / `off_envelope`. Returns `grounded`, `grounded_ratio`, and the violations to repair. No LLM, no network. |
| `get_paper(paper_id)` | Full paper record (title, abstract, authors, MeSH, journal, year, DOI/PMID/URL). |
| `get_paper_chunks(paper_id)` | All chunks for a paper, in order, with section labels. |

### MeSH knowledge graph

| Tool | What it does |
|---|---|
| `find_concepts(fragment, limit=15)` | Resolve a fuzzy term → canonical concept id (e.g. `mesh:BRCA1_Protein`). Typeahead before the graph ops below. |
| `graph_neighbors(concept_name, hops=1, limit=15)` | Concepts that co-occur with an anchor in the corpus, with co-occurrence weights. |
| `concept_papers(concept_name, limit=20)` | Papers tagged with a MeSH concept, most-recent first. |

### Biomarker → therapy (CIViC)

| Tool | What it does |
|---|---|
| `match_therapies(gene, disease?, variant?, limit=20)` | Curated **predictive** evidence, which therapies are indicated/contraindicated for a biomarker, each with a CIViC **evidence level (A=validated … E=inferential)** and a `civic:eid…` `paper_id` that passes `check_grounding`. |
| `variant_evidence(variant, limit=25)` | All CIViC evidence (predictive / diagnostic / prognostic) for a molecular profile (e.g. `BRAF V600E`, `EGFR T790M`), level-ranked. |

### Corpus management & watches

| Tool | What it does |
|---|---|
| `corpus_stats()` | Counts (papers / chunks / vectors / lexical index / CIViC / graph) plus the live embedding config. Sanity-check before reasoning. |
| `ingest_pubmed(query, max_results=20)` | Pull fresh papers from PubMed and persist (embeds + indexes). Cap 500. |
| `add_watch` · `list_watches` · `remove_watch` · `run_watch` | Standing literature watches that track new research over time (delta pulls; see [Watches](#watches)). |

---

## The grounding contract in practice

The intended agent loop (and why the gate matters):

```
pack   = summarize_evidence("first-line therapy for EGFR-mutant NSCLC")
draft  = 
report = check_grounding(draft, pack["allowed_paper_ids"])
# report["grounded"] == false?  → repair every entry in report["violations"], re-check.
# present only when grounded == true.
```

`check_grounding` is intentionally **narrow and deterministic**. It verifies the *floor*,
that provenance is real and reachable, and classifies each claim:

| Status | Meaning |
|---|---|
| `grounded` | Cites ≥1 `paper_id` that exists in the corpus (and, if an envelope was given, was retrieved for this question). |
| `uncited` | No `paper_id` at all, a contract violation. |
| `phantom_citation` | Cites an id that isn't in the corpus, a fabricated or mistyped reference. |
| `off_envelope` | Cites a real corpus paper that wasn't in the retrieved evidence for this question. |

What it deliberately does **not** check: semantic entailment, whether the cited paper actually
*supports* the claim. That judgment is the LLM's job. The gate guarantees the citation is real;
the LLM is responsible for it being relevant. ([`src/medground/retrieve/grounding.py`](src/medground/retrieve/grounding.py))

---

## CLI reference

The `medground` command (prefix with `uv run` unless the venv is active).

```
ingest pubmed -q "" [-n 50] [--batch 16] [--no-embed]   Search PubMed and ingest top results
ingest civic  [-n 0] [--batch 64] [--no-embed]                 Ingest CIViC evidence (0 = all ~11k)
search "" [-k 8]                                         Hybrid retrieval; prints cited hits
stats                                                           Counts across all local stores
index                                                          Rebuild the BM25 lexical index
reembed [--batch 128]                                          Re-embed all chunks (after a provider switch)
compact                                                        Rebuild the DuckDB file, reclaiming bloat
graph stats | rebuild | find  | neighbors      MeSH graph inspection
watch add | list | remove | enable | disable | run | daemon    Manage research watches
mcp                                                            Start the MCP stdio server (one per client)
serve [--host H] [--

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ArnaudTurn-pro](https://github.com/ArnaudTurn-pro)
- **Source:** [ArnaudTurn-pro/medgroundAI](https://github.com/ArnaudTurn-pro/medgroundAI)
- **License:** MIT
- **Homepage:** https://www.medground.ai

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-arnaudturn-pro-medgroundai
- Seller: https://agentstack.voostack.com/s/arnaudturn-pro
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
