JSON-LD logoMCP

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.

ArgumentNotes
graphProjectIdrequired; 32-char lowercase hex (^[a-f0-9]{32}$); scopes authorization only
sourceFormatrequired; jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm
sourceSchemarequired; the source schema text as one string
targetFormatrequired; jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse
vendorsql output only: postgres (default), mysql, sqlserver
synapseOrg, synapseName, synapseVersionsynapse output only: the registered-schema $id parts
targetHintFormat, targetHintSchemathe schema to map toward; required for inferred mapping
mappingKindinferred (default), explicit, ai
guideexplicit: the mapping-guide JSON as text; ai: optional free-text guidance
caseInsensitiveinferred 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=inferred without targetHintSchema returns Could 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. jsonld has 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=explicit requires guide, and says exactly that.
  • The ai gate. mappingKind=ai asks 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.