# OWL — MCP: Ontology Alignment You Can Delegate: `transform_schema` and OWL Over MCP

A partner sends you their vocabulary as a Turtle file. You need your catalog expressed in their terms so the two sides can exchange data, and you need a record of which of your concepts had no counterpart on their side. That used to be an afternoon in an ontology editor plus a spreadsheet nobody trusts.

# Ontology Alignment You Can Delegate: `transform_schema` and OWL Over MCP

A partner sends you their vocabulary as a Turtle file. You need your catalog expressed in their terms so the two sides can exchange data, and you need a record of which of your concepts had no counterpart on their side. That used to be an afternoon in an ontology editor plus a spreadsheet nobody trusts.

With the CoreModels MCP server attached, it is one tool call - and the tool hands back three things: the aligned ontology, the executed mapping plan, and the ledger of what did not carry across. This article walks `transform_schema` end to end for the `owl` format key.

## Connecting

The CoreModels MCP server speaks stateless streamable HTTP with OAuth. From Claude Code:

```bash
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
```

Then run `/mcp` inside Claude Code to complete the OAuth flow. In claude.ai or Claude Desktop, add a custom connector pointing at the same `/mcp` URL. Spec-compliant clients discover the authorization server and run the PKCE authorization-code flow automatically, with dynamic client registration - no pre-registered client id to arrange.

Generic JSON-configured clients want:

```json
{
  "mcpServers": {
    "coremodels": { "type": "http", "url": "https://coremodels.example.com/mcp" }
  }
}
```

`transform_schema` is read-only and stateless. It never writes to a project; the project id it takes scopes authorization only.

## The tool, precisely

| Argument | Required | Meaning |
|---|---|---|
| `graphProjectId` | yes | 32-char hex project id, pattern `^[a-f0-9]{32}$` - the authorization scope |
| `sourceFormat` | yes | `jsonschema \| shex \| avro \| jsonld \| sql \| osi \| osi-json \| owl \| linkml \| protobuf \| odcs \| odm` |
| `sourceSchema` | yes | the source schema text; for `owl`, the Turtle document as a string |
| `targetFormat` | yes | `jsonschema \| shex \| avro \| jsonld \| sql \| osi \| osi-json \| owl \| linkml \| protobuf \| odcs \| synapse` |
| `vendor` | no | SQL output only: `postgres` (default) \| `mysql` \| `sqlserver` |
| `synapseOrg` / `synapseName` / `synapseVersion` | no | Synapse output only: the registered-schema `$id` parts |
| `targetHintFormat` / `targetHintSchema` | no | the schema to map *toward*; required for inferred mapping |
| `mappingKind` | no | `inferred` (default) \| `explicit` \| `ai` |
| `guide` | no | explicit: the mapping-guide JSON as text; ai: optional free-text guidance |
| `caseInsensitive` | no | inferred matching ignores label case, default `true` |

The input schema closes with `additionalProperties: false`, so a schema-validating client rejects a misspelled argument instead of silently sending it.

Two OWL-specific notes. First, **`owl` has no format-specific options**: `vendor` belongs to SQL output and the `synapse*` trio to Synapse output, and Turtle needs neither. Second, `owl` appears in *both* format lists above - it decodes and it encodes - which is not true of every key on the surface (`odm` decodes only, `synapse` encodes only).

One requirement trips up first-time callers. This is a *mapping* tool, and the default `inferred` strategy aligns the source toward a hint. Call it with only the four required arguments and it declines, quoting the engine verbatim:

```
Could not produce a mapping plan: inference: The inference resolver requires a target IR to match against.
```

For a straight format conversion - same model, different syntax - hand the engine the source as its own hint. Every label matches itself and the plan becomes a 1:1 alignment of the whole schema.

## The scenario: our catalog, their vocabulary

Here are the two ontologies. Ours, `catalog.ttl`:

```turtle
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix ex:   <https://example.org/catalog#> .
@prefix pt:   <https://partner.example.net/model#> .

ex:Product a owl:Class ;
    rdfs:label "Product" ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:sku ; owl:minCardinality 1 ] ,
                    [ a owl:Restriction ; owl:onProperty ex:sku ; owl:maxCardinality 1 ] ,
                    [ a owl:Restriction ; owl:onProperty ex:listPrice ; owl:maxCardinality 1 ] .

ex:sku a owl:DatatypeProperty ; rdfs:label "sku" ;
    rdfs:domain ex:Product ; rdfs:range xsd:string .

ex:listPrice a owl:DatatypeProperty ; rdfs:label "listPrice" ;
    rdfs:domain ex:Product ; rdfs:range xsd:decimal .

ex:tag a owl:DatatypeProperty ; rdfs:label "tag" ;
    rdfs:domain ex:Product ; rdfs:range xsd:string .
```

Theirs, `partner.ttl` - same concepts, different names and different IRIs:

```turtle
@prefix owl:  <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .
@prefix pt:   <https://partner.example.net/model#> .

pt:Item a owl:Class ; rdfs:label "Product" .

pt:code a owl:DatatypeProperty ; rdfs:label "sku" ;
    rdfs:domain pt:Item ; rdfs:range xsd:string .

pt:price a owl:DatatypeProperty ; rdfs:label "listPrice" ;
    rdfs:domain pt:Item ; rdfs:range xsd:decimal .
```

Note that `catalog.ttl` declares the `pt:` prefix even though it never uses it. That is deliberate: the encoder emits `@prefix` lines only for prefixes it can resolve from the source document's preserved prefix map, so declaring the target namespace up front is what makes the produced document self-contained.

The agent calls `transform_schema` with both files as strings:

```json
{
  "graphProjectId": "0123456789abcdef0123456789abcdef",
  "sourceFormat": "owl",
  "sourceSchema": "@prefix owl:  <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n@prefix ex:   <https://example.org/catalog#> .\n@prefix pt:   <https://partner.example.net/model#> .\n\nex:Product a owl:Class ;\n    rdfs:label \"Product\" ;\n    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty ex:sku ; owl:minCardinality 1 ] ,\n                    [ a owl:Restriction ; owl:onProperty ex:sku ; owl:maxCardinality 1 ] ,\n                    [ a owl:Restriction ; owl:onProperty ex:listPrice ; owl:maxCardinality 1 ] .\n\nex:sku a owl:DatatypeProperty ; rdfs:label \"sku\" ;\n    rdfs:domain ex:Product ; rdfs:range xsd:string .\n\nex:listPrice a owl:DatatypeProperty ; rdfs:label \"listPrice\" ;\n    rdfs:domain ex:Product ; rdfs:range xsd:decimal .\n\nex:tag a owl:DatatypeProperty ; rdfs:label \"tag\" ;\n    rdfs:domain ex:Product ; rdfs:range xsd:string .",
  "targetFormat": "owl",
  "targetHintFormat": "owl",
  "targetHintSchema": "@prefix owl:  <http://www.w3.org/2002/07/owl#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n@prefix pt:   <https://partner.example.net/model#> .\n\npt:Item a owl:Class ; rdfs:label \"Product\" .\n\npt:code a owl:DatatypeProperty ; rdfs:label \"sku\" ;\n    rdfs:domain pt:Item ; rdfs:range xsd:string .\n\npt:price a owl:DatatypeProperty ; rdfs:label \"listPrice\" ;\n    rdfs:domain pt:Item ; rdfs:range xsd:decimal .",
  "mappingKind": "inferred"
}
```

## What comes back

The tool returns one JSON payload with four keys - `success`, `schema`, `plan`, `lossiness`. The `schema` string is a complete Turtle document:

```turtle
@prefix ex: <https://example.org/catalog#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix pt: <https://partner.example.net/model#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<https://example.org/catalog#OwlSchema> a owl:Ontology ;
    rdfs:label "owl-schema" .

pt:Item a owl:Class ;
    rdfs:label "Product" ;
    owl:equivalentClass ex:Product ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty pt:code ; owl:minCardinality "1"^^xsd:nonNegativeInteger ] ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty pt:code ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ] ;
    rdfs:subClassOf [ a owl:Restriction ; owl:onProperty pt:price ; owl:maxCardinality "1"^^xsd:nonNegativeInteger ] .

pt:code a owl:DatatypeProperty ;
    rdfs:label "sku" ;
    owl:equivalentProperty ex:sku ;
    rdfs:domain pt:Item ;
    rdfs:range xsd:string .

pt:price a owl:DatatypeProperty ;
    rdfs:label "listPrice" ;
    owl:equivalentProperty ex:listPrice ;
    rdfs:domain pt:Item ;
    rdfs:range xsd:decimal .
```

Look at what the alignment did. The model now lives under the partner's IRIs - and it **asserts the alignment in the artifact**: `owl:equivalentClass ex:Product`, `owl:equivalentProperty ex:sku`, `owl:equivalentProperty ex:listPrice`. Our original identities were lifted into cross-standard `mapsTo` assertions when the source was decoded, and on the way out they became equivalence axioms. The mapping is not in a side file the exchange partner has to trust; it is in the ontology, in standard OWL, readable by any reasoner. The cardinality restrictions moved with the properties, so "sku is required and single-valued" is still stated after the rename.

(The document header says `owl-schema` because `catalog.ttl` has no `owl:Ontology` subject. Add one with an `rdfs:label` and your name rides through.)

The `plan` is the executed alignment, three operations, each stamped with its origin:

```json
{
  "operations": [
    { "kind": "TypeMapping", "origin": "Inferred",
      "sourceTypeId": "ex:Product", "targetTypeId": "pt:Item", "targetLabel": "Product" },
    { "kind": "ElementMapping", "origin": "Inferred",
      "sourceElementIds": ["ex:sku"], "targetElementIds": ["pt:code"] },
    { "kind": "ElementMapping", "origin": "Inferred",
      "sourceElementIds": ["ex:listPrice"], "targetElementIds": ["pt:price"] }
  ]
}
```

`origin: "Inferred"` is the agent's cue that these are guesses from label and type matching, not authored intent. Two element mappings for three properties - and the third is exactly what the ledger reports:

```json
[
  { "kind": "StructuralDrop",
    "path": "Type[ex:Product].ex:tag",
    "explanation": "Element is a member of the mapped type but no operation maps or drops it." }
]
```

`tag` has no counterpart in the partner vocabulary, so it did not travel. This is the answer to the spreadsheet question, produced automatically. A good agent surfaces it rather than swallowing it: *"Two of your three product properties align. `tag` has no equivalent on their side - drop it, or ask them to add one."*

## The three mapping kinds, from an agent's seat

- **`inferred`** (default) - label and type matching against the hint, case-insensitive unless you set `caseInsensitive: false`. Good first move whenever the two models use recognizable names.
- **`explicit`** - you supply `guide` as mapping-guide JSON text: `autoMatchByMapsTo`, `fieldMappings` of `{sourceElementIds, targetElementIds, transformName}`, `taxonomyDirectives`, `drops`. For OWL this is unusually strong, because every decoded entity already carries its IRI as a `mapsTo` assertion - so `autoMatchByMapsTo` aligns by identity rather than by name. Two ontologies that each declare equivalence to the same external IRI match even when their labels differ. Unknown keys in a guide are rejected with a path-carrying error.
- **`ai`** - a server-side proposal. Know the boundaries before offering it: it requires Editor or Admin membership on the scoping project *and* a server-configured Anthropic API key, and it sends the schema content to the Anthropic API server-side - the tool advertises that openly with an open-world hint. The proposal then passes the *same* validation gate as every other plan, with at most one repair attempt. A rejected repair is a rejection; the gate is never relaxed for a model. Without a key the tool declines honestly rather than falling back silently.

## The companion tool

When the ontology you want is not in a file but *in a CoreModels project*, use **`export_owl`** instead. Arguments are `graphProjectId` (required) and an optional `spaceId`; it is read-only. It returns the project's governed schema as Turtle, and when anything could not be expressed, the ledger is prepended as `# lossiness:` comment lines at the top of the document - comments are valid Turtle, so the honesty report stays attached to the artifact through copy-paste, email, and object storage.

## Why the shape suits agents

Arguments are flat strings, so there is no client-side serialization cleverness to get wrong. Failures come back as structured messages an agent can quote - including the decoder's `$: Line 1: …` syntax errors, which point at the exact line of the user's file. The ledger turns "did we lose anything?" from a judgment call into a list to iterate. And the plan means a one-off conversion can graduate into a reviewed, replayable pipeline without redoing the alignment.

For the full tool inventory and the write-capable admin endpoint, see the MCP quickstart in the CoreModels documentation.
