# Azure Synapse — MCP: Synapse Schemas from an Agent: transform_schema over MCP, End to End

Connect an MCP client - Claude Desktop, an IDE agent, your own orchestration - to a CoreModels deployment at `https://coremodels.example.com/mcp` (OAuth-protected), and two tools give it the entire Synapse workflow: `transform_schema` produces the registration-ready draft-07 schema, and `generate_synapse_manifests` produces the tabular curation companions. This article is the end-to-end session, with exact arguments and exact results, so an agent - or the person supervising one - knows precisely what each call does and does not do.

# Synapse Schemas from an Agent: transform_schema over MCP, End to End

Connect an MCP client - Claude Desktop, an IDE agent, your own orchestration - to a CoreModels deployment at `https://coremodels.example.com/mcp` (OAuth-protected), and two tools give it the entire Synapse workflow: `transform_schema` produces the registration-ready draft-07 schema, and `generate_synapse_manifests` produces the tabular curation companions. This article is the end-to-end session, with exact arguments and exact results, so an agent - or the person supervising one - knows precisely what each call does and does not do.

The capability boundary first, because an agent should never have to discover it by trial: in `transform_schema`, `synapse` is a **target format only**. The tool's own description says so, and passing it as `sourceFormat` returns an error directing you to `jsonschema` - the output *is* plain draft-07 JSON Schema, so nothing is lost by the asymmetry.

## Call 1: transform_schema

`transform_schema` is the stateless mapping engine over MCP: decode the source, produce and validate a plan, execute deterministically, encode the target. Unlike the REST mapping routes, it accepts the three Synapse identity segments directly - which makes MCP the shortest path from "schema in some format" to "schema with the `$id` I actually intend to register".

The exact call (tool name and `arguments` object as your client sends them):

```json
{
  "name": "transform_schema",
  "arguments": {
    "graphProjectId": "3f9c2a7d41e64b0f8c5d9e1a2b3c4d5e",
    "sourceFormat": "jsonschema",
    "sourceSchema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Observation\",\"type\":\"object\",\"properties\":{\"observationId\":{\"type\":\"string\",\"description\":\"Stable identifier for the observation.\",\"$comment\":\"assigned by the intake pipeline\"},\"status\":{\"type\":\"string\"},\"score\":{\"type\":\"number\",\"minimum\":0,\"exclusiveMaximum\":100},\"notes\":{\"type\":\"string\",\"maxLength\":2000}},\"required\":[\"observationId\",\"status\"]}",
    "targetFormat": "synapse",
    "synapseOrg": "myorg.dcc",
    "synapseName": "experimentalData.observation",
    "synapseVersion": "0.0.1",
    "targetHintFormat": "jsonschema",
    "targetHintSchema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Observation\",\"type\":\"object\",\"properties\":{\"observationId\":{\"type\":\"string\"},\"status\":{\"type\":\"string\"},\"score\":{\"type\":\"number\"},\"notes\":{\"type\":\"string\"}},\"required\":[\"observationId\",\"status\"]}",
    "mappingKind": "inferred"
  }
}
```

Argument notes an agent needs to get right:

- **`graphProjectId`** - 32 lowercase hex characters; it scopes authorization only. Nothing is read from or written to the project by this tool.
- **`sourceSchema` / `targetHintSchema`** - always strings, JSON-escaped when the format itself is JSON.
- **`mappingKind`** - `inferred` (default) matches labels against the hint; `explicit` takes a SIA mapping-guide JSON in `guide`; `ai` asks a server-side model to propose the plan. For plain format conversion, `inferred` with the source (or a slimmed copy of it, as above) as its own hint is the pattern.
- **`synapseOrg` / `synapseName` / `synapseVersion`** - compose the registered-schema `$id`. Defaults are `example`, the root-type name, and `0.0.1`; `org.sagebionetworks` is reserved; the version must be plain `major.minor.patch`, and Synapse will refuse to re-register an existing version, so bump on change.

## What comes back

The tool returns one JSON payload with `success`, `schema`, `plan`, and `lossiness`:

```json
{
  "success": true,
  "schema": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "https://repo-prod.prod.sagebase.org/repo/v1/schema/type/registered/myorg.dcc-experimentalData.observation-0.0.1",
    "type": "object",
    "properties": {
      "observationId": {
        "type": "string",
        "description": "Stable identifier for the observation."
      },
      "status": { "type": "string" },
      "score": { "type": "number", "minimum": 0 },
      "notes": { "type": "string", "maxLength": 2000 }
    },
    "required": ["observationId", "status"],
    "title": "Observation"
  },
  "plan": { "operations": [ { "kind": "TypeMapping", "origin": "Inferred", "sourceTypeId": "Observation", "targetTypeId": "Observation" } ] },
  "lossiness": [
    {
      "kind": "StructuralDrop",
      "path": "#/properties/observationId/$comment",
      "explanation": "'$comment' is not a field of the Synapse JsonSchema object; stripped."
    },
    {
      "kind": "ConstraintRelaxation",
      "path": "#/properties/score/exclusiveMaximum",
      "explanation": "'exclusiveMaximum' is not a field of the Synapse JsonSchema object; the constraint was stripped and is no longer enforced."
    }
  ]
}
```

(The `plan` is shown abbreviated to its first operation; the real one carries an element mapping per property.) Three things an agent should be instructed to check, in order: `success`; then every `lossiness` entry of kind `ConstraintRelaxation` - here the upper bound on `score` no longer exists, and *no error will ever say so again after this response*; then the `$id`, confirming the three segments came through rather than falling back to defaults. `description`, `maxLength`, `minimum`, and the `required` array all survived, because they are fields the Synapse `JsonSchema` object models.

## Call 2: generate_synapse_manifests

A Synapse schema rarely ships alone - curators work from a tabular manifest. `generate_synapse_manifests` produces those CSVs from the same source, so the grid and dictionary can never drift from the schema they describe. It is stateless and read-only: the project scopes auth, nothing is written, no external call is made.

```json
{
  "name": "generate_synapse_manifests",
  "arguments": {
    "graphProjectId": "3f9c2a7d41e64b0f8c5d9e1a2b3c4d5e",
    "sourceFormat": "jsonschema",
    "sourceSchema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Observation\",\"type\":\"object\",\"properties\":{\"observationId\":{\"type\":\"string\",\"description\":\"Stable identifier for the observation.\"},\"status\":{\"type\":\"string\",\"enum\":[\"registered\",\"preliminary\",\"final\"]},\"score\":{\"type\":\"number\"},\"notes\":{\"type\":\"string\"}},\"required\":[\"observationId\",\"status\"]}"
  }
}
```

Two artifacts come back per type, as `{ name, kind, content }` entries in an `artifacts` array:

```json
{
  "success": true,
  "artifacts": [
    {
      "name": "Observation.manifest.csv",
      "kind": "csv",
      "content": "\"observationId\",\"status\",\"score\",\"notes\"\r\n"
    },
    {
      "name": "Observation.dictionary.csv",
      "kind": "csv",
      "content": "Column,Type,Description,Required,Value Set\r\n\"observationId\",\"string\",\"Stable identifier for the observation.\",\"TRUE\",\"\"\r\n\"status\",\"string\",\"\",\"TRUE\",\"Observation::status::enum\"\r\n\"score\",\"number\",\"\",\"FALSE\",\"\"\r\n\"notes\",\"string\",\"\",\"FALSE\",\"\"\r\n\r\nValue Set,Value,Label,Parent\r\n\"Observation::status::enum\",\"registered\",\"registered\",\"\"\r\n\"Observation::status::enum\",\"preliminary\",\"preliminary\",\"\"\r\n\"Observation::status::enum\",\"final\",\"final\",\"\"\r\n"
    }
  ],
  "lossiness": []
}
```

The manifest is the blank entry grid - header row only, one column per element, directly loadable as a curator record-set CSV. The dictionary lists each column's type, description, required flag, and driving value set, then a value-set section with one row per vocabulary term - including a `Parent` column, which is where a term hierarchy survives after the schema's flat `enum` had to discard it. (The generated value-set name here, `Observation::status::enum`, is the identifier minted when an inline enum is decoded; vocabularies that arrive with names - from a governed CoreModels project, for instance - keep them.) An optional `typeId` argument restricts generation to one type, matched by id or label; source formats are the decodable set, which naturally includes `odm` and excludes `synapse`.

## The `ai` mapping kind, disclosed properly

`mappingKind: "ai"` has two gates an agent will hit honestly rather than mysteriously. It requires **Editor or Admin** membership on the scoping project - Viewer is not enough, because the call spends the server's paid Anthropic budget - and a server-configured Anthropic key, without which the tool declines and suggests `explicit` or `inferred`. It is also flagged open-world in the tool's own annotations: the source and target schemas are sent to the external Anthropic API server-side. The proposed plan earns no shortcut - it passes the identical validation gate as every other strategy.

## Failure modes an agent will meet

All failures arrive as tool errors with actionable text, never as opaque protocol faults - worth wiring into an agent's retry logic verbatim:

- **`sourceFormat: "synapse"`** - declined with the redirect: `'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema - decode it with the 'jsonschema' format.` The correct retry is a format swap, not a payload change.
- **`inferred` without a hint** - `Could not produce a mapping plan: inference: The inference resolver requires a target IR to match against.` Supply `targetHintFormat` and `targetHintSchema`; for pure conversion, echo the source.
- **`explicit` without a guide** - `mappingKind=explicit requires 'guide' (SIA mapping-guide JSON).` An unknown key inside the guide is likewise rejected with a path-carrying error rather than silently ignored.
- **An unknown `mappingKind`** - the error enumerates the three valid values, so no retry loop needs to guess.
- **A malformed `sourceSchema`** - parse failures surface as `Could not parse the '…' schema` with the underlying reason, before any mapping work begins.

## The pattern worth copying

One source of truth, two generated artifacts, one ledger. An agent that runs `transform_schema` and `generate_synapse_manifests` back to back from the same `sourceSchema` hands a human three reviewable things: a schema whose `$id` is exactly what will be registered, CSVs a curator can open immediately, and a lossiness list that says - with paths - what the Synapse subset could not hold. The next article turns that pattern into an unattended pipeline.
