Open Semantic Interchange logoMCP

"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.

ArgumentRequiredMeaning for Ossie work
graphProjectIdyes32-char hex project id (^[a-f0-9]{32}$) — scopes authorization only
sourceFormatyesosi or osi-json reading in; both accept YAML or JSON, since JSON is a YAML subset
sourceSchemayesthe document text
targetFormatyesosi writes YAML, osi-json writes JSON; also jsonschema, shex, avro, jsonld, sql, owl, linkml, protobuf, odcs, synapse
targetHintFormat / targetHintSchemafor inferredthe schema to map toward; for a straight conversion, pass the source again
mappingKindnoinferred (default) | explicit | ai
guidefor explicitthe SIA mapping-guide JSON as text; free-text guidance when mappingKind is ai
caseInsensitivenoinferred matching ignores label case (default true)
vendornoSQL output only: postgres (default) | mysql | sqlserver
synapseOrg, synapseName, synapseVersionnoSynapse 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.