"Convert Our Semantic Model for dbt" — Ossie Through the transform_schema Tool
Here is the request, as an analyst actually phrases it: *"Take the billing semantic model in our repo, give me the JSON form our dbt project ingests, and tell me what didn't survive the conversion."*
"Convert Our Semantic Model for dbt" — Ossie Through the transform_schema Tool
Here is the request, as an analyst actually phrases it: "Take the billing semantic model in our repo, give me the JSON form our dbt project ingests, and tell me what didn't survive the conversion."
An agent with the CoreModels MCP server connected can do all three in one tool call. This walkthrough shows that call — the exact arguments, the exact response, and how the agent should read the ledger before reporting back. The format in question is Apache Ossie (formerly OSI), which CoreModels (by ARAMAI) speaks in both directions under two keys: osi for YAML and osi-json for the JSON serialization dbt ingests.
Connecting the server
The MCP endpoint is https://coremodels.example.com/mcp and it uses OAuth 2.0. Spec-compliant clients discover the authorization server, register dynamically, and run the PKCE flow themselves — there is no client id to pre-provision. From Claude Code:
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
Then /mcp inside the session completes the handshake. In claude.ai or Claude Desktop, add a custom connector pointing at the same URL. For a generic JSON-configured client:
{
"mcpServers": {
"coremodels": { "type": "http", "url": "https://coremodels.example.com/mcp" }
}
}
/mcp serves the read-only (Viewer-role) tools, and transform_schema is one of them — it writes nothing to any project. The project id it takes exists to scope authorization.
One scoping note before the walkthrough: the tool is stateless. Schema text goes in, schema text comes out; it never reads a project's stored schema. The MCP export tools that do read a project cover JSON Schema, JSON-LD, ShEx, Avro, LinkML, SQL, and OWL — so to turn a governed project into an Ossie document, call POST graph/transform/schema/export/{projectId} with "format": "osi" on the HTTP surface.
The tool contract
transform_schema runs the full mapping engine: decode the source, produce a plan, validate that plan through the universal gate, execute deterministically, encode into the target format. Its input schema declares additionalProperties: false, so an agent that invents an argument gets a validation error rather than a silently ignored typo.
| Argument | Required | Meaning for Ossie work |
|---|---|---|
graphProjectId | yes | 32-char hex project id (^[a-f0-9]{32}$) — scopes authorization only |
sourceFormat | yes | osi or osi-json reading in; both accept YAML or JSON, since JSON is a YAML subset |
sourceSchema | yes | the document text |
targetFormat | yes | osi writes YAML, osi-json writes JSON; also jsonschema, shex, avro, jsonld, sql, owl, linkml, protobuf, odcs, synapse |
targetHintFormat / targetHintSchema | for inferred | the schema to map toward; for a straight conversion, pass the source again |
mappingKind | no | inferred (default) | explicit | ai |
guide | for explicit | the SIA mapping-guide JSON as text; free-text guidance when mappingKind is ai |
caseInsensitive | no | inferred matching ignores label case (default true) |
vendor | no | SQL output only: postgres (default) | mysql | sqlserver |
synapseOrg, synapseName, synapseVersion | no | Synapse output only; ignored by Ossie targets |
The model
billing.yaml in the repository:
version: "0.1.1"
semantic_model:
- name: billing
description: Subscription billing model.
datasets:
- name: invoices
source: billing.invoices
primary_key: invoice_id
ai_context:
synonyms: [bills, statements]
fields:
- name: invoice_id
- name: issued_at
dimension:
is_time: true
- name: amount_usd
description: Invoice total in USD.
- name: accounts
source: billing.accounts
primary_key: account_id
fields:
- name: account_id
relationships:
- name: invoice_account
from: invoices
to: accounts
from_columns: [account_id]
to_columns: [account_id]
metrics:
- name: mrr
description: Monthly recurring revenue.
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(invoices.amount_usd)
The call
The agent reads the file and issues one tool call. Inference matches labels against a target hint and declines without one (The inference resolver requires a target IR to match against.), so for a pure serialization change the source doubles as its own hint:
{
"graphProjectId": "3f2a9c7e5b1d4a8fbc0e6d2a9f4b7c31",
"sourceFormat": "osi",
"sourceSchema": "version: \"0.1.1\"\nsemantic_model:\n - name: billing\n description: Subscription billing model.\n datasets:\n - name: invoices\n source: billing.invoices\n primary_key: invoice_id\n ai_context:\n synonyms: [bills, statements]\n fields:\n - name: invoice_id\n - name: issued_at\n dimension:\n is_time: true\n - name: amount_usd\n description: Invoice total in USD.\n - name: accounts\n source: billing.accounts\n primary_key: account_id\n fields:\n - name: account_id\n relationships:\n - name: invoice_account\n from: invoices\n to: accounts\n from_columns: [account_id]\n to_columns: [account_id]\n metrics:\n - name: mrr\n description: Monthly recurring revenue.\n expression:\n dialects:\n - dialect: ANSI_SQL\n expression: SUM(invoices.amount_usd)",
"targetFormat": "osi-json",
"targetHintFormat": "osi",
"targetHintSchema": "version: \"0.1.1\"\nsemantic_model:\n - name: billing\n description: Subscription billing model.\n datasets:\n - name: invoices\n source: billing.invoices\n primary_key: invoice_id\n ai_context:\n synonyms: [bills, statements]\n fields:\n - name: invoice_id\n - name: issued_at\n dimension:\n is_time: true\n - name: amount_usd\n description: Invoice total in USD.\n - name: accounts\n source: billing.accounts\n primary_key: account_id\n fields:\n - name: account_id\n relationships:\n - name: invoice_account\n from: invoices\n to: accounts\n from_columns: [account_id]\n to_columns: [account_id]\n metrics:\n - name: mrr\n description: Monthly recurring revenue.\n expression:\n dialects:\n - dialect: ANSI_SQL\n expression: SUM(invoices.amount_usd)",
"mappingKind": "inferred"
}
The response
The tool returns one JSON object with three payloads: the produced schema (a string for text formats), the executed plan, and the lossiness ledger. Abridged to the first dataset:
{
"success": true,
"schema": "{\n \"version\": \"0.1.1\",\n \"semantic_model\": [ ... ]\n}",
"plan": {
"operations": [
{
"kind": "TypeMapping",
"origin": "Inferred",
"sourceTypeId": "invoices",
"targetTypeId": "invoices",
"targetLabel": "invoices"
},
{
"kind": "ElementMapping",
"origin": "Inferred",
"sourceElementIds": ["invoicesIssuedAt"],
"targetElementIds": ["invoicesIssuedAt"]
}
]
},
"lossiness": [
{
"kind": "TypeApproximation",
"path": "$",
"explanation": "OSI carries no field type system; every field was decoded as String (or DateTime when dimension.is_time). One summarized approximation for the whole schema."
},
{
"kind": "StructuralDrop",
"path": "Relation[invoiceAccount]",
"explanation": "Source relation not carried by any relation-mapping operation."
}
]
}
Unescaped, the schema string opens like this — the ai_context synonyms survive, is_time becomes a real JSON boolean, and every field carries the expression the spec requires:
{
"version": "0.1.1",
"semantic_model": [
{
"name": "billing",
"description": "Subscription billing model.",
"datasets": [
{
"name": "invoices",
"source": "billing.invoices",
"primary_key": ["invoice_id"],
"ai_context": { "synonyms": ["bills", "statements"] },
"fields": [
{
"name": "invoice_id",
"expression": { "dialects": [{ "dialect": "ANSI_SQL", "expression": "invoice_id" }] }
},
{
"name": "issued_at",
"expression": { "dialects": [{ "dialect": "ANSI_SQL", "expression": "issued_at" }] },
"dimension": { "is_time": true }
},
{
"name": "amount_usd",
"description": "Invoice total in USD.",
"expression": { "dialects": [{ "dialect": "ANSI_SQL", "expression": "amount_usd" }] }
}
]
}
]
}
]
}
What the agent should report
Not "done" — the ledger has two entries, and both belong in the answer.
The TypeApproximation is structural to the format: an Ossie field has no type slot, so field types are neither read nor written. Nothing was lost that the document ever carried.
The StructuralDrop on Relation[invoiceAccount] is different: the source did carry an invoice_account relationship, and it is not in the output. An inferred plan aligns types, elements, and taxonomies by label; it emits no relation operations, and the engine says so rather than quietly dropping the edge. The mrr metric, which rides as a component, is absent from the output for the same reason — though a component not carried by the plan earns no ledger entry of its own, so that one the agent confirms by checking the output's metrics block.
If the relationship and the metric must survive, the plan the tool returned is the lever. Add two operations to it —
{ "kind": "RelationMapping", "origin": "Explicit",
"sourceRelationId": "invoiceAccount", "targetRelationGroupId": "osiRelationship" }
{ "kind": "ComponentMapping", "origin": "Explicit",
"sourceComponentId": "mrr", "targetComponentId": "mrr" }
— and replay the edited plan through POST graph/transform/plan/execute/{projectId} on the HTTP surface, which puts it through the identical validation gate. The output then carries the relationships and metrics blocks, and the relation drop disappears from the ledger. We verified that round trip; the ids in the operations are the decoder's sanitized ids, which the plan already shows you.
The reverse direction
The same tool, arguments swapped, turns any decodable schema into an Ossie model. Feeding a small JSON Schema (invoice_id required, issued_at a date-time string, amount_usd a number) with "sourceFormat": "jsonschema" and "targetFormat": "osi" returns:
version: "0.1.1"
semantic_model:
- name: "Invoice"
datasets:
- name: "Invoice"
source: "Invoice"
fields:
- name: "invoice_id"
expression:
dialects:
- dialect: "ANSI_SQL"
expression: "invoice_id"
- name: "issued_at"
expression:
dialects:
- dialect: "ANSI_SQL"
expression: "issued_at"
dimension:
is_time: true
- name: "amount_usd"
expression:
dialects:
- dialect: "ANSI_SQL"
expression: "amount_usd"
The date-time format became a time dimension; the numeric type had nowhere to go, because Ossie fields do not carry types. Notice also that required: ["invoice_id"] produced no primary_key — key membership is what makes an Ossie field required, and requiredness alone is not evidence of a key.
Failure modes and the ai option
Errors arrive as text results, not exceptions: a bad document comes back as Could not decode the source schema: $: The document is not valid YAML: ..., a mistyped strategy as Unknown mappingKind 'guessed'. Use: inferred | explicit | ai., and explicit without a guide as mappingKind=explicit requires 'guide' (SIA mapping-guide JSON).
mappingKind: "ai" is available and honestly labeled: it asks a server-side Claude proposer for a plan, and that plan faces the same gate as every other strategy, with at most one repair attempt. It requires a server-configured Anthropic key and Editor or Admin membership on the project, and it sends the schema content to the Anthropic API server-side — which is why the tool declares an open-world hint. For a pure format conversion like this one, inferred is the right tool and costs nothing.
Full tool list, roles, and connection details are in the CoreModels MCP docs.