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):
{
"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;explicittakes a SIA mapping-guide JSON inguide;aiasks a server-side model to propose the plan. For plain format conversion,inferredwith 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 areexample, the root-type name, and0.0.1;org.sagebionetworksis reserved; the version must be plainmajor.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:
{
"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.
{
"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:
{
"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.inferredwithout a hint —Could not produce a mapping plan: inference: The inference resolver requires a target IR to match against.SupplytargetHintFormatandtargetHintSchema; for pure conversion, echo the source.explicitwithout 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 asCould not parse the '…' schemawith 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.