JSON Schema logoMCP

Give an Agent a JSON Schema and a Target: transform_schema Over MCP

An agent working on your repository will find a `.schema.json` file long before it finds your conversion documentation. What happens next depends entirely on the tools it can reach. Without one, it writes a converter, or worse, writes the target schema from scratch and presents its guesses with the same confidence as facts.

Give an Agent a JSON Schema and a Target: transform_schema Over MCP

An agent working on your repository will find a .schema.json file long before it finds your conversion documentation. What happens next depends entirely on the tools it can reach. Without one, it writes a converter, or worse, writes the target schema from scratch and presents its guesses with the same confidence as facts.

CoreModels exposes the conversion itself as an MCP tool, transform_schema, with a declared argument schema, a fixed vocabulary of format keys, and a response that separates what was produced from what was lost. This article covers that tool for JSON Schema (jsonschema, alias sia): the exact arguments, a two-step agent scenario, the failure modes, and the neighboring tools worth knowing.

Connecting

The MCP endpoint is /mcp, over stateless streamable HTTP with OAuth 2.0. Dynamic client registration and PKCE are supported, so there is no client id to obtain up front — an unauthenticated request answers with a pointer to the resource metadata and a compliant client completes the flow on its own.

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

Then run /mcp inside the client to complete the authorization. For a generic JSON-configured client:

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

/mcp serves the read-only tools, which is where transform_schema lives — it is stateless and writes nothing. A second endpoint, /mcp-admin, additionally serves the write tools. Whichever endpoint a token came from, the enforced boundary is the per-project role check.

The contract the agent sees

transform_schema declares its arguments and refuses anything else (additionalProperties: false). Four are required: graphProjectId, sourceFormat, sourceSchema, targetFormat.

ArgumentMeaning
graphProjectId32-hex project id; scopes authorization only, nothing is read or written
sourceFormatjsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm
sourceSchemathe source schema text
targetFormatjsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse
vendorfor SQL output: postgres (default), mysql, sqlserver
synapseOrg, synapseName, synapseVersionthe $id parts for Synapse output
targetHintFormat, targetHintSchemathe schema to map toward — required for inferred mapping
mappingKindinferred (default), explicit, ai
guideexplicit: the mapping guide as JSON text; ai: free-text guidance
caseInsensitiveinferred only: ignore label case when matching (default true)

Two of those lines carry more weight than their size suggests. jsonschema appears in both format lists, so it can be the source, the target, or the hint. And the hint is not decoration: with mappingKind: inferred and no hint, the call comes back with Could not produce a mapping plan: inference: The inference resolver requires a target IR to match against. When you are converting a schema rather than reconciling two, pass the source as its own hint.

Scenario: one schema, two teams

Take a schema an agent found in the repository:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Customer",
  "type": "object",
  "properties": {
    "customerKey": { "type": "integer" },
    "fullName": { "type": "string" },
    "signupDate": { "type": "string", "format": "date-time" },
    "loyaltyPoints": { "type": "integer" }
  },
  "required": ["customerKey", "fullName"]
}

The streaming team wants a proto3 definition; the warehouse team wants Postgres DDL. Two tool calls, differing in one argument.

Call one — arguments as the agent sends them:

{
  "graphProjectId": "0123456789abcdef0123456789abcdef",
  "sourceFormat": "jsonschema",
  "sourceSchema": "{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"title\":\"Customer\",\"type\":\"object\",\"properties\":{\"customerKey\":{\"type\":\"integer\"},\"fullName\":{\"type\":\"string\"},\"signupDate\":{\"type\":\"string\",\"format\":\"date-time\"},\"loyaltyPoints\":{\"type\":\"integer\"}},\"required\":[\"customerKey\",\"fullName\"]}",
  "targetFormat": "protobuf",
  "targetHintFormat": "jsonschema",
  "targetHintSchema": "{\"title\":\"Customer\",\"type\":\"object\",\"properties\":{\"customerKey\":{\"type\":\"integer\"},\"fullName\":{\"type\":\"string\"},\"signupDate\":{\"type\":\"string\",\"format\":\"date-time\"},\"loyaltyPoints\":{\"type\":\"integer\"}},\"required\":[\"customerKey\",\"fullName\"]}",
  "mappingKind": "inferred"
}

The tool returns one JSON document with four keys — success, schema, plan, lossiness:

{
  "success": true,
  "schema": "syntax = \"proto3\";\n\nmessage Customer {\n  int64 customerKey = 1;\n  string fullName = 2;\n  optional string signupDate = 3;\n  optional int64 loyaltyPoints = 4;\n}\n",
  "plan": {
    "operations": [
      {
        "kind": "TypeMapping",
        "origin": "Inferred",
        "sourceTypeId": "Customer",
        "targetTypeId": "Customer",
        "targetLabel": "Customer"
      },
      {
        "kind": "ElementMapping",
        "origin": "Inferred",
        "sourceElementIds": ["Customer::signupDate"],
        "targetElementIds": ["Customer::signupDate"]
      }
    ]
  },
  "lossiness": [
    {
      "kind": "TypeApproximation",
      "path": "Element[Customer::signupDate]",
      "explanation": "proto3 has no date-time scalar; DateTime degrades to string."
    }
  ]
}

(Five operations came back; two are shown.) Text formats arrive as a string in schema; JSON-shaped targets such as avro, jsonld, synapse, and jsonschema arrive as a JSON object.

The lossiness entry is the part an agent should never summarize away. Two required fields became bare proto3 scalars, the two optional ones picked up the optional keyword, and the date-time became a plain string — stated, with the element path attached.

Call two — the same source with "targetFormat": "sql" and "vendor": "postgres":

CREATE TABLE "Customer" (
  "customerKey" INTEGER NOT NULL,
  "fullName" VARCHAR(255) NOT NULL,
  "signupDate" TIMESTAMP,
  "loyaltyPoints" INTEGER
);

Empty ledger this time: Postgres has a timestamp type and a not-null constraint, so nothing needed approximating. That contrast is the useful signal — the same source, two targets, and the honest difference between them is visible without reading either output closely.

The three mapping kinds, from the agent's seat

inferred matches by label and type against the hint, and it is deliberately conservative: two properties with the same name but incompatible primitive types are not matched, and the unmatched source element is reported as a StructuralDrop rather than silently retyped. caseInsensitive defaults to true; set it to false when case is meaningful in your naming.

explicit takes a guide string — a mapping guide JSON whose only recognized keys are autoMatchByMapsTo, fieldMappings, taxonomyDirectives, and drops. Anything else is refused with the path (Unknown mapping-guide key 'fieldMapings'…), which is exactly what you want when a model is generating the guide: a typo fails loudly rather than executing a plan nobody intended. This is also how an enum is carried through a mapping — a taxonomyDirective naming the controlled list.

ai asks a server-side Claude proposer for a plan. Three things are true about it and should be said to whoever approves the call: it requires Editor or Admin membership on the scoping project, it requires a server-configured Anthropic key, and it sends the schema content to the Anthropic API server-side — which is why the tool is annotated as open-world. The proposal earns no shortcut: it passes the same validation gate as every other plan, with at most one repair attempt, and a rejected repair is a rejection. A self-reported confidence may appear on operations; it orders human review and nothing else.

Without the membership, the agent gets mappingKind=ai requires Editor or Admin project membership (it invokes the server-side Anthropic API): …. Without the key, mappingKind=ai requires a server-configured Anthropic API key (Transform:Anthropic:ApiKey or ANTHROPIC_API_KEY). Use 'explicit' or 'inferred' instead.

Failures that read like sentences

Because the tool returns text errors rather than stack traces, a capable agent can usually fix its own call:

  • Could not decode the source schema: $: The root of a JSON Schema must be an object.
  • mappingKind=explicit requires 'guide' (SIA mapping-guide JSON).
  • Unknown mappingKind 'auto'. Use: inferred | explicit | ai.
  • The plan was rejected by the validation gate: …
  • 'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema — decode it with the 'jsonschema' format.

The neighbors

transform_schema is stateless. When the schema should live in a project rather than pass through, other tools apply:

  • export_jsonschema — the project as a JSON Schema string; takes graphProjectId and optionally spaceId, configTypeId (the export profile), and rootNodeId.
  • validate_json — validates a JSON document against the project's stored schema; requires graphProjectId, rootNodeId, and jsonString.
  • fetch_json_schema_import_profiles — lists a project's import and export profiles.
  • import_jsonschema — writes a JSON Schema into a space. It is an Admin tool served only on the admin endpoint, and it fetches the schema from a URL server-side.
  • generate_synapse_manifests — curation-manifest CSV templates from any decodable format, including JSON Schema.

One instruction worth adding

If you give an agent this tool, give it one habit as well: read the ledger and repeat it back before presenting the output. A successful call with a non-empty lossiness array is the normal case, not an anomaly. An agent that ends with "converted, with one type approximation: signupDate has no proto3 date-time scalar" is doing the job. One that ends with "done" has skipped the only part a reviewer cannot reconstruct.

For the full tool inventory, endpoint split, and connection details, see the MCP quickstart in the CoreModels documentation.