An Agent Runs Your Snowflake Governance: The MCP Workflow
"Is our Snowflake estate still in sync with the governed model? If not, show me exactly what drifted, and draft the DDL to fix it." That is a sentence you can now say to an AI agent, and every step it takes to answer — discovery, audit, report, generation — runs through the CoreModels MCP server as first-class tool calls. This article walks the whole flow: how an agent connects, which integration tools it gets, the exact arguments each takes, and how large metadata extracts travel via the `artifactUrls` path.
An Agent Runs Your Snowflake Governance: The MCP Workflow
"Is our Snowflake estate still in sync with the governed model? If not, show me exactly what drifted, and draft the DDL to fix it." That is a sentence you can now say to an AI agent, and every step it takes to answer — discovery, audit, report, generation — runs through the CoreModels MCP server as first-class tool calls. This article walks the whole flow: how an agent connects, which integration tools it gets, the exact arguments each takes, and how large metadata extracts travel via the artifactUrls path.
The important framing up front: the agent gets the same governance surface humans get, with the same role enforcement. Nothing about MCP relaxes the posture — audits stay read-only, imports stay Admin-gated and additive, and every tool declares honest annotations (readOnlyHint, destructiveHint, openWorldHint) so agent frameworks can reason about what they're invoking.
Connecting
CoreModels serves two MCP endpoints over stateless streamable HTTP, secured with OAuth 2.0 (dynamic client registration and PKCE — no pre-registered client id needed):
https://coremodels.example.com/mcp— the public endpoint: read-only, Viewer-role tools.https://coremodels.example.com/mcp-admin— everything, including write tools.
In Claude Desktop or claude.ai, add a custom connector pointing at /mcp and complete the OAuth flow. From Claude Code:
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
and for write access (imports), a second connection:
claude mcp add --transport http coremodels-admin https://coremodels.example.com/mcp-admin
The endpoint split is a serving decision, not the security boundary. The enforced boundary is the per-project role check: import_vendor_project requires Admin membership on the target project no matter which endpoint the token came through.
The integration tools
| Tool | Role | Endpoint(s) |
|---|---|---|
get_vendor_integration_status | Viewer | both |
audit_vendor_project | Viewer | both |
generate_vendor_artifacts | Viewer | both |
import_vendor_project | Admin | /mcp-admin only |
reconcile_vendor_projects | Admin | /mcp-admin only |
All of them take graphProjectId (the 32-character hex project id, validated against ^[a-f0-9]{32}$). The audit, import, and generate tools also take a vendor key; status takes it optionally; reconciliation names two vendors instead (vendorA, vendorB). For everything below, the key is "snowflake".
Step 1: discovery
An agent that doesn't yet know what's connected starts with status, no vendor argument:
{ "tool": "get_vendor_integration_status",
"arguments": { "graphProjectId": "0f8fad5bd9cb469fa16570867728950e" } }
That lists every registered connector with its capabilities and expected artifacts — for Snowflake: information_schema required, keys and object_dependencies optional. With the vendor named, the same tool returns the project's last-import state instead:
{ "tool": "get_vendor_integration_status",
"arguments": { "graphProjectId": "0f8fad5bd9cb469fa16570867728950e",
"vendor": "snowflake" } }
The result says whether an import exists, carries the state record (import time, artifact fingerprint, counts), and the governed dataset count. This is how an agent decides between "audit what's there" and "there's nothing here yet — ask a human for artifacts and an import."
Step 2: import (admin endpoint, Admin role)
The import call mirrors the HTTP surface — artifact name to raw content:
{ "tool": "import_vendor_project",
"arguments": {
"graphProjectId": "0f8fad5bd9cb469fa16570867728950e",
"vendor": "snowflake",
"artifacts": {
"information_schema": "<rows JSON>",
"keys": "<rows JSON>",
"object_dependencies": "<rows JSON>"
} } }
The tool reports datasets and fields added, lineage edges, enriched nodes, and the lossiness ledger. It declares destructiveHint: false and means it: import is additive, existing governed nodes are never mutated, and the tool description itself tells agents to use audit_vendor_project to see drift rather than expecting a re-import to reconcile anything.
The artifactUrls flow: when the extract doesn't fit in a prompt
A real information_schema extract for a wide estate can run to megabytes, and MCP clients cannot always inline that into a tool call. So the artifact-bearing tools accept a second map, artifactUrls, of artifact name to URL. The server fetches each URL itself and treats the response body as that artifact's content:
{ "tool": "audit_vendor_project",
"arguments": {
"graphProjectId": "0f8fad5bd9cb469fa16570867728950e",
"vendor": "snowflake",
"artifactUrls": {
"information_schema": "https://storage.example.com/extracts/information_schema.json?sig=..."
} } }
A presigned object-store URL is the natural fit. You can mix the two maps freely — small artifacts inline, the big one by URL.
Because these are server-side fetches of caller-supplied URLs, the fetch path is deliberately locked down: only https URLs are accepted, redirects are disabled (a redirect to an internal host would bypass any pre-flight check), hosts that resolve to loopback, link-local, or private address ranges are refused, and the response size is capped at 256 MB. Any refused or failed fetch doesn't abort the call — it lands in a FetchProblems list in the result, so the agent sees exactly which artifact went missing and why, alongside whatever it could still audit.
Step 3: audit and read the result
{ "tool": "audit_vendor_project",
"arguments": {
"graphProjectId": "0f8fad5bd9cb469fa16570867728950e",
"vendor": "snowflake",
"artifacts": { "information_schema": "<fresh rows JSON>" } } }
The result gives the agent both representations of the same truth: machine-readable counts and findings (section, severity, code, subject, message, detail) for deciding, and a markdown report for showing a human. The decision rule is one line and the tool description states it outright: an error count above zero is the CI-gate fail signal. For Snowflake, conformance findings arrive under the vendor's own codes — semi-structured-column for VARIANT/OBJECT/ARRAY columns whose inner schema is ungoverned, key-column-undeclared for key-shaped columns with no declared PK/FK, table-no-comment for undocumented objects — on top of the neutral coverage and drift checks.
Note what the audit tool does not have: a recordHistory switch. Over MCP the audit is purely a question; the rolling history and re-audit trail are driven from the HTTP surfaces, where recording a run is an explicit, auditable act.
Step 4: generate the way back
When the governed model is the truth and the warehouse should follow it, the agent can produce the DDL:
{ "tool": "generate_vendor_artifacts",
"arguments": {
"graphProjectId": "0f8fad5bd9cb469fa16570867728950e",
"vendor": "snowflake",
"typeNames": ["CUSTOMERS", "ORDERS"] } }
For Snowflake the result is one artifact, coremodels_tables.sql (kind sql): CREATE OR REPLACE TABLE statements with native types recorded at import (IR defaults otherwise), NOT NULL from checks, informational PRIMARY KEY/FOREIGN KEY declarations, and comments carrying descriptions and governed allowed values. typeNames scopes generation; empty means everything eligible. Views come back as lossiness records, not statements — they're derived objects, and the tool says so rather than fabricating definitions. Generation is read-only: the agent gets a script to propose, and applying it in the warehouse remains a human-reviewed act.
Step 5: when two vendors govern the same tables
Snowflake estates are often governed from two sides at once — the warehouse metadata on one, a dbt project that materializes into it on the other. After both have been imported, the fifth tool links each dataset pair the two vendors describe as the same physical relation (matched by database.schema.table) into one governed entity, with reciprocal sameAs mappings at dataset and field level:
{ "tool": "reconcile_vendor_projects",
"arguments": {
"graphProjectId": "0f8fad5bd9cb469fa16570867728950e",
"vendorA": "dbt",
"vendorB": "snowflake" } }
The result reports linksWritten, the matched pairs (vendor ids, physical name, matched fields), and the unmatched leftovers on each side. It is Admin-gated on /mcp-admin, declares idempotentHint: true, and means it — re-running after the next import refreshes the links rather than duplicating them.
What this adds up to
A realistic agent session against a governed Snowflake project reads like a checklist: status to see what's connected and when it was last imported; audit against a fresh extract (inline or by URL) to get counts and findings; a markdown report handed to the channel where humans live; and, when asked, generated DDL scoped to the types under discussion. Five tools, two of them enough for the everyday loop, all of them role-checked, and none of them able to silently change governed meaning.
The Snowflake quickstart in our docs has the Snowsight queries that produce the extracts these tool calls consume, plus the equivalent HTTP calls for every step.