Library / SDK
thetahealth/mirobody avatar
thetahealth/mirobody

Mirobody: a LOINC resolver that abstains instead of guessing

The AI-native health data engine — collect, standardize, and reason over labs, wearables & genomics.

1,313 stars229 forksPythonApache-2.0

At a glance

What is it?
Mirobody standardizes lab reports, wearable readings and genomics into LOINC codes and UCUM units, then exposes the result to agents over MCP. The interesting design choice is that it returns nothing when it does not know a term.
Who is it for?
Adopt Mirobody if you are ingesting multilingual lab reports or wearable exports and need LOINC-coded, UCUM-normalized records you can hand to an agent or a FHIR store, and if you are willing to run Postgres with pgvector via ./deploy.sh. Do not adopt it if you need a hosted service with an uptime commitment, or if your data is mostly free-text clinical notes rather than named observations with values and units.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem is two reports for the same test

A lipid panel printed in one lab and the same panel printed in another rarely share a spelling or a unit. One writes total cholesterol in mmol/L, the other in mg/dL. Add Chinese, Japanese or Traditional Chinese column headers and a downstream agent has no reliable key to join on. Mirobody's stated job is to turn any reading, in any language, into a LOINC code plus a UCUM unit, so that two reports of the same measurement collapse onto one identifier. The README frames this as the core problem and the package is built around it. The audience is developers building health agents, self-hosted patient data pipelines, or anything that needs to chart a lab draw against a wearable series. The repository topics list ai-agents, fhir, lab-reports, loinc and mcp, which matches that audience rather than a clinical-records or billing audience.

Collect, translate, answer: the three-stage layout

The codebase is split into three directories that mirror the three verbs in the project's own framing. mirobody/pulse/ handles collection: the README lists three device providers, a SQL source, seven file formats, and Apple Health as receive-only, where a signed iOS client POSTs data in. mirobody/indicator/ handles translation, mapping readings to canonical codes across LOINC, SNOMED CT and RxNorm and normalizing units to UCUM. mirobody/agent/ handles answering, and the README describes an agent that reads the original documents through a virtual filesystem and returns charts with citations to the page it read from. That last detail matters more than it looks. Rather than summarizing a parsed record, the agent is described as going back to the source document, which is what makes a citation meaningful. The translation layer is the part with the most public detail, and it is the part most projects get wrong.

Why resolve_reading needs the value and the unit

LOINC does not encode a measurement name alone. It encodes the name plus the result type and the unit, so the same analyte has different codes depending on how it was reported. Mirobody surfaces this directly in its API rather than hiding it. The README shows resolve("total cholesterol") returning 2093-3, the mass-per-volume code, while resolve_reading("total cholesterol", "5.0", "mmol/L") returns 14647-2, the molar code, and resolve_reading("total cholesterol", "193", "mg/dL") returns 2093-3. The unit picks the code. The same split appears for a neutrophil percentage versus a neutrophil count: 26511-6 for "62 %" and 26499-4 for "4.2" in 10*9/L. This is a design constraint that will surprise anyone who expects one code per analyte. If your pipeline drops units before calling the resolver, you are throwing away the input that determines the answer. The README's own advice is to pass the value and the unit when you have them.

Abstention is the feature, not a gap

The resolver returns nothing for terms it cannot place. The README example calls resolve("血脂") (lipids) and notes that it is a category, not an observation, so the resolver returns nothing rather than a plausible wrong code, with resolved set to False. The documentation draws a distinction between an empty result and method="refused": an empty code is described as a gap worth a second look, while a refusal is a decision. That is an unusual posture for a terminology mapper, where the common failure mode is a confident wrong match that silently corrupts a longitudinal series. A wrong LOINC code is worse than a missing one, because nothing downstream flags it. The cost is coverage. Abstention means you will need a review path for whatever the engine declines, and the README does not describe a queue, a UI or a workflow for that. You build it.

Getting it running: pip, then deploy.sh

The fastest path needs no key, no config and no network. The README gives pip install mirobody followed by mirobody resolve "LDL cholesterol" 血红蛋白 ヘモグロビン "空腹血糖(GLU)" 血脂, and states that the resolver runs offline. The install is described as two packages, 52 MB, depending on numpy only, which is a small footprint for a terminology engine. Running the full stack is a different matter. The README's instructions are git clone of the repository, then git lfs install and git lfs pull to fetch the engine's data bundles, then ./deploy.sh, which brings up Postgres with pgvector. The LFS step is not optional: the README warns that a fresh clone holds pointer stubs until you pull. For agent integration, the README states the same tools are served over MCP to Claude Desktop, Cursor or a custom agent, though the README excerpt does not show the MCP configuration block, so check docs.mirobody.ai for the exact client setup before wiring it up.

The bundle version is a real operational constraint

Mirobody pins its terminology to a named bundle. The README exposes mirobody.BUNDLE_VERSION with the value loinc-2.82+2026.08.28-af2524b7a285, which encodes the LOINC release, the cut date and a digest over the bundle's own members. The project also publishes dated terminology build releases, such as data-2026-09-10, separate from the code releases 1.4.0 and 1.3.0. That separation is honest about the fact that code and vocabulary move on different schedules, but it also means an upgrade has two axes. Bumping the package can change which codes your stored readings map to, and the digest in the version string is what lets you detect that. The README explicitly addresses why the project sits on LOINC 2.82 rather than 2.83 and links to docs/standardization.md for the reasoning. If you store resolved codes, treat a bundle change as a migration, not a routine dependency bump.

Where Mirobody is the wrong tool

The engine is built around named observations with values and units. It is not a clinical NLP system for free-text notes, and nothing in the README suggests it parses narrative discharge summaries into structured findings. If your input is mostly prose, the translation layer has little to work on. The semantic tier is another boundary: the README mentions an opt-in semantic tier that cannot abstain, which is the opposite of the default behavior and should be enabled deliberately. Coverage has limits too. The project reports 211/211 on the panels an ordinary checkup prints in English, Chinese (Simplified and Traditional) and Japanese, which is a statement about common panels, not about rare analytes, specialty assays or non-human data. Genomics appears in the project description and topics but the README excerpt gives no detail on how genomic data is standardized, so treat that claim as unverified from the material available. Finally, the README cites a live consumer product with 5,000+ registered users and 500+ daily active as evidence the engine runs in production; that is a claim about someone else's deployment, not a guarantee about yours.

How it compares to a general-purpose terminology server

The obvious alternative is a general FHIR terminology server such as Ontoserver or a plain LOINC search API. Those serve the whole vocabulary and answer lookups, but they do not decide whether a bare string like 血脂 is an observation or a category, and they do not take a unit into account to pick between the mass and molar code for the same analyte. Mirobody's resolver is narrower and opinionated: it accepts a term, optionally a value and a unit, and returns a single canonical code or nothing. That is a different contract. A terminology server is a lookup service; Mirobody is a decision layer that has already made the hard call about whether to answer. The trade-off is that you inherit its judgment and its bundle version, and you cannot easily swap in a different vocabulary build without forking. If you need broad SNOMED CT reasoning across clinical domains, a full terminology server is the better fit. If you need multilingual lab and wearable readings collapsed onto LOINC with units resolved, the narrower contract is the point.

Licence and maintenance cost

Mirobody is Apache-2.0, which permits commercial use, modification and redistribution provided you keep the licence and notice files and state changes. This is not legal advice; read the LICENSE file in the repository. One thing to check is the licensing of the terminology data the bundles carry, since LOINC, SNOMED CT and RxNorm each have their own terms of use, and the repository's Apache-2.0 licence covers the code, not necessarily every bundled artifact. The maintenance picture visible from the material is active: releases 1.3.0 and 1.4.0 landed within a week of each other in late August and early September 2026, with a separate dated terminology build on 10 September 2026. That cadence implies you should expect to refresh bundles periodically if you want current codes. The self-hosted path adds Postgres with pgvector to your operational surface, so the real ongoing cost is a database you now run, not the Python package.

Editorial conclusion

Adopt Mirobody if you are ingesting multilingual lab reports or wearable exports and need LOINC-coded, UCUM-normalized records you can hand to an agent or a FHIR store, and if you are willing to run Postgres with pgvector via ./deploy.sh. Do not adopt it if you need a hosted service with an uptime commitment, or if your data is mostly free-text clinical notes rather than named observations with values and units. Before committing, run mirobody resolve against your own term list and count how many entries come back with resolved set to False, because that abstention rate is the real measure of fit for your vocabulary.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. thetahealth/mirobody on GitHub
Community notes

Community notes