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
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing