Three Questions an Agent Must Answer After Converting a Schema
When an agent converts a vocabulary into something else, it should be able to answer three questions afterwards: what did it produce, what did it lose, and can it do that again identically? Most conversion tooling answers the first. The `transform_schema` tool on the CoreModels MCP server answers all three in a single call — the produced schema, an explicit lossiness ledger, and the executed plan as a replayable artifact. This article runs a JSON-LD vocabulary through it end to end, with the exact arguments and the exact response.
Three Questions an Agent Must Answer After Converting a Schema
When an agent converts a vocabulary into something else, it should be able to answer three
questions afterwards: what did it produce, what did it lose, and can it do that again identically?
Most conversion tooling answers the first. The transform_schema tool on the CoreModels MCP
server answers all three in a single call — the produced schema, an explicit lossiness ledger, and
the executed plan as a replayable artifact. This article runs a JSON-LD vocabulary through it end
to end, with the exact arguments and the exact response.
We are the CoreModels team at ARAMAI; the contract and the outputs below come from the shipped server.
Connecting
CoreModels serves a stateless streamable-HTTP MCP endpoint at /mcp, secured with OAuth. From
Claude Code:
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
then /mcp inside the session to complete the flow. Any JSON-configured client works the same
way:
{
"mcpServers": {
"coremodels": { "type": "http", "url": "https://coremodels.example.com/mcp" }
}
}
An unauthenticated request gets a 401 with a WWW-Authenticate header pointing at the
protected-resource metadata; spec-compliant clients then run discovery, dynamic client
registration, and the PKCE authorization-code flow with no pre-registered client id. The /mcp
endpoint serves read-only tools, and transform_schema is one of them: stateless, writes nothing,
Viewer access is enough — with one exception noted at the end.
The tool contract
transform_schema decodes the source format, produces a mapping plan, validates that plan through
the engine's universal gate, executes it deterministically, and encodes the target format. Its
input schema declares additionalProperties: false, so the list below is exhaustive — an
agent that invents an argument gets a validation error, not a silent ignore.
| Argument | Notes |
|---|---|
graphProjectId | required; 32-char lowercase hex (^[a-f0-9]{32}$); scopes authorization only |
sourceFormat | required; jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm |
sourceSchema | required; the source schema text as one string |
targetFormat | required; jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse |
vendor | sql output only: postgres (default), mysql, sqlserver |
synapseOrg, synapseName, synapseVersion | synapse output only: the registered-schema $id parts |
targetHintFormat, targetHintSchema | the schema to map toward; required for inferred mapping |
mappingKind | inferred (default), explicit, ai |
guide | explicit: the mapping-guide JSON as text; ai: optional free-text guidance |
caseInsensitive | inferred only; ignore label case (default true) |
Note the arguments that are not there. There is no JSON-LD dialect switch, profile selector, or
context-handling option, because the jsonld key means one thing in both directions: a vocabulary
of rdfs:Class and rdf:Property nodes in an @context/@graph document. jsonld is valid as
sourceFormat and as targetFormat, so an agent can read a published vocabulary or emit one.
The scenario
An agent is handed an order vocabulary and asked to stand it up as Postgres tables:
{
"@context": {
"schema": "https://schema.org/",
"ex": "https://example.org/vocab#"
},
"@graph": [
{ "@id": "ex:Order", "@type": "rdfs:Class", "rdfs:label": "Order" },
{ "@id": "ex:OrderStatus", "@type": "rdfs:Class", "rdfs:label": "OrderStatus",
"rdfs:subClassOf": { "@id": "schema:Enumeration" } },
{ "@id": "ex:OrderStatusPlaced", "@type": "ex:OrderStatus", "rdfs:label": "Placed" },
{ "@id": "ex:OrderStatusShipped", "@type": "ex:OrderStatus", "rdfs:label": "Shipped" },
{ "@id": "ex:orderStatus", "@type": "rdf:Property", "rdfs:label": "orderStatus",
"schema:domainIncludes": { "@id": "ex:Order" },
"schema:rangeIncludes": { "@id": "ex:OrderStatus" } },
{ "@id": "ex:total", "@type": "rdf:Property", "rdfs:label": "total",
"schema:domainIncludes": { "@id": "ex:Order" },
"schema:rangeIncludes": { "@id": "schema:Number" } }
]
}
The controlled vocabulary is expressed the schema.org way: ex:OrderStatus subclasses
schema:Enumeration, and its permitted values are nodes typed ex:OrderStatus. The coder reads
that as a taxonomy, not as two more classes.
The inferred strategy matches against a target hint, so a conversion with no reshaping intent
passes the source as its own hint. The tool call:
{
"graphProjectId": "0123456789abcdef0123456789abcdef",
"sourceFormat": "jsonld",
"sourceSchema": "…the vocabulary above, as one JSON string…",
"targetFormat": "sql",
"vendor": "postgres",
"targetHintFormat": "jsonld",
"targetHintSchema": "…the same string…",
"mappingKind": "inferred"
}
What comes back
One JSON document with four fields: success, schema, plan, lossiness. For text targets
such as SQL, schema is a string; for JSON-shaped targets it is an object. The schema here:
CREATE TABLE "Order" (
"orderStatus" VARCHAR(255),
"total" NUMERIC
);
COMMENT ON COLUMN Order.orderStatus IS '{"x-maps-to":{"ex":"https://example.org/vocab#orderStatus"}}';
COMMENT ON COLUMN Order.total IS '{"x-maps-to":{"ex":"https://example.org/vocab#total"}}';
schema:Number became NUMERIC, the taxonomy-valued property became a column, and — the part
worth pausing on — each property's IRI survived into the DDL as a machine-readable column comment.
The DDL knows which vocabulary term each column implements, which means the next conversion in the
chain can match on identity instead of on a column name.
The lossiness ledger is where the agent earns its keep. Exactly one entry:
[
{
"kind": "ConstraintRelaxation",
"path": "Element[ex:orderStatus]",
"explanation": "Postgres has no inline enum; emitted as VARCHAR (the allowed-value constraint is not enforced)."
}
]
A well-built agent does three things with that. It surfaces the relaxation in its answer ("the Placed/Shipped restriction is not enforced by this DDL — add a CHECK constraint or enforce it in the application"). It does not retry: this is a property of the target format, not a transient failure, and a second call produces the same entry. And it stores the third field, the plan:
{
"operations": [
{ "kind": "TypeMapping", "origin": "Inferred", "sourceTypeId": "ex:Order",
"targetTypeId": "ex:Order", "targetLabel": "Order" },
{ "kind": "ElementMapping", "origin": "Inferred",
"sourceElementIds": ["ex:orderStatus"], "targetElementIds": ["ex:orderStatus"] },
{ "kind": "ElementMapping", "origin": "Inferred",
"sourceElementIds": ["ex:total"], "targetElementIds": ["ex:total"] },
{ "kind": "TaxonomyMapping", "origin": "Inferred",
"sourceTaxonomyId": "ex:OrderStatus", "targetTaxonomyId": "ex:OrderStatus",
"targetTreatment": "InlineEnum" }
]
}
Four operations, each naming its constructs by id — and because JSON-LD ids are qnames, the plan
reads as a list of decisions about ex:orderStatus and ex:OrderStatus rather than about
anonymous positions. The TaxonomyMapping operation is the enumeration decision made explicit.
Persist that JSON and the conversion stops being anecdotal: the same plan plus the same source
produces the same output, and the HTTP plan/execute endpoint will replay it on demand.
Changing the target is one argument. Set targetFormat to linkml, owl, avro, shex,
odcs, protobuf, or jsonschema and the response shape is identical — schema, plan, ledger —
so an agent asked for "the same model, four ways" runs the same call four times and diffs four
ledgers.
Failure modes worth wiring in
The tool's errors are written to be self-correcting for a model reading them:
- No hint.
mappingKind=inferredwithouttargetHintSchemareturnsCould not produce a mapping plan: inference: The inference resolver requires a target IR to match against.The fix is the identity hint above, or a real target schema. - Unknown format key. The message carries the whole menu:
Unknown schema format '…'. Use: jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm. - One-way format keys.
sourceFormat: "synapse"returns'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema — decode it with the 'jsonschema' format.jsonldhas no such restriction in either direction. - Malformed document. A parse failure comes back as
Could not parse the 'jsonld' schema: …with the underlying reason; a structurally wrong document (root not an object) comes back as a decode error with the path$. - Explicit without a guide.
mappingKind=explicitrequiresguide, and says exactly that. - The
aigate.mappingKind=aiasks a server-side Claude proposer for the plan. The proposal faces the same validation gate as every other strategy, with at most one repair attempt — a rejected repair is a rejection. It requires Editor or Admin membership on the project and a server-configured Anthropic key, and it sends the schemas to the Anthropic API server-side; the tool advertises that open-world behavior rather than burying it. Without the key or the membership it declines with an actionable message instead of degrading quietly.
Why this format suits agents
Back to the three questions. What did it produce is the schema field, in the target's own
syntax. What did it lose is the ledger, which for this conversion was one specific, named,
actionable entry rather than a shrug. Can it do it again is the plan, which is the same artifact
the HTTP surface replays — so an agent and a build pipeline can share one reviewed conversion.
JSON-LD strengthens all three, because every node in a vocabulary arrives with a global IRI that the coder lifts into a cross-standard annotation. Agents that convert from it produce outputs that still carry their identity, and agents that map toward another schema can match by IRI rather than by hopeful string comparison. For the same engine over plain HTTP, see the Schema Transformation API guide in the CoreModels docs.