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

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

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

| Argument | Meaning |
|---|---|
| `graphProjectId` | 32-hex project id; scopes authorization only, nothing is read or written |
| `sourceFormat` | `jsonschema \| shex \| avro \| jsonld \| sql \| osi \| osi-json \| owl \| linkml \| protobuf \| odcs \| odm` |
| `sourceSchema` | the source schema text |
| `targetFormat` | `jsonschema \| shex \| avro \| jsonld \| sql \| osi \| osi-json \| owl \| linkml \| protobuf \| odcs \| synapse` |
| `vendor` | for SQL output: `postgres` (default), `mysql`, `sqlserver` |
| `synapseOrg`, `synapseName`, `synapseVersion` | the `$id` parts for Synapse output |
| `targetHintFormat`, `targetHintSchema` | the schema to map toward - required for inferred mapping |
| `mappingKind` | `inferred` (default), `explicit`, `ai` |
| `guide` | explicit: the mapping guide as JSON text; ai: free-text guidance |
| `caseInsensitive` | inferred 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:

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

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

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

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