HL7-Bridge MCP
One-liner: HL7-Bridge MCP translates, validates and explains clinical messages between HL7 v2.x and FHIR R4 — deterministically, without guessing.
parse_hl7v2— parses an HL7 v2 message into a typed AST (segments → fields → components), reading separators from MSH-1/MSH-2; fails loudly (e.g.INVALID_HEADER) if it doesn't start with MSH.map_v2_to_fhir— converts an HL7 v2 message into a FHIR R4 Bundle (Patient · Observation · DiagnosticReport) via declarative YAML maps (mapId?,fhirVersion?), returning the bundle plus structured, human-explained validation issues against a profile.validate_message— checks either an HL7 v2 payload (required segments/fields, e.g. missing PV1/PID-5) or a FHIR Bundle against a profile pack (us-coredefault,cl-core,co-core), returning issues with exact locations (segment-field or FHIRPath).explain_error— turns a raw issue into a human explanation: readable location, meaning of HL7 tables (e.g. OBX-11 "F" = Final, table 0085) and an actionable hint.Decision-support traits — deterministic output,
TODO(mapeo)for unclear mappings instead of invented values, PHI-redacting logs, and explicit warnings that it is not a medical device.
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-Bridge MCP convert this ADT message to FHIR"
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-Bridge MCP
Translate, validate and explain clinical messages between HL7 v2.x and FHIR R4 — as a tool for your AI agent.
IN ORU^R01 · HL7 v2, lab result
OBX|1|NM|1554-5^GLUCOSE^LN||95|mg/dL|70-105|N|||F
OUT FHIR R4 Bundle → Patient · Observation · DiagnosticReport
✓ conforms to US Core
⚠ if PID-8 (sex) were empty:
"US Core Patient requires gender" → Patient.gender⚠️ Not a medical device. Not validated for clinical decisions. Mappings require human validation before any use with real data. See SECURITY.md.
What it is
A hospital's systems speak two "languages". HL7 v2 is the old, telegraphic one that labs and admissions use to announce "patient Juan Pérez was admitted" or "glucose 95 mg/dL". FHIR is the modern JSON one used by apps, portals and newer systems.
This is a translator between the two that also reviews the translation and explains errors in human language. It's not just another FHIR CRUD: it's the validated translation layer an agent needs so it doesn't have to talk directly to a raw FHIR server. It does three things:
Translates HL7 v2 → FHIR (and the reverse, in future phases).
Validates against profiles (US Core): that a lab's category isn't missing, that the patient's name is present…
Explains what failed and where — "the required PV1 segment for an admission is missing" — without opening the spec.
The moat isn't the MCP protocol, it's the mapping knowledge: instead of rules hidden in the
code, it uses declarative maps (maps/, readable and versionable YAML) of the form
"message PID-5 → Patient's name". Auditable, correctable translation without touching code.
Related MCP server: io.github.CSOAI-ORG/hl7-fhir-bridge-mcp
Tools
Tool | Input | Output |
|
|
|
|
|
|
|
|
|
|
|
|
profile selects the FHIR profile pack: us-core (default), cl-core, or co-core
(see National profile packs).
Maps in v0.1: ADT^A01, ORU^R01, ORM^O01 → FHIR R4 (see maps/), plus national maps
in maps/cl/ and maps/co/. Non-obvious mappings are marked TODO(mapeo)
and are not guessed.
Note: the server's human-readable messages (
message,humanMessage,hint) are currently emitted in Spanish. The examples below are translated for readability.
Call → result (one per tool)
Each tool is invoked via tools/call with these arguments. Output is abbreviated; the full curl and
more cases (malformed, edge) are in USAGE.md.
// arguments
{ "message": "MSH|^~\\&|LAB|H|EMR|H|20260102||ORU^R01|MSG1|P|2.5\rPID|1||123456^^^H^MR||DOE^JOHN||19800101|M" }
// → result
{ "ast": {
"encoding": { "field":"|","component":"^","repetition":"~","escape":"\\","subcomponent":"&" },
"segments": [ /* MSH, PID → fields → components → subcomponents */ ]
} }
// message not starting with MSH → isError: { "error": { "code":"INVALID_HEADER", "location":"MSH" } }// arguments
{ "message": "MSH|...|ORU^R01|...\rPID|...\rOBR|1||1|1554-5^GLUCOSE^LN\rOBX|1|NM|1554-5^GLUCOSE^LN||95|mg/dL|70-105|N|||F" }
// → result
{ "bundle": { "resourceType":"Bundle", "entry":[ /* Patient · Observation · DiagnosticReport */ ] },
"validation": { "issues": [], "explained": [] } } // clean if it conforms to US Core// arguments: an ADT^A01 with no PV1 and no name
{ "kind": "hl7v2", "payload": "MSH|...|ADT^A01|...\rEVN|A01|20260101\rPID|1||123456^^^H^MR" }
// → result
{ "issues": [
{ "severity":"error", "code":"MISSING_SEGMENT", "location":"PV1", "message":"Falta el segmento requerido PV1 para ADT^A01." },
{ "severity":"error", "code":"MISSING_FIELD", "location":"PID-5","message":"Falta el campo requerido PID-5 para ADT^A01." }
] }// arguments
{ "issue": { "severity":"information", "code":"CODED", "location":"OBX-11", "message":"OBX-11 tiene el valor 'F'." } }
// → result
{ "humanMessage": "OBX-11 tiene el valor 'F'. El valor 'F' significa \"Final\" (tabla HL7 0085).",
"location": "OBX-11 (Observation result status)",
"hint": "Revisa el mensaje de origen contra la especificación del perfil." }National profile packs
Beyond US Core, the validator ships pluggable national profile packs built on the same
declarative mapping engine. Pass profile to validate_message / map_v2_to_fhir, and select the
national map by mapId:
Pack |
| Source | Maps |
US Core (default) |
| — |
|
CL Core — |
| npm-style FHIR package |
|
CO Core — |
| loose StructureDefinition JSONs |
|
Two load modes exist because the official sources differ in shape: CL Core is a complete npm-style
package (package.json + .index.json, read to confirm name/version/FHIR version), CO Core is a set
of loose StructureDefinition-*.json with no manifest (version taken from the IG website, documented
in code). ⚠️ CO Core is a v0.1.0 local dev build — its structure can change without notice;
re-validate before any release.
What's covered: Patient structural conformance (identifier, official name, and for CO
gender/birthDate/address), reusing the generic v2→FHIR mapping (name, birth date, sex, address,
telecom). Same level as US Core: structural + must-support presence, not full value-set binding or
normative slicing.
What's TODO(mapeo) (deliberately not guessed): national conventions with no spec-backed v2
mapping — Chile's RUN identifier typing and DEIS/second-surname extensions; Colombia's
identifier-type slices, Patient.active, DIVIPOLA codes, and profile extensions
(Fathers/MothersFamilyName, GenderIdentity, BirthPlace…), each with the evaluated v2
candidates documented in the map. Neither IG profiles Observation/DiagnosticReport, so lab
resources in an ORU stay on base FHIR. Details in
context/ARCHITECTURE.md → Decisiones.
AR Core is intentionally not implemented in this cycle: the only available source is a 2021 ci-build (v0.5.0) with no formal release. See the deferral note in
ARCHITECTURE.md.
No licensed terminology (LOINC/SNOMED) is redistributed; the IG packages themselves are not shipped with the npm package — validation runs on curated FHIRPath rules derived from each profile's real cardinalities.
Try it (Claude Code)
This is an MCP server, so the natural way to use it is from an MCP client. In Claude Code you
register it once with claude mcp add and then just ask in plain language — Claude picks the tool
and fills the arguments for you. No HTTP, no ports, no curl.
1. Register it as a stdio server. tsx runs the TypeScript source directly — no build step, nothing to go stale:
npm ci
claude mcp add hl7-bridge -- npx tsx "$(pwd)/src/server/index.ts"Prefer a compiled binary? npm run build then register node "$(pwd)/dist/server/index.js" instead — but rebuild after every source change.
2. Verify with claude mcp list, and inside Claude Code with /mcp (you should see the four
hl7-bridge tools). To remove it later: claude mcp remove hl7-bridge.
3. Ask in natural language. Paste a message and let Claude call map_v2_to_fhir:
Map this HL7 v2 lab result to FHIR and tell me if it conforms to US Core:
MSH|^~\&|LAB|H|EMR|H|20260102||ORU^R01|1|P|2.5PID|1||123456^^^H^MR||DOE^JOHN||19800101|MOBR|1||1|1554-5^GLUCOSE^LNOBX|1|NM|1554-5^GLUCOSE^LN||95|mg/dL|70-105|N|||F
You get a Bundle with Patient · Observation · DiagnosticReport and clean validation
("issues": []). Observation.category = laboratory (which US Core requires) is populated as a
deliberate constant documented in the map — HL7 v2 has no field that carries it and an
ORU^R01 is always laboratory —, not a guessed value.
The value is in what it warns about. Now drop the trailing M (PID-8, sex) from that PID
segment — ...||19800101|M → ...||19800101|. Because gender does come from a message field,
its absence is real debt, and validation reports it with an exact location and a human explanation:
"issues": [
{ "severity":"error", "code":"PROFILE_REQUIRED", "location":"Patient.gender",
"message":"US Core Patient requiere gender." }
],
"explained": [
{ "humanMessage":"US Core Patient requiere gender.", "location":"Patient.gender",
"hint":"El perfil FHIR exige este elemento (must-support). Ajusta el mapa o el mensaje de origen para poblarlo." }
]The difference with category is one of origin: gender has a v2 field (PID-8) that was left empty;
category is a constant with no field. The full test set for all 4 tools (valid, malformed, edge
cases) is in USAGE.md.
Not using Claude Code? To drive the server over HTTP from another system (or raw
curl), see the HTTP transport, protocol and per-tool calls in USAGE.md.
Why use it vs. letting the agent translate directly
An LLM can approximate HL7 v2 → FHIR on its own. The value here isn't the conversion — it's that the conversion is repeatable, auditable, and refuses to guess.
Concern | With this MCP | LLM translating directly |
Determinism | Same input → identical output every run (declarative YAML maps) | Non-deterministic; same message can map differently across runs/models |
Won't invent data | Refuses to guess — unknown coding system → | Hallucinates plausible values (a wrong |
Parsing | Reads separators from MSH-1/2; handles repetitions, subcomponents, escapes, | Usually assumes |
Validation | Checks US Core requirements, returns exact | "Looks conformant" — nothing actually validated it |
Failure mode | Fails loudly with a structured error ( | Fails silently — a partial/wrong Bundle that looks fine |
Auditability | Mapping rule lives in versioned YAML you can diff and correct | Rule lives in a prompt/weights; you can't inspect why it mapped that way |
The gender case above is the thesis: it flags real debt (empty PID-8) with an exact location,
and does not flag the deliberate category constant. An LLM alone would likely invent a gender
or stay silent.
Where it doesn't add much: coverage is narrow today (R4 only; ADT^A01, ORU^R01, ORM^O01, OUL^R22
partial — outside those you get a clean MAP_NOT_FOUND); validation is minimal US Core, not the
full IG; and it's still not a clinical safety net — output requires human validation. For a one-off,
throwaway translation where repeatability and correctness don't matter, the plain LLM is faster.
Bottom line: use it when the output feeds a real system and a wrong-but-plausible Bundle is worse than a loud failure. For casual exploration, it may be overhead you don't need.
Usage
npm ci && npm test # + npm run typecheck / lint / test:coverage
npm run build && npm start # stdio server for local MCP clients (Claude Code, Claude Desktop)Full setup, testing and deployment steps are in USAGE.md: the HTTP transport (POST /mcp, for hosting or driving the server from another system), the JSON-RPC protocol, a curl call per tool, the complete test set, and Render deployment.
Design principles
PHI-safe by default — the repo never contains real PHI; logs redact PID/NK1/GT1.
Narrow tools — each tool has typed input/output, not a "do whatever you want".
Determinism and explainability — every error carries structure + what and where in human language.
Fail loudly — a malformed message yields a structured error, never a silent partial result.
Not a medical device — no clinical decisions without human validation.
Stack and structure
TypeScript (Node ≥ 20) strict, official MCP SDK (@modelcontextprotocol/sdk), fhirpath.js
@types/fhir, Vitest. Typed errors (Hl7BridgeErrorwithcode,location,humanMessage).
/src/{server,parser,mapper,validator,errors} # MCP · HL7 v2 · declarative mapping · FHIR profiles · errors
/maps # declarative YAML maps (ADT, ORU, ORM…)
/test/fixtures # synthetic messages + expected outputsDetail in context/ARCHITECTURE.md (components, I/O contracts,
map format) and context/PRD.md.
References
Mapping and validation decisions are grounded in these authoritative sources, not guessed:
HL7 v2.5.1 Chapter 7 — Observation Reporting — message structures (ORU, OUL) and segment optionality.
HL7 Version 2 to FHIR Implementation Guide — segment/datatype ConceptMaps, e.g. SPM → Specimen.
HL7/v2-to-fhir repository — source
.fsh/CSV mappings (order-control, patient-class, report-status tables used by transforms).US Core Implementation Guide — Patient/Observation profile invariants.
FHIR R4 specification — target resource model.
License
Apache-2.0 at the core (parser, mapper, validator, MCP tools). See LICENSE.
Available Tools
4 toolsexplain_errorA
Convierte un issue de validación en explicación humana: ubicación legible, significado de tablas HL7 y una pista accionable.
Inputs: issue (required).
map_v2_to_fhirA
Mapea un mensaje HL7 v2 a un Bundle FHIR R4 con un mapa declarativo y valida el resultado contra US Core mínimo, explicando cada issue.
Inputs: message (required), mapId, fhirVersion.
parse_hl7v2A
Parsea un mensaje HL7 v2 a un AST tipado (segmentos, campos, componentes) con los separadores leídos de MSH-1/MSH-2.
Inputs: message (required).
validate_messageA
Valida un mensaje HL7 v2 (segmentos/campos requeridos) o un Bundle FHIR (perfil US Core mínimo) y devuelve issues estructurados.
Inputs: kind (required), payload (required), profile.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 4 tools
Each tool has a clear core purpose (parse, validate, map, explain), but map_v2_to_fhir and validate_message both perform validation, which could create confusion when an agent is deciding which tool to use for a raw input check. The descriptions are specific enough to generally disambiguate.
All tool names follow a consistent verb-first snake_case pattern (parse, validate, map, explain), with clear object references. There is no mixing of casing or verb styles, making the set predictable.
Four tools is a well-scoped count for an HL7-to-FHIR bridge workflow. Each tool covers a distinct step without unnecessary duplication, and nothing feels like filler.
The core pipeline (parse, validate, map, explain) is well covered, but a 'bridge' might imply bidirectional conversion. Since all tools are v2-to-FHIR oriented, a reverse mapping tool is a natural but missing addition, and there is no map management tool, which would be a minor gap.
Maintenance
Related MCP Connectors
Privacy-preserving synthetic health data generation. FHIR R4/R5 compliant.
Check ISO 20022 messages (pacs.008, pain.001, camt.053, seev.031) against schema and rules.
HL7 FHIR R4 health-data API for AI agents: free sandbox, 30 synthetic patients, AI-ready context.
Guardrailed FHIR access for AI agents: PHI redaction, audit trail, step-up auth, tenant isolation
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceProvides FHIR resource validation, synthetic test fixture generation, and HIPAA-safe logging review as MCP tools for AI agents.-
- AlicenseNot gradedqualityDmaintenanceEnables healthcare systems to parse HL7 v2 messages, convert them to FHIR R4 Bundles, validate FHIR resources, and detect/govern PHI with HIPAA, EU MDR, and GDPR compliance evidence for AI governance.MIT
- AlicenseNot gradedqualityBmaintenanceEnables LLMs to retrieve segment, field, message-structure, and code-table details; parse pipe-delimited messages; and validate messages with deep profiles for ORM^O01, ADT^A08/A31/A40, and ORU^R01.MIT
- AlicenseNot gradedqualityBmaintenanceEnables users to look up how specific v2 fields, datatypes, and table codes translate to R4, and to convert whole ORM, ADT, and ORU messages into transaction Bundles with explicit warnings for unmapped values.MIT