fhir-journey-mcp
# fhir-journey-mcp
A read-only [MCP](https://modelcontextprotocol.io) server that gives an AI agent a patient's context from any FHIR R4 server: who they are, when they were last seen, what is booked next, and which discharged patients never got a follow-up.
Hospitals already run HIS, EMR and scheduling systems that speak (some) FHIR. An agent that calls patients or answers the front desk needs that context before it says a word, and it should not need write access to get it.
## Tools
| Tool | What it answers |
|---|---|
| `fhir_search_patients` | Find a patient by name, phone, identifier (MRN, ABHA number) or birth date. Requires at least one filter. |
| `fhir_get_patient_journey` | Encounters + appointments as one dated timeline, last seen, next appointment, no-show and cancellation counts. |
| `fhir_list_appointments` | Appointments by patient, practitioner, status and date range, e.g. all `noshow` + `cancelled` last week to rebook. |
| `fhir_find_open_slots` | Free slots for a practitioner (chained through `Schedule.actor`) or schedule, earliest first. Does not book. |
| `fhir_find_followups_due` | Finished encounters in a window with **no live follow-up within N days**. Cancelled or no-show follow-ups don't count as booked. Each row says whether the window is already `overdue` or can still be met, and shows any follow-up booked too late. |
`fhir_find_followups_due` output, from the live smoke run below:
```json
[
{ "patient": "Meera Demo", "discharged": "2026-10-09T12:00:00.000Z", "dueBy": "2026-10-16T12:00:00.000Z", "lateBooking": "2026-10-28T10:00:00.000Z" },
{ "patient": "Rohan Demo", "discharged": "2026-10-13T12:00:00.000Z", "dueBy": "2026-10-20T12:00:00.000Z", "lateBooking": null }
]
```
## Run it
```bash
npm install
npm run build
FHIR_BASE_URL=https://your-server/fhir FHIR_TOKEN=... npx fhir-journey-mcp
```
Claude Desktop / any MCP client:
```json
{
"mcpServers": {
"fhir-journey": {
"command": "node",
"args": ["/path/to/fhir-journey-mcp/dist/mcp/server.js"],
"env": { "FHIR_BASE_URL": "https://hapi.fhir.org/baseR4" }
}
}
}
```
With no `FHIR_BASE_URL` it uses the public HAPI test server, which holds synthetic data only.
## Verify it
```bash
npm test # 22 offline tests against a fake FHIR server
npm run seed # loads 4 synthetic patients with known answers, tagged per run
npm run smoke # starts the server over stdio and checks every tool's answer
```
`scripts/seed.ts` documents the expected answers (who is booked, who is due, whose follow-up was cancelled and rebooked late). `npm run smoke` passes 10/10 against the public HAPI server.
## Design choices
- **Read-only.** Booking, cancelling and messaging patients belong behind a human or a separate, audited tool.
- **No full-table reads.** Search tools refuse to run without a filter.
- **Ids are validated** before they reach a URL path; a model can't turn `patientId` into `../Practitioner/1`.
- **Paging links are only followed on the configured origin**, so a bearer token is never sent to a host the server named in a `next` link.
- **Search URLs are redacted from errors**, since they carry names and phone numbers.
- **GET retries on 408/429/5xx with backoff; transaction POSTs are never replayed.**
- Compact summaries, not raw resources: less token spend, fewer fields for a model to misread.
- `meta.tag` filter on every search tool, for scoping to one tenant or one test dataset.
## Not done / honest limits
- Tested against HAPI FHIR only. Real hospital systems use FHIR profiles and partial implementations differently (chained search and `_sort` support vary). Not tested against any production EMR/HIS.
- No ABDM gateway integration, consent artefact handling, or SMART-on-FHIR auth flow; it takes a bearer token you already have.
- "Follow-up" means any live appointment for the patient after discharge, not one matched to the same department or condition.
- `fhir_find_followups_due` makes one appointment query per patient (5 in parallel). Fine for a daily worklist, not for a 10,000-discharge backfill.
MIT licensed.
TDQS
Scored across 5 tools
Each tool targets a distinct resource or workflow: patient search, patient timeline, appointment listing, open slots, and follow-up detection. The only mild overlap is that get_patient_journey already embeds a patient's appointments while list_appointments offers a filtered appointment view, so an agent could occasionally hesitate on which to use for appointment data.
All tools use a uniform fhir_ prefix followed by a consistent verb_noun pattern (search_patients, get_patient_journey, list_appointments, find_open_slots, find_followups_due). The convention is predictable and readable throughout.
Five tools is well-scoped for a focused patient-journey server; each one earns its place covering search, context, appointments, availability, and follow-up detection without redundancy.
The read-only surface covers discovery and identification well, but the implied workflow dead-ends: find_open_slots explicitly cannot hold or book, and find_followups_due returns patients to contact with no tool to actually schedule the follow-up. Booking/rescheduling operations are notable gaps for the scheduling workflow the tools describe.