HL7 v2.5.1 → FHIR R4 Mapping MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@HL7 v2.5.1 → FHIR R4 Mapping MCP ServerConvert this ORM^O01 message into a FHIR R4 transaction Bundle"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
HL7 v2.5.1 → FHIR R4 Mapping MCP Server
A Model Context Protocol server for the crossing between the two standards that actually carry an order end to end. It answers what does this v2 field become in FHIR R4 at field, datatype and code-value level, and it converts whole ORM, ADT and ORU messages into a FHIR transaction Bundle — with a warning for every value it could not map faithfully, rather than a plausible guess.
Protocol | MCP Streamable HTTP |
Source | HL7 v2.5.1 |
Target | FHIR R4 (4.0.1) |
Runtime | Node.js 18+ |
License | MIT |
What this is not: a certified conversion engine. The output is a syntactically valid R4 transaction Bundle built from one message in isolation — it has no patient index, no identifier registry and no memory of earlier messages. Assigning authorities that carry no universal ID come out as
urn:id:placeholders, and every one of them is reported inwarnings. Resolve those against your own registry before anything reaches a shared store.
Companion server: hl7-v2-mcp-server answers what does this segment mean against the v2.5.1 standard. This one answers what does it become in R4. The default port here is 3001, chosen so both can run at once.
Quick start
git clone https://github.com/coffeemilktea/hl7-v2-fhir-mcp-server.git
cd hl7-v2-fhir-mcp-server
npm install
npm start
# HL7 v2.5.1 → FHIR MCP server listening on http://localhost:3001/mcpAdd to Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (Mac) or
%APPDATA%\Claude\claude_desktop_config.json (Windows). The two servers complement each other — run
both:
{
"mcpServers": {
"hl7-reference": {
"type": "http",
"url": "http://localhost:3000/mcp"
},
"hl7-to-fhir": {
"type": "http",
"url": "http://localhost:3001/mcp"
}
}
}Restart Claude Desktop. The nine tools appear in the tool panel.
Add to Claude Code
In your project's .claude/settings.json:
{
"mcpServers": {
"hl7-to-fhir": {
"type": "http",
"url": "http://localhost:3001/mcp"
}
}
}Environment variables
Variable | Default | Description |
|
| HTTP port the server listens on |
A health check is served at /health.
Related MCP server: HL7-Bridge MCP
Tools
Nine tools: six that explain the mapping, three that perform it.
Tool | Parameters | What it does |
| — | Every message type the server converts, the FHIR resources each produces, and how many documented pitfalls it carries. |
|
| The full segment-to-resource plan for one message type, including the pitfalls that make that message different from its neighbours. |
|
| Field-by-field FHIR targets for one segment — PID, PV1, ORC, OBR, OBX, MRG, SPM, NTE, AL1, DG1, NK1, PD1, EVN, MSH, ZDS. |
|
| One field, e.g. OBR-25 or PID-5, with the FHIR element it feeds and the caveat attached to it. |
|
| Component-level mapping for a v2 datatype — XPN, XAD, CX, XTN, CE, TS, XCN, HD, PL, EI, CQ, SN and the primitives. |
|
| Translate a v2 table value to its FHIR code, or list a whole table. Tables 0001, 0002, 0004, 0007, 0038, 0078, 0085, 0119, 0123, 0125, 0136, 0203, 0396. |
|
| Reverse lookup. Name a FHIR resource and get every v2 field that lands in it — the question you actually have when a FHIR element is empty and you need to know which segment failed you. |
|
| HL7 TS to FHIR date/dateTime, preserving the source precision instead of padding it, and flagging a time sent with no timezone offset. |
|
| A whole ORM, ADT or ORU message to a FHIR R4 transaction Bundle, plus the warnings and notes the conversion generated. Ask for the bundle, the summary, or both. |
What each message becomes
Conversion is a transaction Bundle. Resources with a usable business identifier get a conditional
PUT so a replayed message updates rather than duplicates; resources without one fall back to
POST, and the missing identifier is reported as a warning.
Message | Resources emitted | The part that goes wrong |
| ServiceRequest, Patient, Encounter, Practitioner, Location, Observation, ImagingStudy | One ORM can carry several ORC/OBR pairs — that is N ServiceRequests, not one with repeated codes. And OBX inside an ORM is an order-entry answer, not a result: it is emitted with |
| Patient, Encounter, Location chain, Practitioner, Condition, AllergyIntolerance |
|
| Patient, Encounter, Condition, AllergyIntolerance | A08 is a snapshot, but most pipelines treat it as a patch, so a field the sender cleared never clears downstream. A changed MRN in an A08 is an unannounced merge, not an update. |
| Patient only | Person-level, so there is no Encounter — even when the sender includes a PV1. Emitting one anyway, because the A08 converter always does, is the classic A31 bug; the server warns instead. |
| Patient (survivor) + Patient (retired, | PID survives, MRG-1 is retired with |
| DiagnosticReport, Observation, Patient, Encounter, Specimen, Practitioner, ImagingStudy | A radiology narrative arrives as dozens of repeating TX rows. They are one report: the server joins consecutive narrative OBX rows sharing a code into a single Observation, in line order, and repeats the text in |
Where the two status fields go
The most common conversion defect in an ORU is copying one status onto the other. They come from different fields, use different tables, and legitimately differ — a final report can contain a corrected line.
Source | Table | FHIR element |
OBR-25 | 0123 |
|
OBX-11 | 0085 |
|
ORC-5 | 0038 |
|
ORC-1 | 0119 | (create / update / cancel — a verb, not a state) |
Rules the converter holds to
Every one of these is a place where a conversion can look successful and be wrong. The server's position on each is fixed, and departures are reported rather than absorbed.
Rule | Why |
No invented systems | An assigning authority with no universal ID becomes |
Precision is preserved |
|
Timezones are never assumed | A TS with a time and no offset is converted without one and flagged. Applying the receiver's locale is what shifts admissions across midnight between sites. |
Out-of-table codes fail loudly | A PID-8 that is not in table 0001 leaves |
Idempotent output |
|
Escapes are reversed |
|
OBX-2 decides value[x] | NM becomes |
ED and RP are not values | Encapsulated data and reference pointers belong in |
Example prompts
Once connected, you can ask Claude questions like:
# Field-level mapping
"Where does OBR-25 go in FHIR?"
→ get_field_mapping(segment="OBR", field=25)
# Reverse lookup — the question you have at 2am
"DiagnosticReport.effectiveDateTime is empty. Which v2 field feeds it?"
→ which_v2_fields_feed(resourceType="DiagnosticReport")
# Code translation
"What does patient class 'B' become in Encounter.class?"
→ lookup_concept_map(table="0004", code="B")
# Whole-message conversion
"Convert this ORU to FHIR and tell me what didn't map cleanly."
→ convert_message(message="MSH|^~\&|RIS|HOSP|...", include="both")
# Just the diagnosis, not the payload
"Will this ADT^A08 convert cleanly?"
→ convert_message(message="...", include="summary")
# Merge safety
"Which patient does this A40 retire?"
→ get_message_mapping(messageType="ADT^A40")
convert_message(...) survivor keeps PID-3, MRG-1 goes active=false with replaced-by
# Datatype detail
"How do I get an email address out of an XTN?"
→ get_datatype_mapping(datatype="XTN")
# Timestamps
"Is 20250901143000 safe to send as a FHIR instant?"
→ convert_datetime(value="20250901143000")Deploying remotely
Run on Railway or Render for a persistent endpoint you can share across machines.
Railway
railway init # answer prompts
railway up # deploys; gives you https://yourapp.up.railway.appSet the PORT env var if needed. Railway auto-detects Node.js.
Render
Create a new Web Service, point it at this repo, and set the Start Command to node server.js. The
deployed URL becomes your MCP endpoint — https://yourapp.onrender.com/mcp.
Related
hl7-v2-mcp-server — the companion HL7 v2.5.1 reference server
coffeemilktea.github.io — browser-based healthcare IT tools, including a browser-side HL7 v2 ↔ FHIR converter
hl7-dicom-tools — the DICOM and HL7 v2 browser tools
Mappings follow FHIR R4 (4.0.1) and the HL7 v2-to-FHIR conventions. HL7® and FHIR® are registered trademarks of Health Level Seven International; this project is not affiliated with or endorsed by HL7 International.
MIT licensed — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Privacy-preserving synthetic health data generation. FHIR R4/R5 compliant.
Diagnoses, drugs & lab codes: ICD-11, SNOMED, LOINC, RxNorm, MeSH, ATC, CID-10. 33 tools, MIT.
- mcpOAuthcom.medplum
Securely access and manage FHIR healthcare data stored in Medplum.
Read-only U.S. lab-test catalog, collection-site search, and reference-range context.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to securely interact with FHIR R4 servers for clinical decision support workflows, including PlanDefinition execution, FHIR resource management, terminology services, and Questionnaire/StructureMap transformation via Matchbox.1-
- AlicenseAqualityBmaintenanceTranslates HL7 v2.x messages to FHIR R4, validates against profiles like US Core, and explains errors in natural language for AI agents.48 npmApache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables natural-language querying of a mock legacy healthcare database and returns validated FHIR resources (Patient, Observation, Condition).MIT
- FlicenseNot gradedqualityAmaintenanceConnects MCP clients to FHIR R4+ clinical APIs over SMART on FHIR v2 Backend Services, exposing resources, named operations, terminology lookup, and pagination as configurable tools. It adds metadata- and scope-aware search/CRUD, compact responses with FHIRPath filtering, safe pagination, audit events, and bundle execution.63 npm17-