Handing the Glue Catalog to an Agent: CoreModels over MCP
An agent that can answer *"did our lake drift?"* needs three things: a way to reach the governed model, a way to hand it a fresh catalog export, and a guarantee that asking the question cannot change the answer. The CoreModels MCP server provides all three. The same vendor-integration engine behind the HTTP routes is exposed as MCP tools, with the read verbs at Viewer role and the write verbs behind an admin endpoint and real project membership.
Handing the Glue Catalog to an Agent: CoreModels over MCP
An agent that can answer "did our lake drift?" needs three things: a way to reach the governed model, a way to hand it a fresh catalog export, and a guarantee that asking the question cannot change the answer. The CoreModels MCP server provides all three. The same vendor-integration engine behind the HTTP routes is exposed as MCP tools, with the read verbs at Viewer role and the write verbs behind an admin endpoint and real project membership.
This article is the agent-side of AWS Glue governance: the tools, their exact argument names, the flow for catalogs too large to paste into a conversation, and the response quirks worth teaching your agent about.
Connect
The server speaks stateless streamable HTTP with OAuth 2.0. Two endpoints:
/mcp— the public endpoint. Only Viewer-role tools are listed or executable here./mcp-admin— additionally carries the write tools, including vendor import.
From Claude Code:
claude mcp add --transport http coremodels https://coremodels.example.com/mcp
then run /mcp inside the session to complete authorization. For a JSON-configured client:
{
"mcpServers": {
"coremodels": { "type": "http", "url": "https://coremodels.example.com/mcp" }
}
}
Authorization is discovery-driven: unauthenticated requests answer 401 with a
WWW-Authenticate header pointing at the protected-resource metadata, and spec-compliant clients
handle dynamic client registration and the PKCE authorization-code flow without a pre-registered
client id. Point a second connection at /mcp-admin only when the agent genuinely needs to
import. The endpoint split is convenience; the enforced boundary is the per-project role check,
so a Viewer-scoped identity on the admin endpoint still cannot write.
The five tools
| Tool | Role | Endpoint | What it does for Glue |
|---|---|---|---|
get_vendor_integration_status | Viewer | both | lists connectors, or reports the project's last Glue import |
audit_vendor_project | Viewer | both | audits a get-tables export against the governed model |
generate_vendor_artifacts | Viewer | both | emits Athena/Hive CREATE EXTERNAL TABLE DDL from the model |
import_vendor_project | Admin | /mcp-admin | imports the catalog into the graph, additively |
reconcile_vendor_projects | Admin | /mcp-admin | links datasets two estates govern as the same physical relation |
The tools are vendor-neutral — their descriptions use dbt as the worked example — and the
vendor argument is what selects Glue. Every schema sets additionalProperties: false, so an
agent that invents an argument gets a validation failure rather than a silently ignored field,
and graphProjectId is pattern-checked against ^[a-f0-9]{32}$.
Start with discovery
{ "graphProjectId": "9f2c41ab7e0d4c6fa1b83e57c02d9e14" }
With no vendor, get_vendor_integration_status returns every registered connector with its
key, display name, capabilities and artifact notes. That is how an agent learns — with no prior
knowledge and no hard-coded list — that Glue exists, is keyed glue, supports Import, Audit and
Generate, and wants one artifact called tables.
Add the vendor and the same tool switches to project state:
{ "graphProjectId": "9f2c41ab7e0d4c6fa1b83e57c02d9e14", "vendor": "glue" }
{
"vendor": "glue",
"imported": true,
"state": {
"vendor": "glue",
"projectName": "lake",
"importedAt": "2026-08-04T09:14:22.1043117+00:00",
"sourceFingerprint": "3f9c1d5a7b2e4086",
"counts": "fieldsAdded=0, lineageAdded=0, lineageSkipped=0, nodesEnriched=10",
"facts": "{\"tables\":\"2\",\"views\":\"0\"}"
},
"governedDatasets": 2
}
A well-prompted agent checks this first. Auditing a project that was never imported produces a
wall of dataset-unmapped coverage findings, and the honest answer in that case is "nothing is
governed yet", not "your lake is broken".
Audit with inline artifacts
{
"graphProjectId": "9f2c41ab7e0d4c6fa1b83e57c02d9e14",
"vendor": "glue",
"artifacts": { "tables": "{\"TableList\": [ … ]}" }
}
graphProjectId and vendor are the required arguments; artifacts, artifactUrls and
spaces are optional, but at least one of the two artifact channels must yield content. The tool
runs at Viewer role precisely because auditing writes nothing — there is no recordHistory
switch on the MCP surface at all.
A clean run over a small estate comes back like this:
{
"vendor": "glue",
"projectName": "lake",
"warningCount": 1,
"infoCount": 2,
"metrics": {
"Datasets (estate)": "2",
"Datasets governed": "2 / 2",
"Fields governed": "8 / 8"
},
"findings": [
{
"section": "Conformance",
"severity": "Warning",
"code": "semi-structured-column",
"subject": "lake.events",
"message": "1 struct/map/array column(s) carry ungoverned inner schemas.",
"detail": "payload"
}
],
"markdown": "# glue Schema Audit — lake\n…",
"fetchProblems": []
}
Teach your agent one serialization rule: MCP responses are compact, and default values are
dropped. errorCount is absent from the payload above because it was zero — a missing count
means zero, not unknown. The same applies to a finding's detail. Reason over findings and
markdown; treat errorCount > 0 as the violation signal exactly as CI does.
Large catalogs: the artifactUrls flow
A get-tables export for a real lake can run to many megabytes, which no conversation should
carry. Both artifact-bearing tools accept artifactUrls — artifact name to URL — and the server
fetches the content itself:
{
"graphProjectId": "9f2c41ab7e0d4c6fa1b83e57c02d9e14",
"vendor": "glue",
"artifactUrls": { "tables": "https://artifacts.example.com/exports/glue/tables.json" },
"spaces": []
}
You can mix the two channels — inline one artifact, fetch another — though Glue only defines one. The fetch is deliberately narrow, because a server-side GET to a caller-supplied URL is a classic SSRF vector:
- https only. Anything else is refused before a socket opens.
- Redirects disabled. A redirect into an internal host would defeat any pre-flight check.
- Address filtering. Hosts resolving to loopback, link-local (including the cloud metadata range), or private ranges are refused by resolved address, not by hostname string matching.
- Size capped, with a fetch timeout.
Refusals and failures do not crash the call. They arrive as strings in fetchProblems, e.g.
Refused to fetch 'tables' from https://…: host resolves to a non-public address (10.0.3.14) —
so the agent can tell you precisely why the audit ran on less than you thought, or why it could
not run at all.
Practically: put the export where the server can reach it over https — a pre-signed object URL works well, and expiring links keep the exposure short — and pass the link instead of the bytes.
Import, on the admin endpoint
{
"graphProjectId": "9f2c41ab7e0d4c6fa1b83e57c02d9e14",
"vendor": "glue",
"artifactUrls": { "tables": "https://artifacts.example.com/exports/glue/tables.json" }
}
import_vendor_project takes the same arguments as the audit tool and requires Admin membership.
It is declared non-destructive, and that is structural rather than aspirational: tables become
Types, columns and partition keys become Elements, vendor metadata is written or refreshed, and
already-governed nodes are never mutated. The worst a repeated import can do is add. The result
mirrors the HTTP verb — datasets added, fields added, nodes enriched, plus lossiness, errors and
the same fetchProblems channel.
This is the property that makes agent-initiated import tolerable at all. An agent can bring new lake tables under governance; it cannot quietly rewrite the meaning of the ones already there. Changed meaning surfaces as audit findings, where a human decides.
Generation closes the loop
{
"graphProjectId": "9f2c41ab7e0d4c6fa1b83e57c02d9e14",
"vendor": "glue",
"typeNames": ["events"]
}
generate_vendor_artifacts runs at Viewer role because it only reads. For Glue it returns one
artifact — coremodels_glue_tables.sql, kind sql — containing CREATE EXTERNAL TABLE DDL with
governed descriptions and taxonomy allowed-values on column COMMENTs, partition keys in
PARTITIONED BY, and a placeholder LOCATION. An agent asked "show me the DDL our governed
model implies for the events table" answers with exactly that, and the typeNames filter keeps
the reply to the table in question. The tool also accepts targetVersion for connectors with
dialect switches; the Glue generator has none.
What the MCP surface does not carry
Three governance verbs are HTTP-only: reaudit, history and badge. An agent cannot replay
the stored snapshot, read the rolling trail, or fetch the status SVG through MCP today. If your
workflow needs "has this been getting worse?", point the agent at those routes through whatever
HTTP capability it has, or run them from your pipeline.
The trade is a deliberate one. The MCP toolkit is the investigative surface: discover what is connected, audit what is there, generate what the model implies, import when authorised. Every read tool declares itself read-only, artifact fetching is guarded and reports its failures as data, and the findings your agent reasons over carry the same stable kebab-case codes your CI gate enforces — one governance engine, two consumers, no second opinion.
The AWS Glue quickstart in the CoreModels documentation lists these tool calls next to their HTTP equivalents if you want both surfaces side by side.