Skip to main content
Glama
celia-zheng

ClinicalTrials-MCP

by celia-zheng
README.md
# ClinicalTrials-MCP

An [MCP](https://modelcontextprotocol.io) server that grounds an LLM in the public
[ClinicalTrials.gov](https://clinicaltrials.gov) registry (API v2, no key required),
built as a production-minded slice of the **BioLit-MCP** portfolio.

Beyond simple API wrappers, it adds the pieces a real clinical-AI platform needs:
**trial matching**, **LLM-powered eligibility extraction**, a **retrieval eval
harness with regression detection**, and **per-tool observability**.

## Tools

| Tool | What it does |
|------|--------------|
| `search_trials(condition, max_results)` | Keyword search over the registry. |
| `get_trial(nct_id)` | Full detail for one study (status, phase, conditions, sponsor, summary). |
| `match_patient_to_trials(condition, age, sex, keywords, max_results)` | Ranks *recruiting* trials for a patient profile: filters by age/sex eligibility, ranks by keyword (e.g. biomarker) overlap. Decision support, not medical advice. |
| `extract_eligibility(nct_id)` | Parses free-text eligibility into structured inclusion/exclusion lists. Uses an LLM (Anthropic) when `ANTHROPIC_API_KEY` is set; falls back to a deterministic heuristic parser otherwise. |
| `server_metrics()` | Live per-tool metrics: calls, error rate, p50/p95 latency. |

## Why these, for a clinical-AI role

This maps directly to the day-to-day of a clinical-AI ML engineer:

- **Trial matching** — the core "connect patients to the right trial" problem.
- **LLM extraction** — turning messy clinical free text into structured data.
- **Eval infrastructure** — measuring output quality continuously and catching
  regressions before they ship (`evals/`).
- **Observability** — metrics/logging/alerting for production tool calls
  (`observability.py`).

## Quickstart

```bash
uv sync                      # or: pip install -e ".[dev,llm]"
uv run mcp dev server.py     # open the MCP Inspector to call tools interactively
```

Add to Claude Desktop (`claude_desktop_config.json`), then restart it:

```json
{
  "mcpServers": {
    "clinicaltrials": {
      "command": "/abs/path/to/.venv/bin/python",
      "args": ["/abs/path/to/clinicaltrials-mcp/server.py"]
    }
  }
}
```

LLM extraction is optional — set `ANTHROPIC_API_KEY` (and optionally
`ANTHROPIC_MODEL`) to enable it; without a key the heuristic parser is used.

## Evals

The harness scores retrieval quality against a 20-case gold set and gates on
regressions vs. a committed baseline.

```bash
python evals/run_evals.py                    # run + compare to baseline
python evals/run_evals.py --update-baseline  # record current scores as baseline
```

Metrics reported:

- **hit@k** — fraction of cases where a relevant study appears in the top *k*.
- **avg precision** — mean fraction of top-*k* results that are on-topic.
- **regression gate** — non-zero exit if `hit@k` drops more than the tolerance
  (default 5%) below baseline; wired into CI.

> Note: the evals hit the live public API, which rate-limits aggressive clients.
> Run locally to populate `evals/baseline.json` and `evals/results.json` (the
> committed copies are placeholders — the scaffolding environment was rate-limited).

## Resilience

The API layer wraps every call to the public registry in exponential backoff
(retrying rate-limit `429`s and transient `5xx`s, failing fast on `4xx`) and a
short-lived TTL cache, so repeated lookups — e.g. the many `api_get` calls inside
`match_patient_to_trials` — don't re-hit the network. See `_get` / `_ttl_cache`
in `server.py`.

## Tests & CI

```bash
pytest -q                              # 14 unit tests, network mocked — fast & deterministic
RUN_INTEGRATION=1 pytest -q \
    tests/test_integration.py          # live contract tests against the real API
```

The unit tests mock the network, which keeps them fast but blind to upstream
*contract drift* — a renamed query field makes every live call `400` while every
mocked test still passes. The opt-in integration tests (`tests/test_integration.py`)
exercise the real request/response contract so that breakage is caught, not shipped.

GitHub Actions (`.github/workflows/ci.yml`) runs the unit tests on every push/PR
and runs the retrieval evals as a separate, non-blocking regression job.

## Layout

```
clinicaltrials-mcp/
├── server.py            # MCP tools + core API layer
├── observability.py     # logging + metrics (@track decorator, snapshot())
├── evals/
│   ├── gold_set.json    # 20 labeled retrieval cases
│   ├── run_evals.py     # scoring + regression detection
│   ├── baseline.json    # committed baseline (populate locally)
│   └── results.json     # last run output (populate locally)
├── tests/
│   ├── test_server.py       # unit tests (mocked network) — logic + retry/cache
│   └── test_integration.py  # opt-in live API contract tests
├── .github/workflows/ci.yml
└── pyproject.toml
```

## Disclaimer

Research/portfolio project. Not a medical device; output is not clinical advice.
Always verify eligibility against the full protocol with a qualified clinician.