Skip to main content
Glama
sarathi-aiml

clinical-mcp

by sarathi-aiml
README.md
# clinical-mcp

An [MCP](https://modelcontextprotocol.io) server for clinical workflows: search and summarize
synthetic FHIR R4 patient records, pull literature from PubMed, and de-identify free text —
all from Claude (or any MCP client).

Built as a reference-quality MCP server: it implements the full spec surface (**tools,
resources, and prompts** — most public servers stop at tools), ships with a test suite that
exercises the wire protocol, and runs over stdio or authenticated streamable HTTP.

**All patient data is synthetic**, generated by [Synthea](https://github.com/synthetichealth/synthea).
No real PHI exists anywhere in this project.

## Architecture

```
[Claude / MCP client]
        |  stdio  or  streamable-http (+ bearer auth)
        v
[clinical-mcp  (MCPServer)]
   |-- tools ------ search_patients, get_patient_summary, get_observations,
   |                search_pubmed, get_pubmed_abstract, deidentify_text
   |-- resources -- fhir://patients            (roster)
   |                fhir://patients/{id}       (full record, URI template)
   |-- prompts ---- clinical_summary, literature_review
   |
   +-- FhirStore ----------- in-memory index over Synthea FHIR R4 bundles
   +-- PubMedClient -------- NCBI E-utilities, rate-limited (3/s, 10/s w/ key)
   +-- deidentify() -------- HIPAA Safe Harbor regex redaction
```

## Quick start

```bash
pip install clinical-mcp
```

Claude Desktop / Claude Code config (`mcpServers` entry):

```json
{
  "clinical": {
    "command": "clinical-mcp",
    "env": { "CLINICAL_MCP_DATA_DIR": "/path/to/fhir/bundles" }
  }
}
```

From source:

```bash
git clone https://github.com/sarathi-aiml/clinical-mcp
cd clinical-mcp
pip install -e ".[dev]"
clinical-mcp                       # stdio, serves the bundled 10-patient sample
pytest                             # 33 tests, no network needed
```

Then ask Claude things like:

> *"Find female patients over 50 with hypertension, summarize the first one, and pull the
> three most recent PubMed papers relevant to her medication list."*

## Tools

| Tool | What it does |
|---|---|
| `search_patients` | Filter the roster by name, gender, age range, or diagnosed condition |
| `get_patient_summary` | Demographics + conditions, medications, allergies, immunizations |
| `get_observations` | Labs and vitals, filterable by FHIR category, name, and date |
| `search_pubmed` | PubMed search via NCBI E-utilities (supports field tags like `[MeSH]`) |
| `get_pubmed_abstract` | Full abstract for a PMID, section labels preserved |
| `deidentify_text` | Safe Harbor redaction: names, dates, SSN/MRN, phone, email, ZIP, ages > 89 |

Resources expose the same data addressably (`fhir://patients/{id}`), so clients can attach a
full patient record as context without a tool round-trip. Prompts encode the two workflows I
use most — chart summarization and patient-grounded literature review — as reusable templates.

## HTTP transport with auth

```bash
CLINICAL_MCP_API_KEY=$(openssl rand -hex 32) clinical-mcp --transport http --port 8000
```

Every request must carry `Authorization: Bearer <key>`; the server refuses to start
unauthenticated on HTTP. stdio (the default) needs no key — the transport is the trust boundary.

## Data

The repo ships 10 trimmed synthetic patients under `data/sample/`. For a bigger corpus:

```bash
python scripts/fetch_data.py --out data/full            # ~1,100 patients
CLINICAL_MCP_DATA_DIR=data/full clinical-mcp
```

`--trim` strips bundles to the resource types the server actually reads
(Patient, Condition, MedicationRequest, Observation, AllergyIntolerance, Encounter,
Immunization, Procedure, DiagnosticReport, CarePlan) and caps high-volume types.

## De-identification: scope and limits

`deidentify_text` is regex-based Safe Harbor screening: it catches the identifier formats
that appear in structured clinical text and additionally redacts every patient name loaded in
the store. It is **not** a certified de-identification pipeline — free-text names without
honorifics, misspellings, and rare-context identifiers will get through. For real PHI you
want a trained NER pass (e.g. Philter, or an LLM pass with human review) layered on top;
this tool is the deterministic first filter, and its per-category counts make audits cheap.

## What breaks at 500K documents a week

This server is deliberately sized for its job — a reference implementation over a synthetic
corpus. Here is what fails first under production load, and the upgrade path for each:

1. **The in-memory store.** Everything loads into RAM at startup; ~10K patients is
   comfortable, ~100K is not, and startup time grows linearly. First fix: SQLite/DuckDB with
   indexes on name, birth date, and condition codes behind the same `FhirStore` interface.
   Real fix: point the store at an actual FHIR endpoint (HAPI, or a cloud FHIR API) and make
   the tools thin translation layers over FHIR search parameters.
2. **One patient per bundle.** The loader assumes Synthea's layout. Mixed bundles need
   reference resolution (`subject.reference`) instead of file-level grouping.
3. **PubMed rate limits.** 3 req/s (10 with a key) is fine interactively and useless in
   batch. At volume you need a local cache keyed by query hash with a TTL, and batch efetch
   (up to 200 PMIDs per request) instead of per-article calls.
4. **Regex de-identification recall.** At 500K documents/week even 99% recall leaks
   thousands of identifiers. The counts output is designed for exactly this measurement:
   sample, audit, and gate on measured recall — then put a NER model in the pipeline.
5. **Single-process HTTP.** Streamable HTTP under uvicorn on one process serves a team, not
   a fleet. Horizontal scale needs stateless sessions (the store is read-only, so this is
   mostly free) behind a load balancer, and per-client rate limiting at the gateway.

## Development

```bash
pip install -e ".[dev]"
pytest              # protocol-level + unit tests, PubMed mocked
ruff check .
```

Docker:

```bash
docker build -t clinical-mcp .
docker run --rm -i clinical-mcp                                   # stdio
docker run --rm -p 8000:8000 -e CLINICAL_MCP_API_KEY=secret \
  clinical-mcp --transport http --host 0.0.0.0
```

## License

MIT