# ShEx — MCP: Agent-Driven ShEx Conversion with the transform_schema MCP Tool

"The ontology team published ShEx shapes for the product catalog. Give me the JSON Schema our validation service needs, and tell me exactly what didn't survive the trip."

# Agent-Driven ShEx Conversion with the transform_schema MCP Tool

"The ontology team published ShEx shapes for the product catalog. Give me the JSON Schema our validation service needs, and tell me exactly what didn't survive the trip."

That request - pasted into an AI assistant connected to CoreModels - is a one-tool-call job. CoreModels, by ARAMAI, ships a Model Context Protocol (MCP) server whose `transform_schema` tool runs the full mapping engine statelessly: decode the source format, produce a plan, validate it through the universal gate, execute deterministically, encode the target. ShEx (format key `shex`) works on both sides of that pipeline - as a source and as a target. This article walks the end-to-end flow an agent actually takes, with the exact arguments.

## Connecting

The MCP endpoint lives at `/mcp` and authenticates with OAuth; spec-compliant clients discover the authorization server and run the flow automatically, with no pre-registered client id needed. From Claude Code:

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

then run `/mcp` inside the session to complete OAuth. In Claude Desktop or claude.ai, add a custom connector pointing at the same URL. `/mcp` serves the read-only (Viewer-role) tools - which includes everything in this article. Write tools such as node CRUD live on the separate `/mcp-admin` endpoint; you do not need it here, because `transform_schema` writes nothing.

## The tool contract

`transform_schema` takes these arguments - the input schema declares `additionalProperties: false`, so names must match exactly:

- `graphProjectId` (required) - 32-char hex, pattern `^[a-f0-9]{32}$`. It scopes authorization only; the call is stateless.
- `sourceFormat` (required) - `jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm`.
- `sourceSchema` (required) - the source text. For ShEx: the ShExC document as a string.
- `targetFormat` (required) - `jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | synapse`.
- `targetHintFormat` / `targetHintSchema` - the schema to map toward; required for inferred mapping.
- `mappingKind` - `inferred` (default) | `explicit` | `ai`.
- `guide` - for `explicit`: the mapping-guide JSON as text; for `ai`: optional free-text guidance.
- `caseInsensitive` - inferred matching ignores label case (default `true`).
- `vendor` - only for `sql` output; `synapseOrg`/`synapseName`/`synapseVersion` - only for `synapse` output. ShEx itself needs no format-specific options.

## The scenario, step by step

The agent has the ontology team's shapes:

```text
PREFIX schema: <https://schema.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

schema:ProductShape {
  schema:name xsd:string ;
  schema:sku xsd:string ;
  schema:price xsd:decimal ? ;
}
```

and the app team's current JSON Schema, which it uses as the target hint so the inferred strategy has something to match labels against. The tool call it issues:

```json
{
  "graphProjectId": "0123456789abcdef0123456789abcdef",
  "sourceFormat": "shex",
  "sourceSchema": "PREFIX schema: <https://schema.org/>\nPREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n\nschema:ProductShape {\n  schema:name xsd:string ;\n  schema:sku xsd:string ;\n  schema:price xsd:decimal ? ;\n}",
  "targetFormat": "jsonschema",
  "targetHintFormat": "jsonschema",
  "targetHintSchema": "{ \"$id\": \"Product\", \"type\": \"object\", \"title\": \"Product\", \"properties\": { \"name\": { \"type\": \"string\" }, \"sku\": { \"type\": \"string\" }, \"price\": { \"type\": \"number\" } } }",
  "mappingKind": "inferred",
  "caseInsensitive": true
}
```

Why this lines up: on decode, the shape name `schema:ProductShape` becomes a type labeled `Product` (the `Shape` suffix is stripped), and each predicate's local name labels an element - `name`, `sku`, `price`. Those match the hint's `title` and property names one-to-one, and the types are compatible (`xsd:decimal` decodes to our Double kind; the hint's `number` does too). The inferred interpreter aligns them, and the plan it produces passes the same validation gate every strategy passes - inference earns no shortcut.

The tool returns one JSON document:

```jsonc
{
  "success": true,
  "schema": { ... },                    // the produced JSON Schema
  "plan": { "operations": [ ... ] },    // the executed plan - replayable
  "lossiness": [ ... ]                  // the honest ledger, possibly empty
}
```

Three things the agent does with it, in order. First, it hands `schema` to the app team - and inside it, the `name` property carries `"x-maps-to": { "schema": "https://schema.org/name" }`, because ShEx predicates are IRIs and the coder lifts them into cross-standard mappings automatically. Second, it reads `lossiness` aloud: each record is a `kind` (`StructuralDrop`, `TypeApproximation`, `ConstraintRelaxation`, or `SemanticNarrowing`), a `path`, and a plain-English `explanation` - which is precisely the "tell me what didn't survive" half of the user's request. Third, it saves `plan`: the same conversion can be replayed deterministically later, and a stored plan is revalidated by the same gate when it is.

## The reverse direction: shapes out of a governed project

ShEx is also a first-class *output*. Two routes, depending on where the schema lives:

If the source is another format in hand - say the app team's JSON Schema should become shapes for the RDF validation pipeline - the agent calls `transform_schema` again with the directions swapped: `sourceFormat: "jsonschema"`, `targetFormat: "shex"`, and a hint to map toward. Elements carrying `mapsTo` URIs come back as real prefixed predicates (`schema:name`); everything else is emitted deterministically under our `cm:` namespace.

If the source is the governed project itself, there is a dedicated read-only tool, `export_shex`, on the same `/mcp` endpoint:

```json
{
  "graphProjectId": "0123456789abcdef0123456789abcdef",
  "includeCardinality": true
}
```

Optional arguments narrow it: `spaceId` scopes to one space, `nodeIds` selects specific type nodes (omit it to export all types), and `includeCardinality` (default `true`) controls whether the ShEx cardinality markers `?`, `*`, `+` are emitted. The result is the project's schema as a ShExC string - a natural final step after `list_projects` and `get_project_summary` when an agent is asked "give me shapes for what we govern."

## When the agent reaches for mappingKind: ai

If labels do not line up and nobody has authored an explicit guide, the agent can set `"mappingKind": "ai"`, optionally with free-text `guide` hints. Know the rules before you let it: the proposal is generated server-side and then validated by the **same** gate as every other plan, with at most one repair attempt - a rejected repair is a rejection, and the gate is never relaxed. It requires a server-configured Anthropic API key and **Editor or Admin** membership on the scoping project (Viewer is not enough), and it sends the schema content to the Anthropic API server-side - the tool advertises this openly in its annotations. Without the key, the call is declined honestly with a pointer to `explicit` or `inferred`. Any self-reported confidence in the proposed plan is advisory only; the gate never trusts it.

## When it goes wrong

Failures come back as tool errors with the reason spelled out, which matters for agents because a plain-English error is a recoverable one. A bad format key returns the full list of valid keys to pick from. A ShExC document that will not parse returns `Could not parse the 'shex' schema:` with the underlying reason; a document the decoder rejects (an empty one, say) returns `Could not decode the source schema:` with the path and message attached, and the same pattern covers a broken target hint, a rejected mapping guide, a plan the gate refused, and an encode the target format could not complete. Nothing surfaces as an opaque failure - every stage of the pipeline reports in the same shape, so the agent's retry can actually address the cause.

That is the whole loop: one connector, one stateless tool for any-to-any conversion with ShEx on either end, one dedicated exporter for governed shapes, and a lossiness ledger that lets the agent answer the honest half of the question instead of hand-waving it. For the rest of the MCP tool inventory, see the CoreModels MCP quickstart in our docs.
