The trustworthy FHIR bridge for AI agents — MCP server with reproducible clinical evals, reversible de-identification, and CDS Hooks.
Project description
fhir-mcp
The trustworthy FHIR bridge for AI agents. Plug Claude Code, Cursor, or any MCP-compatible assistant into a real FHIR R4 server — with reproducible clinical evals, reversible keyed de-identification, CDS Hooks decision support, and per-call audit trails out of the box.
Why this exists
LLM coding agents have been plumbed into databases, IDEs, GitHub, Slack, and the desktop. Healthcare is the holdout. When a clinician, founder, or back-office team tries to give Claude or Cursor real EHR context, they hit the same wall: FHIR is a 2,000-page spec, the data is dense with PHI, and nobody trusts a freeform LLM to touch it.
Six open-source FHIR MCP servers already exist. None of them solve the trust problem. They expose FHIR resources as MCP tools, hope the LLM doesn't hallucinate codes, and mask some PHI with regex. There is no reproducible clinical-accuracy benchmark. There is no controlled re-identification path. There is no bridge to CDS Hooks — the production standard for clinical decision support. There is no link between LLM traces and audit logs.
fhir-mcp is the first FHIR MCP server engineered for trust, not just access — so you can drop it into Claude Code on Monday morning, demo it to a clinical-safety officer on Friday, and have a defensible answer to every question they ask.
| Capability | Momentum | xSoVx | WSO2 | AWS HealthLake | Flexpa | langcare | fhir-mcp |
|---|---|---|---|---|---|---|---|
| MCP tools for FHIR resources | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| De-identification on egress | ❌ | ⚠️ irreversible mask | ❌ | ❌ | ❌ | ⚠️ log-scrub only | ✅ |
| Reversible, keyed, audited re-id | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Reproducible clinical eval harness (in-repo, CI-gated) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| CDS Hooks bridge | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| MCP Resources + Prompts | ❌ | ⚠️ prompts only | ❌ | ⚠️ resources only | ❌ | ⚠️ via embedded apps | ✅ all three |
| Audit log linked to LLM trace id | ❌ | ⚠️ generic trace ids | ❌ | ❌ | ❌ | ⚠️ HIPAA audit, not LLM-trace | ✅ |
| Zero-config local mode | ⚠️ | ⚠️ | ❌ | ❌ | ✅ | ⚠️ | ✅ |
Last manually verified 2026-05-23 against each linked repo. If you find a row that's out of date, open an issue — we'll update it the same day.
Eval scoreboard
Generated by CI on every push. Run uv run python evals/report.py locally.
| Eval | Score | Coverage |
|---|---|---|
| Clinical accuracy (structural) | 7/7 ✅ | 5 golden patients · ~40 resources · 7 scenarios |
| Clinical accuracy (judge LLM) | skipped without ANTHROPIC_API_KEY |
optional opt-in |
| De-id leakage | 0 leaks ✅ | all 5 bundles → ~40 resources → ≥30 known PHI tokens, scanned across every tool output |
| Code validation | 31/31 ✅ | LOINC · SNOMED · RxNorm · ICD-10 (offline table) |
| CDS Hooks correctness | 4/4 ✅ | drug-drug, drug-allergy, prefetch-de-id, unknown-hook safety |
| Cross-model judge spread | opt-in | run python -m evals.cross_model with API keys |
Engineering health (v0.2.1): 206 tests passing · 96% line coverage (run uv run pytest tests evals --cov=src/fhir_mcp locally) · ruff + pyright clean · MCP stdio end-to-end verified · SMART OAuth2 (PKCE + backend-services) implemented · 5 golden Synthea bundles · STRIDE threat model.
The scoreboard above is reproducible:
uv run python evals/report.py --out scoreboard.mdre-runs every suite and writes the table. Judge-LLM scoring is opt-in; without an API key it's reported asskipped, never as a fake pass.
SMART on FHIR
fhir-mcp is a confidential SMART app. Two OAuth2 flows are wired up:
| Flow | Use case | CLI helper |
|---|---|---|
| Authorization code + PKCE | Interactive launch against SMART Health IT / Epic on FHIR sandboxes. | fhir-mcp smart-discover <iss> · fhir-mcp smart-authorize-url |
| Backend services (asymmetric JWT bearer) | Server-to-server access against EHRs that support SMART backend services. | fhir-mcp smart-jwt (mints a client assertion) |
The custom scope fhir-mcp/reid gates re-identification:
# Either a local key …
FHIR_MCP_ENABLE_REID=true FHIR_MCP_REID_KEY=… fhir-mcp reid PT_a1b2
# … or pass a SMART token carrying `fhir-mcp/reid` to reidentify() from code.
Every re-id call writes an audit_events row carrying the requesting SMART
subject (or Patient/... context), the Langfuse trace id, and the auth path
used. See docs/THREAT_MODEL.md for the full STRIDE
breakdown.
30-second quickstart
# 1. Run the server (no signups, no .env required — uses in-memory Synthea patients)
uvx fhir-mcp serve
# 2. Wire it into Claude Desktop
cat >> ~/Library/Application\ Support/Claude/claude_desktop_config.json <<'JSON'
{
"mcpServers": {
"fhir-mcp": {
"command": "uvx",
"args": ["fhir-mcp", "serve"]
}
}
}
JSON
# 3. Restart Claude Desktop and ask:
# "Find patients named Smith with diabetes and summarize the first one."
Want real FHIR data? Switch the backend in one env var:
FHIR_MCP_BACKEND=hapi uvx fhir-mcp serve
Architecture
flowchart LR
subgraph CLI["AI Client"]
A[Claude Code / Cursor / Gemini]
end
subgraph SRV["fhir-mcp server"]
B[MCP Server<br/>mcp-python-sdk]
C[Tool Router]
D[FHIR Client<br/>httpx, R4 JSON]
E[De-id Pipeline<br/>Structural walk + regex + Vault]
F[CDS Hooks Bridge]
G[Terminology<br/>LOINC/SNOMED/RxNorm]
H[Audit Logger]
I[Langfuse Tracer]
end
subgraph BE["Data backends"]
J[HAPI Public Test Server]
K[In-memory Synthea Bundles]
end
subgraph OPT["Cloud opt-in"]
L[(Supabase<br/>audit + phi_vault + pgvector)]
M[Langfuse Cloud]
end
subgraph EXT["External standards"]
N[cds-hooks.org sandbox]
O[tx.fhir.org]
end
A <-->|MCP stdio| B
B --> C
C --> D
C --> E
C --> F
C --> G
C --> H
C --> I
D --> J
D --> K
F --> N
G --> O
H --> L
I --> M
Tool catalog (all 8 shipped in v0.0.1)
| Tool | Purpose |
|---|---|
search_patients |
Search by name, MRN, DOB, gender. Returns de-identified summaries with stable pseudonyms. |
get_patient_summary |
Composite clinical summary: conditions, active meds, allergies, recent obs, primary Dx. |
search_observations |
LOINC + date range + value-op queries, plus trend windows ("HbA1c slope, last 12 months"). |
search_conditions |
SNOMED-coded or free-text search, filterable by clinical status. |
get_medications |
Active med list with RxNorm + dosing + drug-drug interaction flags from CDS Hooks. |
validate_code |
Resolve a LOINC / SNOMED / RxNorm / ICD-10 code. Offline-first; optional tx.fhir.org fallback. |
create_clinical_note |
Structured SOAP / discharge / prior-auth note; emits a FHIR DocumentReference. |
run_cds_hook |
Execute a CDS Hooks decision-support call. Offline mock by default; live sandbox opt-in. |
Resources (fhir://patient/{pseudonym}/…):
| Resource | URI | Format |
|---|---|---|
patient_summary |
fhir://patient/{pseudonym}/summary |
text/markdown |
lab_trends |
fhir://patient/{pseudonym}/trends |
application/json |
medications |
fhir://patient/{pseudonym}/medications |
text/markdown |
Prompts: soap_note · discharge_summary · prior_auth_letter — versioned templates that orchestrate the tool calls listed above and produce production-shaped documentation.
Security & compliance posture
- De-identification is on by default for every egress. Structural FHIR path walk + regex recognizers (MRN, phone, email, dates, SSN) detect PHI. Detected PHI is replaced with stable pseudonyms (
PT_a1b2c3d4e5f60718, etc., HMAC-SHA256 over a per-vault salt, 64-bit tail). The original ↔ pseudonym mapping lives in a vault (SQLite locally; Postgres / Supabase in cloud mode). Presidio is available as an optionaldeid-advancedextra. AES-GCM at-rest encryption for the vault is on the v0.3 roadmap —docs/THREAT_MODEL.mdis explicit that the current vault stores plaintext at rest and operators must treat it accordingly. - Re-identification is a privileged, audited path. Disabled unless
FHIR_MCP_ENABLE_REID=trueplus a scoped key (or a SMART-on-FHIR token with the right scope). Every re-id call writes an audit row carrying the requester, the Langfuse trace ID, and the revealed pseudonyms. - Every tool call is audited.
audit_eventsrow per call:(timestamp, actor, tool, args_hash, resource_refs[], trace_id, outcome). Open the audit row → jump straight to the LLM trace. - No PHI escapes to observability. Langfuse spans only ever see pseudonyms; vault values never leave the audit DB.
- HIPAA-aware logging defaults. Argument hashes (not values) by default; opt into verbose audit only in dev.
- SMART on FHIR OAuth2 (v0.2) — both flows supported: PKCE authorization-code for interactive sandbox launches, and backend-services asymmetric JWT bearer for server-to-server. Re-identification can be authorized either by the local
FHIR_MCP_REID_KEYor by a SMART token carrying thefhir-mcp/reidcustom scope; the actor recorded in the audit row is the SMARTPatient/...context when one is present.
See evals/test_deid.py for the leakage test suite.
Eval methodology — the killer feature
Most FHIR MCP projects ship without any clinical-accuracy benchmark. We ship four suites that run in CI on every push:
- Clinical accuracy. Deterministic Synthea bundles → call a tool → judge LLM (
claude-sonnet-4-6, pinned) scores the output against a versioned rubric. Pass = ≥8/10. - De-id leakage. Each bundle's known PHI tokens are re-scanned in every tool's output. Pass = zero leaks across 1,000+ fixtures.
- Code validation. 50 known LOINC / SNOMED / RxNorm codes → expected canonical display.
- CDS Hooks correctness. Known drug-drug interaction pairs must produce the expected
cards[].
Reproducibility is non-negotiable: Synthea seeds, judge prompts, and judge model are all pinned in evals/conftest.py. The full scoreboard is auto-rendered to the top of this README on every CI run.
Roadmap
- v0.1 (shipped 2026-05-23): all 8 tools, 3 resources, 3 prompts, full eval suite, 169 tests at 96% coverage, MCP stdio verified end-to-end.
- v0.2 (shipped 2026-05-23): SMART on FHIR OAuth2 (PKCE + backend-services flows); scope-enforced re-identification; Supabase Postgres mode for audit + vault; cross-model judge harness skeleton; 5 golden Synthea bundles; STRIDE threat model; PyPI publish workflow.
- v0.2.1 (shipped 2026-05-23): closed the "live CDS Hooks leaks PHI to third party" trust gap (prefetch is now de-identified at the client boundary); pseudonym width bumped to 64 bits + collision-detection; vault gained an
asyncio.Lock;@audited(phi_args=...)strips declared PHI fields even underFHIR_MCP_VERBOSE_AUDIT=true; HAPI date-range search fixed;$everythingnow caps at 200 resources. - v0.3 (Q3 2026): AES-GCM vault encryption at rest (#1); FHIR bulk export (
$export); FHIR Subscriptions; R5 dual support; nightly live-FHIR sandbox integration test (#3); cross-model bench wired into CI; MCP2026-07-28stateless transport migration (#2).
Contributing
PRs welcome. See CLAUDE.md for contributor context (mental model, conventions, do/don't). Every PR must keep the eval scoreboard green.
License
Apache-2.0. FHIR® is a registered trademark of HL7 and is used under license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fhir_mcp-0.2.1.tar.gz.
File metadata
- Download URL: fhir_mcp-0.2.1.tar.gz
- Upload date:
- Size: 290.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bb57df3b80393a4d988324e54ab86d8f3b14e1c558e77120c3f1d6211d9338d
|
|
| MD5 |
6c25de3f40b112ebcc74bdb2caa0d87d
|
|
| BLAKE2b-256 |
b61c7a12210b4bd694f56425b8f73a3fbd8c2522579694589a1b6959fc6168b5
|
Provenance
The following attestation bundles were made for fhir_mcp-0.2.1.tar.gz:
Publisher:
publish.yml on DhairyaShah981/fhir-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fhir_mcp-0.2.1.tar.gz -
Subject digest:
1bb57df3b80393a4d988324e54ab86d8f3b14e1c558e77120c3f1d6211d9338d - Sigstore transparency entry: 1620597127
- Sigstore integration time:
-
Permalink:
DhairyaShah981/fhir-mcp@b6f781acdda83e6b87aac07d727b3b9ad5a502e9 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/DhairyaShah981
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b6f781acdda83e6b87aac07d727b3b9ad5a502e9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fhir_mcp-0.2.1-py3-none-any.whl.
File metadata
- Download URL: fhir_mcp-0.2.1-py3-none-any.whl
- Upload date:
- Size: 68.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8340f88bdde676798776949fe4af4d1aa9f5a1dcd656aaa752355f3fdd8c189
|
|
| MD5 |
ba075388588e9d6eef72e897143d654b
|
|
| BLAKE2b-256 |
d14f0bdabd1c8933a7dcf22487b27140e6468bc3eff1fb272aaad21ed493199f
|
Provenance
The following attestation bundles were made for fhir_mcp-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on DhairyaShah981/fhir-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fhir_mcp-0.2.1-py3-none-any.whl -
Subject digest:
b8340f88bdde676798776949fe4af4d1aa9f5a1dcd656aaa752355f3fdd8c189 - Sigstore transparency entry: 1620597215
- Sigstore integration time:
-
Permalink:
DhairyaShah981/fhir-mcp@b6f781acdda83e6b87aac07d727b3b9ad5a502e9 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/DhairyaShah981
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b6f781acdda83e6b87aac07d727b3b9ad5a502e9 -
Trigger Event:
push
-
Statement type: