# JSON-LD — MCP: 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:

```bash
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:

```json
{
  "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:

```json
{
  "@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:

```json
{
  "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:

```sql
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:

```json
[
  {
    "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:

```json
{
  "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.
