Handing a cBioPortal study to an agent: governance over MCP
"Check whether the study files on this branch still match what we govern, and tell me what changed."
Handing a cBioPortal study to an agent: governance over MCP
"Check whether the study files on this branch still match what we govern, and tell me what changed."
That sentence is a complete task specification for an AI agent connected to CoreModels over the Model Context Protocol. The agent needs no client library, no schema parser, and no copy of your governance rules — it needs four tools, three artifact names, and a project id. This article shows the exact calls, the exact argument names, and the response shapes an agent actually sees, plus the two places where the MCP surface deliberately differs from the HTTP one.
The two endpoints
CoreModels serves MCP over stateless streamable HTTP at two paths:
https://coremodels.example.com/mcp— the public endpoint. Only Viewer-role tools are listed or executable here.audit_vendor_project,generate_vendor_artifacts, andget_vendor_integration_statusall run at Viewer role, so the entire read side of cBioPortal governance is available on the public endpoint.https://coremodels.example.com/mcp-admin— the admin endpoint, which additionally serves the write tools, includingimport_vendor_project(Admin role).
Authorization is OAuth 2.0 with RFC 9728 protected-resource metadata, Dynamic Client Registration,
and PKCE (S256). A spec-compliant client discovers all of it on its own: an unauthenticated request
gets a 401 with a WWW-Authenticate header pointing at the resource metadata, and the client
takes it from there. Adding the server from a terminal client is one line.
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
The authorization boundary that actually matters is not which endpoint minted a token — it is the
per-project role check. Write tools require Editor or Admin membership on the project regardless of
endpoint, and every tool declares readOnlyHint plus, where relevant, destructive, idempotent, and
open-world annotations in tools/list, so a client can reason about a call before making it.
Step 1 — orient with get_vendor_integration_status
Before touching artifacts, an agent should confirm what the project already knows. Called without a
vendor, this tool lists every registered connector with its capabilities and expected artifacts;
called with one, it returns the project's last-import state.
{
"name": "get_vendor_integration_status",
"arguments": {
"graphProjectId": "9d41c2b7e85f4a63b0d7c1e58f2a6b04",
"vendor": "cbioportal"
}
}
{
"vendor": "cbioportal",
"imported": true,
"state": {
"vendor": "cbioportal",
"projectName": "example_brca_2026",
"importedAt": "2026-08-03T09:14:22.7431180+00:00",
"sourceFingerprint": "c4a71f2e9b380d56",
"counts": "fieldsAdded=0, lineageAdded=0, lineageSkipped=0, nodesEnriched=10",
"facts": "{\"instruments\":\"2\",\"attributes\":\"8\",\"cancerType\":\"brca\",\"studyId\":\"example_brca_2026\",\"studyName\":\"Example BRCA Cohort 2026\"}"
},
"governedDatasets": 2
}
One property of MCP payloads is worth building into your agent's prompt: they are serialized
compactly in camelCase with default values omitted. Null strings, false booleans, and — this is
the one that bites — zero-valued integers do not appear at all. An agent must read a missing
errorCount as zero, not as "unknown". Empty collections still serialize as [].
Every argument schema is closed (additionalProperties: false) and graphProjectId is validated
against the pattern ^[a-f0-9]{32}$, so a mistyped id fails fast instead of running against the
wrong project.
Step 2 — audit with audit_vendor_project
This is the Schema Audit: fresh artifacts against the governed graph, read-only, reporting coverage,
drift, and conformance. Required arguments are graphProjectId and vendor. Artifacts arrive
inline through artifacts (name to raw content), through artifactUrls (name to URL), or both
merged. spaces is optional.
{
"name": "audit_vendor_project",
"arguments": {
"graphProjectId": "9d41c2b7e85f4a63b0d7c1e58f2a6b04",
"vendor": "cbioportal",
"artifacts": {
"clinical_patient": "#Patient Identifier\tAge\tSex\tOverall Survival Status\n#Unique patient identifier\tAge at diagnosis in years\tSex at birth\n#STRING\tSTRING\tSTRING\tSTRING\n#1\t1\t1\t2\nPATIENT_ID\tAGE\tSEX\tOS_STATUS\n",
"clinical_sample": "#Patient Identifier\tSample Identifier\tCancer Type\tTumor Purity\n#Patient this sample belongs to\tUnique sample identifier\tOncoTree cancer type\tEstimated tumor purity percentage\n#STRING\tSTRING\tSTRING\tNUMBER\n#1\t1\t1\t3\nPATIENT_ID\tSAMPLE_ID\tcancerType\tTUMOR_PURITY\n"
}
}
}
Note what changed in that patient file relative to what the project governs: the datatype row now
reads STRING where AGE used to be NUMBER. Here is what comes back:
{
"vendor": "cbioportal",
"projectName": "cbioportal-study",
"errorCount": 1,
"warningCount": 1,
"infoCount": 1,
"metrics": {
"Datasets (estate)": "2",
"Datasets governed": "2 / 2",
"Fields governed": "8 / 8",
"Governed nodes with canonical mappings": "0 / 10 (0%)",
"Last import": "2026-08-03T09:14:22.7431180+00:00"
},
"findings": [
{
"section": "Drift",
"severity": "Error",
"code": "field-type-drift",
"subject": "patient.AGE",
"message": "Field type changed since the last import.",
"detail": "governed: NUMBER, estate: STRING"
},
{
"section": "Conformance",
"severity": "Info",
"code": "attribute-no-description",
"subject": "patient",
"message": "1 clinical attribute(s) carry no description row — curators downstream will guess.",
"detail": "OS_STATUS"
},
{
"section": "Conformance",
"severity": "Warning",
"code": "attribute-id-not-upper",
"subject": "sample.cancerType",
"message": "Attribute IDs must be UPPER_CASE for cBioPortal validation to pass."
}
],
"markdown": "# cbioportal Schema Audit — cbioportal-study\n\n🔴 **1 errors · 1 warnings · 1 info**\n…",
"fetchProblems": []
}
Two things to teach the agent. projectName is cbioportal-study because this call sent no meta
artifact — the study identifier comes from cancer_study_identifier in meta_study.txt, and
without it the connector uses a neutral default rather than guessing. And markdown is what belongs
in a review comment: it already carries the verdict line, the metrics table, and per-section
collapsible blocks, so the agent should not re-render findings itself.
Unlike the HTTP audit route, this tool has no recordHistory argument. Agent-driven audits are
strictly read-only; appending to the rolling trail stays with the HTTP surfaces that CI and humans
call.
The result shape is also trimmed for a conversational client. It carries the counts, the metrics, the
full findings list, the markdown, and fetchProblems — but not the codes rollup, the
driftedObjects list, the artifact fingerprint, or the lossiness channel that the HTTP report
includes. An agent that needs a code histogram counts the findings itself; an agent that needs the
fingerprint calls the HTTP route.
Step 3 — the artifactUrls flow
Clinical staging headers are small, but agents frequently cannot inline file contents at all —
either the client truncates large tool arguments or the files are not on the agent's disk.
artifactUrls solves that: the server fetches each artifact itself.
{
"name": "audit_vendor_project",
"arguments": {
"graphProjectId": "9d41c2b7e85f4a63b0d7c1e58f2a6b04",
"vendor": "cbioportal",
"artifactUrls": {
"clinical_patient": "https://files.example.org/studies/example_brca_2026/data_clinical_patient.txt",
"clinical_sample": "https://files.example.org/studies/example_brca_2026/data_clinical_sample.txt",
"meta": "https://files.example.org/studies/example_brca_2026/meta_study.txt"
}
}
}
Those fetches are server-side GETs to caller-supplied URLs, so they are guarded rather than trusted. HTTPS only. Redirects disabled, because a redirect to an internal host would defeat any pre-flight check. Hosts that resolve to loopback, link-local (including cloud metadata addresses), or private ranges are refused. The response is size-capped and the fetch times out after a minute.
Failures are reported, never silently swallowed. A refused or unreachable URL adds a line to
fetchProblems while the audit proceeds on whatever was retrieved:
{
"fetchProblems": [
"Refused to fetch 'meta' from https://10.0.0.7/meta_study.txt: host resolves to a non-public address (10.0.0.7)"
]
}
If nothing at all is usable — no inline artifacts, no fetchable URLs — the tool returns an error
naming the arguments it needs. That generic message is phrased around dbt's artifact names, so
remember that for this connector the names are clinical_patient, clinical_sample, and meta,
with at least one clinical file required. You can also mix the two maps: inline the small meta
file and fetch the clinical files by URL. Both merge into one artifact set.
Step 4 — writing, on the admin endpoint
Bringing a new study under governance is an Admin act, and import_vendor_project is served only on
/mcp-admin. Its arguments are identical to the audit tool's — same artifacts and artifactUrls
handling, same URL guards.
{
"name": "import_vendor_project",
"arguments": {
"graphProjectId": "9d41c2b7e85f4a63b0d7c1e58f2a6b04",
"vendor": "cbioportal",
"artifactUrls": {
"clinical_patient": "https://files.example.org/studies/example_brca_2026/data_clinical_patient.txt",
"clinical_sample": "https://files.example.org/studies/example_brca_2026/data_clinical_sample.txt",
"meta": "https://files.example.org/studies/example_brca_2026/meta_study.txt"
}
}
}
{
"success": true,
"vendor": "cbioportal",
"projectName": "example_brca_2026",
"datasetsAdded": 2,
"nodesEnriched": 10,
"lossiness": [
{
"kind": "TypeApproximation",
"path": "patient.AGE",
"explanation": "Native type 'NUMBER' was approximated as Double; the exact native type is preserved in the vendor metadata mixin."
},
{
"kind": "TypeApproximation",
"path": "sample.TUMOR_PURITY",
"explanation": "Native type 'NUMBER' was approximated as Double; the exact native type is preserved in the vendor metadata mixin."
}
],
"errors": [],
"fetchProblems": []
}
datasetsSkippedExisting, fieldsAdded, and both lineage counts are absent from that payload
because they are zero — the compact-serialization rule again. The import is additive and never
mutates governed nodes, so an agent that re-imports a study it already imported is safe: it will see
datasetsSkippedExisting appear and datasetsAdded disappear.
One field the HTTP import returns is not in the tool's result at all: snapshotStored. The snapshot
is still persisted exactly as it would be over HTTP — it is simply not reported back here. An agent
that needs to confirm a re-audit baseline exists should check the HTTP import response, or try the
re-audit route and read its refusal message.
Step 5 — generating scaffolds back out
generate_vendor_artifacts runs at Viewer role and is therefore on the public endpoint. For
cBioPortal it emits one data_clinical_{instrument}.txt header scaffold per governed Type.
{
"name": "generate_vendor_artifacts",
"arguments": {
"graphProjectId": "9d41c2b7e85f4a63b0d7c1e58f2a6b04",
"vendor": "cbioportal",
"typeNames": ["patient"]
}
}
{
"artifacts": [
{
"name": "data_clinical_patient.txt",
"kind": "tsv",
"content": "#PATIENT_ID\tAGE\tSEX\tOS_STATUS\n#Unique patient identifier\tAge at diagnosis in years\tSex at birth\tOS_STATUS\n#STRING\tNUMBER\tSTRING\tSTRING\n#1\t1\t1\t1\nPATIENT_ID\tAGE\tSEX\tOS_STATUS\n"
}
],
"lossiness": []
}
typeNames matters more than it looks: generation walks every governed Type in the project, not
only the ones cBioPortal imported. Instruct agents to scope it in any project that governs more than
one estate. targetVersion exists on the schema for vendors with dialect versions; cBioPortal
ignores it.
What is not on MCP
Three verbs from this series' HTTP surface have no MCP tool: reaudit, history, and badge
(the sync-plan routes are HTTP-only too). That is a deliberate boundary — the drift trail and its
badge are automation bookkeeping, driven by CI pipelines and scheduled jobs rather than by
conversational agents. An agent that needs the trail
calls the HTTP route or asks a human to.
Everything else composes normally. After an audit, an agent can call get_project_summary for the
Types and Elements in the project, search_nodes to inspect one attribute, or the schema export
tools to hand the governed study to another system. The audit tells it what drifted; the read tools
tell it what the drifted thing means.
For the artifact contract behind all of this, see the cBioPortal quickstart in the CoreModels integration docs.