Skip to content

Agents and MCP

Ask a language model to convert one system’s records into another’s and it will do it. It will match dob to birthDate because those look alike, decide that status: "VOID" is probably cancelled, and produce output that is right often enough to be dangerous. What it cannot do is tell you which of its guesses were guesses.

An ontology replaces the guessing with something the publisher actually said. The correspondences are stated, the code translations are enumerated, and the places where a conversion loses information are marked as lossy. That is exactly the material a model is otherwise inventing.

Meaning, not just shape. A JSON Schema says status is a string. The model says what a Person is in this system and the vocabulary enumerates every status code with its display text and definition. A model reading the schema alone has to infer the domain; reading the ontology, it is told.

Correspondences the publisher stands behind. Each mapsTo entry names an external schema and how closely the alignment holds. An agent does not have to decide whether this system’s Person is FHIR’s Patient. Somebody who knows has already answered.

An honest signal about loss. type is one-to-one, one-to-many, many-to-one or partial. A partial alignment is the agent’s cue to surface the conversion for review rather than to complete it quietly, and it is a cue that arrives before the data does.

Executable transformations, where they exist. When mapping is present it points at a FHIR StructureMap or ConceptMap. Running it beats asking a model to reproduce it. The published map is deterministic, reviewable and the same every time, which are three things a generated transformation is not.

Prefer the mapping to the model’s judgement

Section titled “Prefer the mapping to the model’s judgement”

The useful discipline is a fallback order, not a choice:

  1. A published mapping. Execute it. The model’s job is to arrange the call, not to perform the conversion.
  2. A mapsTo with no mapping. The alignment is stated, so the target is settled; the agent works out the field correspondences between two known schemas rather than between two unknown systems. Much smaller problem, and one a reviewer can check.
  3. No alignment at all. This is the case an ontology cannot help with, and the agent should say so rather than proceed. A model with no mapsTo is invisible to integration, which is why validateBundle warns about it.

Whatever produced the output, validate it against the target schema before returning it. Vocabularies are published as JSON Schema, so an invented code fails an ordinary validator, without anyone having to trust the model’s recollection of the list.

An agent should not be handed a whole ontology. A large one does not fit a context window, most of it is irrelevant to the task, and paying attention to forty models to convert one record makes the conversion worse rather than better.

The format already anticipates this. Discovery starts at one well-known URI and fetches outward, and a consumer takes only what the task needs. That is the same access pattern a tool interface wants, which makes MCP a natural fit: the server fetches, the agent asks.

A tool surface that follows the format’s own shape:

Tool
find_ontology Takes a domain, returns the ontology from its well-known URI.
list_models Names, ids and categories only. Enough to choose, not enough to fill a context window.
get_model One model, with its relationships and alignments.
get_schema The JSON Schema a model points at.
get_vocabulary One code list, or the schema derived from it.
find_alignment Given a model and a target system, the mapsTo entry and its mapping, if there is one.

find_alignment is the one worth having. It is the question an agent actually arrives with, and answering it directly keeps the agent from reading every model to work out which one is relevant.

@openhi/ours is the part of that server you do not have to write. It parses the documents, assembles them, and hands back typed resources:

import { assembleBundle, wellKnownOntologyUrl } from "@openhi/ours";
const fetchJson = (url: string) => fetch(url).then((r) => r.json());
const ontology = await fetchJson(wellKnownOntologyUrl("example.org"));
const bundle = assembleBundle({
ontology,
models: [await fetchJson(ontology.models)],
vocabularies: [await fetchJson(ontology.vocabularies)],
});
// The answer to "what does this become in FHIR?"
const alignmentTo = (modelUrl: string, system: string) =>
bundle.models.get(modelUrl)?.mapsTo?.find((m) => m.system === system);

Nothing in this repository ships an MCP server today. The tool surface above is a sketch of one, not a specification, and OURS does not define how an ontology is exposed to an agent. If you build one, the format is the contract; the tool names are yours.

An ontology is a claim, not a proof. It says what a publisher believes about their own data. A one-to-one alignment that quietly drops a field is a bug in the ontology, and an agent trusting it will produce confidently wrong output. Validating against the target schema catches the structural half of this. Nothing catches the semantic half except somebody looking.

A fetched ontology is somebody else’s content. Descriptions, code display text and comments are prose from a third party, arriving inside an agent’s context. Treat them as data to reason about rather than as instructions to follow, the same way you would treat any other fetched document.