<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: László Hadházy</title>
    <description>The latest articles on DEV Community by László Hadházy (@lhadhazy).</description>
    <link>https://dev.to/lhadhazy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4093667%2F712ee963-9d68-4802-9deb-20c892319d30.jpg</url>
      <title>DEV Community: László Hadházy</title>
      <link>https://dev.to/lhadhazy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lhadhazy"/>
    <language>en</language>
    <item>
      <title>The catalog is the API — a governed mesh over MCP</title>
      <dc:creator>László Hadházy</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:14:00 +0000</pubDate>
      <link>https://dev.to/lhadhazy/the-catalog-is-the-api-a-governed-mesh-over-mcp-1o18</link>
      <guid>https://dev.to/lhadhazy/the-catalog-is-the-api-a-governed-mesh-over-mcp-1o18</guid>
      <description>&lt;p&gt;A domain engineer should be able to ask an assistant &lt;em&gt;"which sensitivity level applies to a column that stores an EU IBAN?"&lt;/em&gt; and get the answer from the live LakeFormation policy rather than from a wiki page somebody last edited in 2023.&lt;/p&gt;

&lt;p&gt;That sentence is easy to write and considerably harder to earn. The interesting part is not that a model can answer the question — it is &lt;em&gt;where the answer comes from&lt;/em&gt;, and what stops the model from answering when it should not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Companion to &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws" rel="noopener noreferrer"&gt;Fabric + Mesh on AWS&lt;/a&gt;.&lt;/strong&gt; I wrote that sentence at the end of July; that post's &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s3" rel="noopener noreferrer"&gt;§ 3&lt;/a&gt; ended with a forward-pointer and no landing page: &lt;em&gt;"the next affordance this ontology deserves is an MCP-exposed catalog tool… It is a natural next step; it is not in the reference implementation yet."&lt;/em&gt; This post is the design behind that sentence. The tool is still unbuilt; what follows marks which parts are protocol, which are pattern, and which are neither yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; moved a long way while those posts were being written. Everything below is checked against revision &lt;strong&gt;&lt;code&gt;2026-07-28&lt;/code&gt;&lt;/strong&gt;, and the revision is named on purpose: a post about a protocol that dates itself is more useful than one that pretends to be timeless.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 1 — Why the catalog, and not the warehouse
&lt;/h2&gt;

&lt;p&gt;The reflex when someone says &lt;em&gt;"let the agent query our data"&lt;/em&gt; is text-to-SQL against the lake. &lt;a href="https://cordata.tech/en/blog/pipeline-half-openlineage-gx#s7" rel="noopener noreferrer"&gt;Part 2 § 7&lt;/a&gt; argued why that is the wrong surface: raw SQL against a governed lake is unsafe for an agentic consumer, and a typed semantic API above it is what makes a question answerable without hallucination.&lt;/p&gt;

&lt;p&gt;That names one tier — the semantic layer, where &lt;code&gt;monthly_recurring_revenue&lt;/code&gt; means one thing to every consumer. This post is about the other, and the two are easy to swap. The semantic layer answers questions &lt;strong&gt;about numbers&lt;/strong&gt;: &lt;em&gt;what was fraud recall at 30 days last quarter?&lt;/em&gt; The catalog answers questions &lt;strong&gt;about the data itself&lt;/strong&gt;: &lt;em&gt;which sensitivity level applies to a column that stores an EU IBAN? Who owns the claims domain? May this role read it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;None of it is queryable from the lake. It is metadata &lt;em&gt;about&lt;/em&gt; the datasets rather than data in them — LakeFormation's &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s3" rel="noopener noreferrer"&gt;LF-tags&lt;/a&gt;, ownership records, grant expressions, descriptors — four stores behind four APIs in the governance account, reached through the console or Terraform.&lt;/p&gt;

&lt;p&gt;A determined engineer can assemble the answer by hand, and nothing here is impossible without an assistant. But that is the wrong counterfactual. Nobody writes a four-API join at 16:40 while adding a column to a table — they copy the tag from the column next to it, or ask in Slack and get somebody's memory of a decision made in March. The tool competes against guessing, not against scripting. And &lt;em&gt;"stores an EU IBAN"&lt;/em&gt; is not a lookup key in any case: no record is filed under that phrase, so answering means matching an informal description against the columns already classified — which is § 7's problem, not SQL's.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A catalog question is not a query with tighter permissions on it.&lt;/strong&gt; It is a different question against a different store — and the answer counts only if the model retrieved it rather than knew it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That constraint is sharper than it looks. A domain engineer tagging a new column asks which sensitivity level applies to an EU IBAN, and the model answers &lt;code&gt;high&lt;/code&gt; — fluently, and correctly: that is exactly how &lt;code&gt;policy_curated.customer.iban&lt;/code&gt; is tagged in the reference.&lt;/p&gt;

&lt;p&gt;Now the same engineer tags an IBAN column in an analytics extract that is replicated across regions. The sensitivity is still &lt;code&gt;high&lt;/code&gt;, so the answer still looks right. But &lt;code&gt;residency&lt;/code&gt; records where the &lt;em&gt;data&lt;/em&gt; sits, not where the account holder banks — the source table is &lt;code&gt;residency: eu&lt;/code&gt; and this copy is &lt;code&gt;residency: global&lt;/code&gt;, which changes which grants reach it while leaving the sensitivity untouched. A model reasoning from world knowledge will offer &lt;code&gt;eu&lt;/code&gt; anyway, because that is what &lt;em&gt;EU&lt;/em&gt; IBAN says, and validation accepts it — &lt;code&gt;eu&lt;/code&gt; is a permitted value. The tag set is legal, it is wrong, and nothing downstream will say so.&lt;/p&gt;

&lt;p&gt;That is the failure mode — right about the obvious key, quietly wrong about the one carrying the policy. The platform team owns this ontology, and they are not standing behind the domain engineer while the column gets tagged. Retrieving how comparable columns are &lt;em&gt;already&lt;/em&gt; classified is what puts them there in effect; asking a model what it knows about IBANs cannot. So the assistant's job here is retrieval and routing; the judgement stays in the policy store.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 2 — Tools or Resources — the decision everything else hinges on
&lt;/h2&gt;

&lt;p&gt;MCP exposes two server-side primitives that look interchangeable in a diagram and are not. The specification separates them by &lt;em&gt;who decides&lt;/em&gt;. Tools are &lt;strong&gt;model-controlled&lt;/strong&gt; — the language model discovers and invokes them from its own reading of the conversation. Resources are &lt;strong&gt;application-driven&lt;/strong&gt; — the host application decides what to put in context, typically through a picker the user drives.&lt;/p&gt;

&lt;p&gt;A catalog is both, and splitting it correctly is the most consequential decision in the design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LF-tag ontology is a Resource.&lt;/strong&gt; From &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s3" rel="noopener noreferrer"&gt;§ 3&lt;/a&gt; of the reference: five keys, twenty-one values, small enough to fit on a whiteboard. That is a &lt;em&gt;document&lt;/em&gt;, and a host should be able to pin it into context wholesale — the same way an IDE pins an open file — so the model reasons against the full vocabulary instead of discovering it one lookup at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A grant check is a Tool.&lt;/strong&gt; &lt;em&gt;"May this role read this table?"&lt;/em&gt; is a question with arguments, asked mid-reasoning, whose answer the model cannot predict. It has to be invoked.&lt;/p&gt;

&lt;p&gt;The costs of getting this backwards are asymmetric. Everything-as-Tools burns turns re-fetching a twenty-one-value vocabulary the model could have held from the start. Everything-as-Resources is worse — the model cannot look anything up on its own, and a host forced to anticipate every dataset a conversation might touch will anticipate wrong.&lt;/p&gt;

&lt;p&gt;The split shows up in the wire format, and it is worth seeing the two side by side. The ontology is one fixed document, so it is a &lt;code&gt;Resource&lt;/code&gt; with a &lt;code&gt;uri&lt;/code&gt;. Datasets are parameterised, so they are &lt;code&gt;ResourceTemplate&lt;/code&gt;s with a &lt;code&gt;uriTemplate&lt;/code&gt; — the protocol uses &lt;a href="https://datatracker.ietf.org/doc/html/rfc6570" rel="noopener noreferrer"&gt;RFC 6570 URI templates&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ← resources/list — one fixed document, addressed directly&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttlMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cacheScope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cordata://ontology/lf-tags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lf-tag-ontology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LF-tag ontology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The governance-owned tag vocabulary. Keys, permitted values, and what each value implies."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// ← resources/templates/list — a shape, expanded per domain and table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttlMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cacheScope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resourceTemplates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"uriTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cordata://domain/{domain}/dataset/{table}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dataset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dataset descriptor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Schema, ownership, LF-tags and contract for one published dataset."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A URI with no variables in it is not a template — it is a resource that has been filed in the wrong list. The distinction is mechanical, and it is the same one the section has been arguing all along.&lt;/p&gt;

&lt;p&gt;Template arguments can be auto-completed through the protocol's completion API, which means a host can offer a domain picker without the server writing a picker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0cg8jm944ntvhqv7uy9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0cg8jm944ntvhqv7uy9.png" alt="The catalog server's reach across accounts, and where the metadata plane stops" width="800" height="1830"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagram read-out.&lt;/strong&gt; Three tiers, and the boundary that matters is not the one between accounts. &lt;strong&gt;Outside the governed accounts&lt;/strong&gt; sit the host application, which pins the ontology as a Resource, and the language model, which invokes lookups as Tools — the host may well run inside a consumer account, but the model inference usually does not, which is the whole reason § 6 exists. Both paths enter the &lt;strong&gt;central governance account&lt;/strong&gt; — the same one that holds the LakeFormation admin plane in the &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s1" rel="noopener noreferrer"&gt;reference topology&lt;/a&gt; — where the caller's token is validated as this server's own audience before anything is read. That validation is the account boundary doing its job: the server holds its own LakeFormation grants rather than borrowing the caller's, which is what § 5 means by refusing passthrough. Where scope falls short the answer is a challenge, not an empty list — an empty one would tell the agent the dataset does not exist, which is a different sentence and a false one (§ 5).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;producer accounts&lt;/strong&gt; are where the distinction has to be exact. They hold two different things, and the catalog reaches one of them. Column schema lives in the domain's Glue catalog, and &lt;code&gt;get_dataset&lt;/code&gt; reads it — that is metadata, and the dotted edge is real traffic. The S3 objects beside it hold the rows, and they carry no inbound edge at all. So the line the design defends is the &lt;strong&gt;metadata plane against the data plane&lt;/strong&gt;, not one account against another: a catalog server that reads a producer's Glue catalog is behaving correctly, and the same server reading a producer's S3 object is the failure § 4 is built to prevent.&lt;/p&gt;

&lt;p&gt;Which leaves the decision the section opened with, now with the consequences attached. Resource or Tool is not a tidy way to organise an API surface — it decides &lt;strong&gt;who may pull a thing into context&lt;/strong&gt;: the host, deliberately and in advance, or the model, mid-reasoning and on its own initiative. That single choice then fixes what varies by the caller's scopes (§ 5), what a cache may hold and for how long (§ 7), and which primitive the safety envelope has to be enforced on rather than annotated (§ 4). Get it backwards and none of those three has a stable answer, because the thing being governed keeps changing hands. Everything downstream is a consequence of this split, which is why it comes first.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 3 — The tool set, and where its schema comes from
&lt;/h2&gt;

&lt;p&gt;The catalog server exposes four tools. § 1 put three questions to the catalog — &lt;em&gt;who owns the claims domain? which sensitivity level applies to a column that stores an EU IBAN? may this role read it?&lt;/em&gt; — and each already has its answer sitting in a store the &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s1" rel="noopener noreferrer"&gt;reference topology&lt;/a&gt; holds: ownership in DataZone, schema in the domain's Glue catalog, the permitted vocabulary in the LF-tag ontology, the reach of a role in the LakeFormation grants. The sensitivity question takes two tools rather than one, because retrieving precedent and validating a proposal are different operations — the exchange below shows why.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Determinism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What domains exist, and who owns them?&lt;/td&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_dataset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What is this dataset's schema, owner, and tag set?&lt;/td&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resolve_lf_tag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is this tag set valid against the ontology?&lt;/td&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check_grant_expression&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Would this role's grant expression reach this dataset?&lt;/td&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one is a lookup or an evaluation. None asks a model to decide anything — which is what makes the tool set auditable, and which is the inclusion rule in full: &lt;strong&gt;a question earns a tool when its answer already exists as a record in the governance plane.&lt;/strong&gt; That rule is what generates the exclusions in § 4 as well — rows are data rather than metadata, a per-user grant listing is an access-review artefact with its own approval path, cross-domain composition is a subscription workflow. Those are not gaps in the tool set; they are the same rule, applied.&lt;/p&gt;

&lt;p&gt;Most of the schema for these tools does not need inventing. &lt;a href="https://cordata.tech/en/blog/pipelines-as-descriptors#s2" rel="noopener noreferrer"&gt;Part 1 § 2&lt;/a&gt; ended on the rule that one Pydantic model is imported by both CI and the executor, because two implementations of the same rules drift. The catalog server becomes its third importer: ownership and the policy contract are the descriptor's own fields, so &lt;code&gt;get_dataset&lt;/code&gt; reuses them rather than restating them. Only the column list is declared locally, because the descriptor does not carry one — columns come from the domain's Glue catalog, which is the same plane boundary the § 2 diagram draws.&lt;/p&gt;

&lt;p&gt;What follows is a fragment of the catalog server's own source: a module-level declaration of one tool. It is evaluated once, when the server process starts, and the resulting object is what every &lt;code&gt;tools/list&lt;/code&gt; response hands back. Nothing here runs per request — the two &lt;code&gt;model_json_schema()&lt;/code&gt; calls happen at import, which is why a change to the descriptor model reaches the advertised schema without anyone editing the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipeline_runtime.descriptor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Metadata&lt;/span&gt;

&lt;span class="n"&gt;GET_DATASET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dataset lookup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Schema, ownership and policy tags for one published dataset.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Owning domain, e.g. fraud or claims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-mcp-header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# The governance-owned halves are not redefined here. One model, three
&lt;/span&gt;    &lt;span class="c1"&gt;# consumers: CI validation, the executor, and now the catalog server.
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_json_schema&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;# name, domain, owner
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_json_schema&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;# freshness_sla, lf_tags
&lt;/span&gt;            &lt;span class="c1"&gt;# Columns are the one part the descriptor does not carry: they are
&lt;/span&gt;            &lt;span class="c1"&gt;# read from the domain's Glue catalog, so their shape is declared here.
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;array&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lf_tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lf_tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One declaration, three consumers. The model that fails a pull request when a descriptor is malformed is the same model that tells an agent what shape to expect. When a field is added, all three move together or none do.&lt;/p&gt;

&lt;p&gt;The declaration says what the answer will look like. The handler is what fills it in, and it runs once per &lt;code&gt;tools/call&lt;/code&gt; — two reads against the domain's account, joined on the column name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# § 5 — before either read, not after
&lt;/span&gt;
    &lt;span class="c1"&gt;# The server runs in the governance account; the catalog it needs lives in
&lt;/span&gt;    &lt;span class="c1"&gt;# the domain's. One assumed role per call, session-named for the caller, so
&lt;/span&gt;    &lt;span class="c1"&gt;# the producer account's CloudTrail attributes the read to a person.
&lt;/span&gt;    &lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nf"&gt;assume_reader_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;glue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lf&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lakeformation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# `&amp;lt;domain&amp;gt;_curated` is the per-domain Glue database the blueprint creates
&lt;/span&gt;    &lt;span class="c1"&gt;# (reference § 2: name = "${var.domain_name}_curated"), so the tool takes a
&lt;/span&gt;    &lt;span class="c1"&gt;# domain and a table and never asks the caller for a database name.
&lt;/span&gt;    &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DatabaseName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_curated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;StorageDescriptor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;tagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_resource_lf_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TableWithColumns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DatabaseName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_curated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ColumnNames&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;]}},&lt;/span&gt;
        &lt;span class="n"&gt;ShowAssignedLFTags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;by_column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TagKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TagValues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LFTags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tagged&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LFTagsOnColumns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                            &lt;span class="c1"&gt;# validated against outputSchema on the way out
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;metadata_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;contract_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lf_tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;by_column&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{})}&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in there is clever, which is the point: two AWS calls and a dictionary join. The role assumption is the one line worth pausing on — it is the cross-account hop from &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s1" rel="noopener noreferrer"&gt;§ 1&lt;/a&gt; of the reference, and naming the session after the caller is what keeps the producer account's CloudTrail useful. A read attributed to &lt;code&gt;mcp-catalog-server&lt;/code&gt; tells an auditor nothing; one attributed to the engineer who asked is the same behaviour record &lt;a href="https://cordata.tech/en/blog/behaviour-first-governance-in-practice#s4" rel="noopener noreferrer"&gt;Behaviour-first governance&lt;/a&gt; already projects from. Neither call can return a row — &lt;code&gt;get_table&lt;/code&gt; returns column definitions and &lt;code&gt;get_resource_lf_tags&lt;/code&gt; returns tags, and there is no third call to Athena. The read-only property § 4 insists on is a fact about which APIs the handler is able to reach, visible in the source, rather than a claim made in an annotation.&lt;/p&gt;

&lt;p&gt;The protocol is strict about the contract this creates: where an output schema is declared, servers &lt;strong&gt;MUST&lt;/strong&gt; return structured results conforming to it, and clients &lt;strong&gt;SHOULD&lt;/strong&gt; validate against it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;x-mcp-header&lt;/code&gt; annotation on &lt;code&gt;domain&lt;/code&gt; is worth a note. A parameter marked this way is mirrored into an &lt;code&gt;Mcp-Param-Domain&lt;/code&gt; HTTP header, so a load balancer or WAF can route on it without parsing the request body — which maps directly onto the account-per-domain topology from &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s1" rel="noopener noreferrer"&gt;§ 1&lt;/a&gt; of the reference. It carries its own warning, and it is a real one: header values are visible to network intermediaries, so sensitive parameters must never be marked this way. A domain name is fine. A subject identifier is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The IBAN question, answered
&lt;/h3&gt;

&lt;p&gt;Back to the sentence that started this. A domain engineer is tagging a new column and asks which sensitivity level applies to one holding an EU IBAN.&lt;/p&gt;

&lt;p&gt;The answer does not come from the model's opinion about IBANs. It comes from &lt;strong&gt;precedent already in the catalog&lt;/strong&gt;. &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s3" rel="noopener noreferrer"&gt;§ 3&lt;/a&gt; of the reference tags exactly this column, in Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lakeformation_resource_lf_tags"&lt;/span&gt; &lt;span class="s2"&gt;"iban_column_high"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;database&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"policy_curated"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;table&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;column&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"iban"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;lf_tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sensitivity"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the exchange is a retrieval, then a validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// → tools/call — find how comparable columns are already classified&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_dataset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// ← result&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"structuredContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"lf_tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sensitivity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"residency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                     &lt;/span&gt;&lt;span class="nl"&gt;"subject_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy-domain"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// → tools/call — validate the proposed tag set before it is written&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resolve_lf_tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sensitivity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"residency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="nl"&gt;"subject_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second call is not a formality. It runs the same &lt;code&gt;policy.resolve()&lt;/code&gt; the executor runs, against the same governance-owned vocabulary, and it fails the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ontology&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# governance-owned vocabulary
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnknownTagKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UnknownTagValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An assistant proposing &lt;code&gt;sensitivity: hgh&lt;/code&gt; gets &lt;code&gt;UnknownTagValue&lt;/code&gt; with the permitted set attached, at the moment of tagging — rather than a table written with a tag nothing grants against. That is the defence-in-depth the original callout claimed, and it is worth being clear about which half does the work: the model reduces search time and the ontology refuses invalid answers, and neither half is sufficient alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 4 — The safety envelope is enforced, not annotated
&lt;/h2&gt;

&lt;p&gt;The tempting shortcut is declaring the tool set read-only. MCP has a field that looks exactly like it should do the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema's own documentation removes any doubt about what that is worth — this is the doc comment on &lt;code&gt;ToolAnnotations&lt;/code&gt; in &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts" rel="noopener noreferrer"&gt;&lt;code&gt;schema/2026-07-28/schema.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: all properties in &lt;code&gt;ToolAnnotations&lt;/code&gt; are &lt;strong&gt;hints&lt;/strong&gt;. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like &lt;code&gt;title&lt;/code&gt;). Clients should never make tool use decisions based on &lt;code&gt;ToolAnnotations&lt;/code&gt; received from untrusted servers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So &lt;code&gt;readOnlyHint&lt;/code&gt; is display metadata. It tells a host how to render a confirmation dialog. It enforces nothing, and a safety envelope that depends on it is not an envelope. &lt;strong&gt;Read-only has to hold by construction.&lt;/strong&gt; The handler has no write path, the database role has no &lt;code&gt;INSERT&lt;/code&gt;, and the grants stop at &lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;DESCRIBE&lt;/code&gt;. A server built that way stays read-only no matter what its annotations say.&lt;/p&gt;

&lt;p&gt;What the tool set deliberately does not expose matters as much as what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No raw rows.&lt;/strong&gt; The catalog answers questions about datasets, never from them. An agent that can read &lt;code&gt;customer.iban&lt;/code&gt; values has defeated the entire LF-tag model in one call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-user grant listings.&lt;/strong&gt; &lt;em&gt;"Would this expression reach this dataset"&lt;/em&gt; is answerable. &lt;em&gt;"Show me everyone who can read the claims domain"&lt;/em&gt; is an access-review artefact with its own approval path, not a tool call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No join hints across domains.&lt;/strong&gt; Cross-domain composition is a subscription workflow in DataZone, and routing around it through a helpful assistant is exactly the failure the workflow exists to prevent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every call gets logged into the governance-plane event stream that &lt;a href="https://cordata.tech/en/blog/behaviour-first-governance-in-practice#s4" rel="noopener noreferrer"&gt;Behaviour-first governance § 4&lt;/a&gt; already builds its audit projection from. The tool set is not an exception to the governance model — it is another source of behaviour, and it should show up in the same query a supervisor's ninety-day access question runs against.&lt;/p&gt;

&lt;p&gt;One further trap, new enough in the specification to be easy to miss. MCP has no protocol-level session, so servers needing state across calls hand back a handle — and the specification's guidance here — non-normative, but unusually direct — is that for authenticated servers &lt;em&gt;"a handle is a name, not a capability"&lt;/em&gt;, to be re-validated against the caller's authorization on every call. A &lt;code&gt;dataset_id&lt;/code&gt; returned by &lt;code&gt;list_domains&lt;/code&gt; is not a ticket. If the second call skips that re-check, the first call has quietly become an access-granting operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 5 — Authorization is the governance plane
&lt;/h2&gt;

&lt;p&gt;This is the part that changed most, and it lands directly on the mesh argument.&lt;/p&gt;

&lt;p&gt;Honesty about scope first, because the specification's framing is easy to misread. &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization" rel="noopener noreferrer"&gt;Protocol Requirements&lt;/a&gt; opens by declaring authorization &lt;strong&gt;OPTIONAL&lt;/strong&gt; for MCP implementations, and then says why in the next three lines: HTTP-based transports &lt;strong&gt;SHOULD&lt;/strong&gt; conform, STDIO transports &lt;strong&gt;SHOULD NOT&lt;/strong&gt; — they &lt;em&gt;"retrieve credentials from the environment"&lt;/em&gt; instead — and alternative transports &lt;strong&gt;MUST&lt;/strong&gt; follow the best practices of their own protocol.&lt;/p&gt;

&lt;p&gt;So OPTIONAL is about which transports the specification covers, not about how much security a server owes its callers. A stdio server is a local subprocess started by the host: there is no browser to redirect, no audience to bind a token to, and the operating system already decided who may run it. An OAuth flow would protect nothing the process boundary does not already protect. Nothing about that concession applies to a catalog server, which is remote, HTTP, and multi-tenant by construction — the &lt;strong&gt;SHOULD&lt;/strong&gt; lands squarely on it.&lt;/p&gt;

&lt;p&gt;And once a server is inside that scope, the requirements stop being soft:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP servers &lt;strong&gt;MUST&lt;/strong&gt; implement OAuth 2.0 Protected Resource Metadata (&lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728&lt;/a&gt;); clients &lt;strong&gt;MUST&lt;/strong&gt; use it for authorization-server discovery.&lt;/li&gt;
&lt;li&gt;Clients &lt;strong&gt;MUST&lt;/strong&gt; implement Resource Indicators (&lt;a href="https://www.rfc-editor.org/rfc/rfc8707.html" rel="noopener noreferrer"&gt;RFC 8707&lt;/a&gt;) and send &lt;code&gt;resource&lt;/code&gt; on &lt;strong&gt;both&lt;/strong&gt; authorization and token requests.&lt;/li&gt;
&lt;li&gt;Servers &lt;strong&gt;MUST&lt;/strong&gt; validate that access tokens were issued specifically for them as the intended audience, and &lt;strong&gt;MUST NOT&lt;/strong&gt; accept or transit any other tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last requirement is the confused-deputy protection stated normatively: token passthrough is prohibited. A catalog server cannot take the token it was handed and replay it against LakeFormation on the caller's behalf. It has to be its own audience, with its own grants.&lt;/p&gt;

&lt;p&gt;On AWS that prohibition costs nothing, because there is nothing to pass through to. &lt;strong&gt;LakeFormation has never heard of a bearer token.&lt;/strong&gt; Its grants are written against IAM principals, so an OAuth access token is not a weaker credential there — it is not a credential at all. The identity has to be translated, and that translation is where this section stops being about MCP and starts being about your accounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_SAFE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^\w+=,.@-]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# STS: alphanumerics and +=,.@- only
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assume_reader_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Scope check, against the token this server already validated as its
&lt;/span&gt;    &lt;span class="c1"&gt;#    own audience. The token does not travel any further than this line.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalog:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalog:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. The server assumes the domain's reader role under its OWN identity.
&lt;/span&gt;    &lt;span class="c1"&gt;#    The caller is carried as a name and a session tag, never as a
&lt;/span&gt;    &lt;span class="c1"&gt;#    credential — an OIDC `sub` may contain characters STS rejects.
&lt;/span&gt;    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;assume_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;RoleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:role/cordata-catalog-reader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RoleSessionName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_SAFE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cordata:caller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]}],&lt;/span&gt;
        &lt;span class="n"&gt;DurationSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AccessKeyId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SecretAccessKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_session_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SessionToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;assume_role&lt;/code&gt; rather than &lt;code&gt;assume_role_with_web_identity&lt;/code&gt; is deliberate. Federating the caller's own JWT into AWS would mean forwarding a token this server accepted as its own audience on to a third party — the passthrough the &lt;strong&gt;MUST NOT&lt;/strong&gt; above prohibits. The token stops at the scope check.&lt;/p&gt;

&lt;p&gt;So the governance chain has three links, no two of them enforced by the same system, and only one of them living in the MCP server at all:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Enforced by&lt;/th&gt;
&lt;th&gt;Owned by&lt;/th&gt;
&lt;th&gt;Decides&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Role trust policy&lt;/td&gt;
&lt;td&gt;IAM, in the domain's account&lt;/td&gt;
&lt;td&gt;Platform team, via the account blueprint&lt;/td&gt;
&lt;td&gt;Whether this server may assume the role at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LF-tag grant expression&lt;/td&gt;
&lt;td&gt;LakeFormation, governance account&lt;/td&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Which columns come back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope check&lt;/td&gt;
&lt;td&gt;The MCP server's own code&lt;/td&gt;
&lt;td&gt;The server&lt;/td&gt;
&lt;td&gt;Whether the call is attempted in the first place&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server holds none of that policy — a scope check and an &lt;code&gt;AssumeRole&lt;/code&gt; call is the whole of it, which is the entire reason a compromised catalog server is a contained problem rather than a catastrophic one.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;role's trust policy&lt;/strong&gt; decides who may assume it at all, and it is platform-owned — part of the same domain-account blueprint as everything else in &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws#s2" rel="noopener noreferrer"&gt;§ 2&lt;/a&gt; of the reference, not something a domain writes by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"catalog_reader_trust"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="c1"&gt;# sts:TagSession is not optional here — without it the Tags= argument&lt;/span&gt;
    &lt;span class="c1"&gt;# in the handler fails, and the caller's identity never reaches CloudTrail.&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sts:TagSession"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;catalog_server_role_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# governance account, one role&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:ExternalId"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;catalog_external_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One named principal, in one account. Nothing else, in any account, can assume this role. A domain that wants to stop answering catalog questions removes the trust statement, and no coordination with the server is required.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;LF-tag grant&lt;/strong&gt; on that role decides which columns come back, and it is governance-owned — written once against a tag expression, never against a table name.&lt;/p&gt;

&lt;p&gt;And the &lt;strong&gt;scope check stays in the MCP server&lt;/strong&gt;, necessarily — AWS never sees the caller's token, so no IAM policy can condition on what is in it. That check is ordinary application code and can therefore have bugs, which is why the role it guards is scoped to a single domain: a bug there costs one domain's metadata, not every domain's.&lt;/p&gt;

&lt;p&gt;The sentence that matters most for a federated mesh sits in the &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools" rel="noopener noreferrer"&gt;tools chapter&lt;/a&gt;, and repeats verbatim for &lt;code&gt;resources/list&lt;/code&gt;. The tool set &lt;strong&gt;MUST NOT&lt;/strong&gt; vary per-connection, but it &lt;strong&gt;MAY&lt;/strong&gt; vary &lt;em&gt;"by the authorization presented on the request — for example, returning only the tools the caller's granted scopes permit — since credentials are per-request input, not connection state."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is account-per-domain expressed in a protocol: a fraud engineer's token surfaces fraud datasets, and the identical server shows a different catalog to the governance function. Not a filtered view rendered by a client — a different response, decided server-side, per request.&lt;/p&gt;

&lt;p&gt;And when the caller asks for something outside their scope, the correct answer is not an empty list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;403&lt;/span&gt; &lt;span class="ne"&gt;Forbidden&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer error="insufficient_scope",&lt;/span&gt;
&lt;span class="s"&gt;                         scope="catalog:claims:read",&lt;/span&gt;
&lt;span class="s"&gt;                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",&lt;/span&gt;
&lt;span class="s"&gt;                         error_description="Read access to the claims domain is required"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty result tells an agent &lt;em&gt;"no such dataset"&lt;/em&gt;, and a competent agent will believe it and tell the user the data does not exist. A scope challenge tells it &lt;em&gt;what to ask for&lt;/em&gt;, and the client can run a step-up authorization flow. The specification also asks servers to emit all required scopes in a single challenge rather than one at a time, because incremental challenges force repeated round-trips for one operation.&lt;/p&gt;

&lt;p&gt;This maps onto grant expressions better than anything designed for the purpose would have. &lt;em&gt;"You cannot see this"&lt;/em&gt; and &lt;em&gt;"this does not exist"&lt;/em&gt; are different sentences, and only one of them is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 6 — DSGVO (the GDPR) — what the design keeps out of scope
&lt;/h2&gt;

&lt;p&gt;Two legal statements, both narrow enough that anyone can check them against the text. &lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679" rel="noopener noreferrer"&gt;Art. 28(3)(a) DSGVO&lt;/a&gt; requires a processor to act only on the controller's documented instructions. &lt;a href="https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679" rel="noopener noreferrer"&gt;Art. 5(2)&lt;/a&gt; requires the controller to be able to demonstrate compliance. Everything else in this section is architecture rather than law, and I will keep the two apart.&lt;/p&gt;

&lt;p&gt;The architectural claim is about scope. § 4's rule — questions about datasets, never from them — means a &lt;code&gt;sensitivity=high&lt;/code&gt; tag and a column name cross the boundary and an IBAN never does. That is worth stating plainly, because the reflex assumption about an assistant near a governed lake is the opposite one. An assistant that reaches rows raises the whole processor question; this design's contribution is to keep one surface on the other side of that line rather than to answer it.&lt;/p&gt;

&lt;p&gt;What the protocol contributes is evidence. Audience-bound tokens make a call attributable to a named client acting for a named principal rather than to an ambient credential, and prohibited token passthrough stops the chain widening silently to a fourth party. Whether the resulting record is legally required is not mine to say. Where it goes is checkable, and it is worth being precise rather than waving at the earlier post.&lt;/p&gt;

&lt;p&gt;Every call in this design leaves an AWS trail before it leaves an MCP one: &lt;code&gt;sts:AssumeRole&lt;/code&gt; into the producer account, then &lt;code&gt;glue:GetTable&lt;/code&gt; and &lt;code&gt;lakeformation:GetResourceLFTags&lt;/code&gt; inside it. Those are CloudTrail events, and the cross-account stitching in &lt;a href="https://cordata.tech/en/blog/behaviour-first-governance-in-practice#s4" rel="noopener noreferrer"&gt;Behaviour-first governance § 4&lt;/a&gt; already reads exactly this shape — its own diagram has the &lt;code&gt;AssumeRole into producer&lt;/code&gt; hop in it. The session name from the handler is what makes the trail resolve to a person rather than a service. One caveat that post is explicit about and this one should repeat: CloudTrail data events are &lt;strong&gt;not on by default&lt;/strong&gt;, so "no new reporting path" holds only where that Terraform is already applied.&lt;/p&gt;

&lt;p&gt;The RoPA projection is a different matter, and the honest answer is that a catalog read does not appear in it. &lt;a href="https://cordata.tech/en/blog/behaviour-first-governance-in-practice#s2" rel="noopener noreferrer"&gt;§ 2&lt;/a&gt; derives RoPA from OpenLineage &lt;code&gt;RunEvent&lt;/code&gt;s — jobs moving data — and a tool call is not a job run. That is a gap only if you expected the catalog to be a processing activity. Under the scoping above it is not one, so the absence is consistent rather than missing. The reference runtime's ontology carries &lt;code&gt;subject_type&lt;/code&gt; and &lt;code&gt;retention&lt;/code&gt; because the pipelines that &lt;em&gt;do&lt;/em&gt; move data need them for that projection, not because the catalog is doing anything to a data subject.&lt;/p&gt;

&lt;p&gt;Two cautions that are not legal claims at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Organisational, before architectural.&lt;/strong&gt; A works council will hear &lt;em&gt;"an AI can see the data catalog"&lt;/em&gt; as a monitoring question long before an architecture one, so bring the tool inventory and the audit projection to that meeting rather than the protocol diagram.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation, not any single answer.&lt;/strong&gt; One dataset name is unremarkable; the complete list of them is a map of how the business is organised — which domains exist, what they are called, where the regulated ones sit. An agent that can enumerate everything holds that map, and no individual call is the moment it was handed over. That is the argument for scoping &lt;code&gt;list_domains&lt;/code&gt; by authorization exactly as § 5 permits: domain names are not harmless in bulk merely because each one is harmless alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  § 7 — Retrieval over a closed set
&lt;/h2&gt;

&lt;p&gt;§ 3's IBAN exchange skipped a step. The handler turns a domain and a table into &lt;code&gt;policy_curated.customer&lt;/code&gt; on its own — but how did the assistant arrive at &lt;code&gt;domain: policy, table: customer&lt;/code&gt; to begin with?&lt;/p&gt;

&lt;p&gt;The naive answer is to let the model guess a table name. The better one exploits a property of this problem — the vocabulary is &lt;em&gt;closed and tiny&lt;/em&gt;. Five keys, twenty-one values, and dataset and column names that are enumerable.&lt;/p&gt;

&lt;p&gt;For a closed set, embedding the vocabulary and retrieving the nearest entries beats asking a model to produce a value from memory, for a reason that has nothing to do with model quality: retrieval &lt;strong&gt;cannot return a value that is not in the set&lt;/strong&gt;, and generation always can. A model asked to invent a sensitivity level will invent &lt;code&gt;pii&lt;/code&gt; or &lt;code&gt;restricted&lt;/code&gt; — plausible, absent from the ontology, and rejected by &lt;code&gt;policy.resolve()&lt;/code&gt; one step later. Retrieval over the twenty-one permitted values cannot produce that failure at all.&lt;/p&gt;

&lt;p&gt;That disposes of invalid values. It does not touch the harder case from § 1, where the model proposes &lt;code&gt;residency: eu&lt;/code&gt; for a globally replicated copy — a permitted value, accepted by &lt;code&gt;resolve()&lt;/code&gt;, and wrong anyway. No vocabulary check catches that one, because the vocabulary was never violated. What catches it is a second retrieval against a different corpus: the classifications already applied to comparable columns. Asking &lt;code&gt;get_dataset&lt;/code&gt; what the other columns in that same extract carry answers from precedent, and the reply stops depending on what the phrase &lt;em&gt;EU IBAN&lt;/em&gt; suggests.&lt;/p&gt;

&lt;p&gt;So the shape is: embed the ontology values and the catalog's column descriptions once, retrieve candidates for &lt;em&gt;"stores an EU IBAN"&lt;/em&gt;, and let the tools do the rest. Generation belongs where the answer is genuinely open — a description, a summary of why a dataset exists — and nowhere near a policy value.&lt;/p&gt;

&lt;p&gt;That word &lt;em&gt;once&lt;/em&gt; is where the cost sits. An embedding index is a snapshot; the catalog it describes is not. Datasets get published, columns get reclassified, and a retrieval that was correct on Monday starts answering from a world that has moved — silently, because a stale index returns confident nearest matches rather than errors.&lt;/p&gt;

&lt;p&gt;The protocol has an answer for the part of this it can see. List and read results alike carry &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt;, so a client knows how long the answer it just received may be trusted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;/* … */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttlMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cacheScope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"private"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// never "public": this list varies by scope&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about this is stored in the catalog. &lt;code&gt;ttlMs&lt;/code&gt; is a property of the response, not of the asset — the server picks a number as it answers, from what it knows about how quickly that particular answer goes stale. The LF-tag ontology changes by governance pull request, so it can afford minutes. A dataset list changes whenever any domain publishes, so it can afford less. A single &lt;code&gt;resources/read&lt;/code&gt; of one descriptor sits somewhere between. None of that is catalog metadata; it is a policy the server holds, and the only thing the client is told is the number.&lt;/p&gt;

&lt;p&gt;The server's own embedding index is outside all of it. No client-side TTL refreshes something the server built for itself — that refresh is triggered by the same events described below, not by anything in a response.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;private&lt;/code&gt; is not a preference here. It follows from the choice made in § 5 to let the lists vary by the caller's granted scopes, because the specification defines &lt;code&gt;public&lt;/code&gt; as a response any client or intermediary may cache and serve &lt;em&gt;across authorization contexts&lt;/em&gt;. Combine the two and a shared cache in front of the catalog becomes a way to hand one caller's view to another — the fraud engineer's dataset list served from a proxy to somebody with no fraud scope at all. A server that returned an identical list to everyone could say &lt;code&gt;public&lt;/code&gt; honestly. This one cannot, and the field is where that consequence becomes visible to the client.&lt;/p&gt;

&lt;p&gt;Five minutes is too long whenever a tag write lands inside the TTL window. A governance PR retags &lt;code&gt;policy_curated.customer.iban&lt;/code&gt; from &lt;code&gt;sensitivity: confidential&lt;/code&gt; to &lt;code&gt;high&lt;/code&gt; at 14:03; a client that listed at 14:01 keeps serving the old classification until 14:06, and every retrieval in between answers against a grant expression that no longer holds. The protocol's answer is subscriptions: the client opens a long-lived &lt;code&gt;subscriptions/listen&lt;/code&gt; stream naming the URIs it cares about in &lt;code&gt;notifications.resourceSubscriptions&lt;/code&gt;, the server replies with &lt;code&gt;notifications/subscriptions/acknowledged&lt;/code&gt;, and thereafter pushes &lt;code&gt;notifications/resources/updated&lt;/code&gt; whenever a watched resource changes. A server declares &lt;code&gt;resources: { subscribe: true }&lt;/code&gt; to offer it at all.&lt;/p&gt;

&lt;p&gt;Writing that sentence is considerably easier than deploying it. What it actually costs, on AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A compute target that stays up.&lt;/strong&gt; Not Lambda. ECS, Fargate or App Runner, with the scaling, health checks and monitoring that a long-running service needs and a function does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;idle_timeout.timeout_seconds&lt;/code&gt; raised on the ALB.&lt;/strong&gt; The default is &lt;strong&gt;60 seconds&lt;/strong&gt;; a quiet subscription is closed by your own load balancer before it reports anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A heartbeat&lt;/strong&gt;, if raising that timeout is not enough or not permitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Egress proxies you do not control.&lt;/strong&gt; Any enterprise network may buffer or terminate long-lived responses whatever you configure at either end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side reconnect, backoff and re-subscribe&lt;/strong&gt; — plus reconciling what changed while disconnected, which is the part that gets discovered in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So subscriptions are an optimisation, and for a catalog it is worth asking whether it is ever the right one. The ontology looks like the obvious candidate because every answer depends on it — but &lt;em&gt;small&lt;/em&gt; and &lt;em&gt;rarely changed&lt;/em&gt; are exactly the properties that make re-reading it cheap. One document, a short &lt;code&gt;ttlMs&lt;/code&gt;, and the exposure is bounded by the window. Dataset descriptors have the opposite shape: they genuinely do change, but watching them costs a URI per table and a held stream per connected host. Neither case is a comfortable fit.&lt;/p&gt;

&lt;p&gt;Which leaves the unglamorous default doing most of the work, including through a proxy you cannot change: a short &lt;code&gt;ttlMs&lt;/code&gt;, plus a &lt;code&gt;resources/read&lt;/code&gt; of the one descriptor immediately before acting on its policy value — the retrieve-then-validate shape from § 3, with the read doing what the stale index cannot. Subscriptions pay off where a resource changes often and is watched by few. A catalog is usually the other way round. Reclassification propagating without touching pipelines was already a property of the ontology in &lt;a href="https://cordata.tech/en/blog/pipelines-as-descriptors#s6" rel="noopener noreferrer"&gt;Part 1 § 6&lt;/a&gt;; this extends the same property to the agent's view of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  § 8 — What this design does not solve
&lt;/h2&gt;

&lt;p&gt;Three gaps, stated rather than buried.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A catalog cannot describe what nobody registered.&lt;/strong&gt; Asked about a dataset a domain built last week and never published, an agent correctly reports that it does not exist — indistinguishable, to the person asking, from the tool being broken. Coverage is a governance property, not a protocol one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Annotations remain untrusted in aggregate.&lt;/strong&gt; § 4 covers a single server. A host aggregating several inherits all their trust assumptions, and cross-server tool-name collisions are explicitly the client's problem to disambiguate. A catalog tool that is safe alone is not automatically safe beside an arbitrary third-party server in the same context window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Half of this does not port.&lt;/strong&gt; Everything the protocol contributes is cloud-agnostic — Tools versus Resources, output schemas, scope challenges and cache scoping hold wherever you run them — but the identity chain in § 5 is built out of &lt;code&gt;AssumeRole&lt;/code&gt;, LF-tag grant expressions and account-per-domain, none of which exist elsewhere. On another cloud the question &lt;em&gt;how does a validated caller become a credential the catalog can be read with?&lt;/em&gt; has to be answered again from first principles, and while the shape of that answer transfers, none of the code does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets built next
&lt;/h2&gt;

&lt;p&gt;Every protocol claim above is checked against the specification; none of it has served a request yet. That is worth stating plainly, and worth not leaving there.&lt;/p&gt;

&lt;p&gt;The next piece of work is a running server in its own repository, built on the descriptor model the &lt;a href="https://github.com/cordata-tech/pipeline-runtime" rel="noopener noreferrer"&gt;pipeline runtime&lt;/a&gt; already publishes — so that § 3's argument about one model and three consumers becomes something you can check out and run rather than something I have asserted. Until then this post is a design, and it is not meant to stay one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://cordata.tech/en/blog/pillar/data-governance" rel="noopener noreferrer"&gt;Governance pillar&lt;/a&gt; claimed governance artefacts should be byproducts of behaviour; &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws" rel="noopener noreferrer"&gt;Fabric + Mesh on AWS&lt;/a&gt; built the account topology and the tag ontology; &lt;a href="https://cordata.tech/en/blog/behaviour-first-governance-in-practice" rel="noopener noreferrer"&gt;Behaviour-first governance&lt;/a&gt; turned the emitted events into the projections an auditor asks for; the &lt;a href="https://cordata.tech/en/blog/pipelines-as-descriptors" rel="noopener noreferrer"&gt;pipeline pair&lt;/a&gt; made a pipeline a declaration that emits its own evidence.&lt;/p&gt;

&lt;p&gt;This post adds the read path, and the arc closes somewhere I did not expect when the first one was written. The ontology those posts spent so long keeping small, explicit and machine-checkable is what makes agentic access tractable — not because the agents are clever, but because a twenty-one-value vocabulary with a validator behind it is a search problem rather than a judgement problem. Invert it and the claim falls over: three hundred values and no validator, and no amount of protocol rescues any of this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you have put an agent in front of a governed catalog — or think doing so is a worse idea than I have made it sound, and can say where it breaks — I would genuinely like to hear the counter-argument. The door is open at &lt;a href="https://cordata.tech/en/contact" rel="noopener noreferrer"&gt;cordata.tech/contact&lt;/a&gt;. Related reading: the &lt;a href="https://cordata.tech/en/blog/fabric-mesh-on-aws" rel="noopener noreferrer"&gt;Fabric + Mesh on AWS reference&lt;/a&gt; for the LF-tag ontology every tool here reads from; &lt;a href="https://cordata.tech/en/blog/pipelines-as-descriptors" rel="noopener noreferrer"&gt;part 1 — a pipeline is a descriptor, not a program&lt;/a&gt; for the descriptor model the output schemas derive from, and &lt;a href="https://cordata.tech/en/blog/pipeline-half-openlineage-gx" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; for the semantic-layer tier this one sits beside; and &lt;a href="https://cordata.tech/en/blog/behaviour-first-governance-in-practice" rel="noopener noreferrer"&gt;Behaviour-first governance in practice&lt;/a&gt; for the audit projection every tool call lands in.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cordata.tech/en/blog/governed-mesh-over-mcp" rel="noopener noreferrer"&gt;cordata.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>aws</category>
      <category>datagovernance</category>
    </item>
    <item>
      <title>One skeleton, many screens — React + TypeScript</title>
      <dc:creator>László Hadházy</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:21:56 +0000</pubDate>
      <link>https://dev.to/lhadhazy/one-skeleton-many-screens-react-typescript-4698</link>
      <guid>https://dev.to/lhadhazy/one-skeleton-many-screens-react-typescript-4698</guid>
      <description>&lt;p&gt;Every internal admin tool starts clean. One screen lists articles: a table, a few count cards across the top, a status pill. Someone writes it in an afternoon and it's good.&lt;/p&gt;

&lt;p&gt;Then comes the pages screen. It's &lt;em&gt;almost&lt;/em&gt; the same — a table, count cards, no status pill this time. So it gets copy-pasted and trimmed. Then a users screen, a media screen, an audit-log screen. Six months later the tool is a graveyard of near-identical table components, each with its own subtly different pagination handling, its own loading spinner, its own idea of what an empty state looks like. Fixing a bug means fixing it five times, and you'll miss one.&lt;/p&gt;

&lt;p&gt;The reflex fix is to reach for a single mega-component — a &lt;code&gt;&amp;lt;SuperTable&amp;gt;&lt;/code&gt; with forty props and a &lt;code&gt;variant&lt;/code&gt; enum — and wire every screen through it. That trades a copy-paste problem for a God-object problem. The component grows a new prop every time a screen needs something slightly different, and eventually no one can change it without breaking a caller three domains away.&lt;/p&gt;

&lt;p&gt;The better move is to change &lt;em&gt;what a screen is&lt;/em&gt;. Not a component you configure, but &lt;strong&gt;data you declare&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A screen is a descriptor
&lt;/h2&gt;

&lt;p&gt;The unit I settled on is a plain description of the screen — its columns, and the numbers it summarises — with no rendering logic in it at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ColumnDef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sortable&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Derive the sortable/display value from the row. */&lt;/span&gt;
  &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Custom cell content, e.g. a status pill. */&lt;/span&gt;
  &lt;span class="nl"&gt;render&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StatCard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole contract a screen author touches. Everything about &lt;em&gt;how&lt;/em&gt; a table paginates, sorts, shows a loading bar, or renders an error lives elsewhere — in a single generic component the domains never edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TablePage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;GridValidRowModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getRowId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;statCards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TablePageProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// maps ColumnDef -&amp;gt; the grid's native column shape, once&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gridColumns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GridColDef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sortable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sortable&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;valueGetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;renderCell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...renders the stat cards + the data grid&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small translations do the real work here. A column's &lt;code&gt;value&lt;/code&gt; becomes the grid's &lt;code&gt;valueGetter&lt;/code&gt; — the function that turns a row into a sortable, displayable cell value. A column's &lt;code&gt;render&lt;/code&gt; becomes &lt;code&gt;renderCell&lt;/code&gt; — arbitrary JSX for the cell, which is how a status pill or a link gets in without the framework knowing anything about statuses or links. The domain describes intent; the framework owns the mechanics of the grid library underneath. Swap that library out one day and no descriptor changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a DDD boundary, not just a helper
&lt;/h2&gt;

&lt;p&gt;The split isn't only about avoiding duplication. It's a &lt;a href="https://martinfowler.com/bliki/DomainDrivenDesign.html" rel="noopener noreferrer"&gt;domain-driven design&lt;/a&gt; boundary drawn in the file tree — DDD in the one sense that earns its keep here: each part of the product (articles, pages, users) is a &lt;a href="https://martinfowler.com/bliki/BoundedContext.html" rel="noopener noreferrer"&gt;&lt;em&gt;bounded context&lt;/em&gt;&lt;/a&gt; that lives in its own module, owns its own rules, and is not allowed to reach into another. The shared machinery lives somewhere those modules can depend on, but which depends on none of them.&lt;/p&gt;

&lt;p&gt;On disk that's two top-level folders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  framework/                 # reusable, domain-agnostic — knows no domain
    bodies/
      TablePage.tsx          # the generic, descriptor-driven list screen
  features/                  # bounded contexts (migrating to domains/)
    articles/
      ArticlesDataGrid.tsx   # a descriptor, nothing more
      StatusChip.tsx         # article-specific cell content
    pages/
      PagesTable.tsx         # a descriptor, nothing more
  utils/api/                 # typed client + query hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The folder is still called &lt;code&gt;features/&lt;/code&gt; — the conventional Next.js name — and is on its way to being renamed &lt;code&gt;domains/&lt;/code&gt;, because &lt;em&gt;bounded context&lt;/em&gt; is the honest label for what lives there. The name matters less than the rule it encodes: nothing in &lt;code&gt;articles/&lt;/code&gt; may import from &lt;code&gt;pages/&lt;/code&gt;, and neither may be imported by &lt;code&gt;framework/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feur69a08e73as47u698f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feur69a08e73as47u698f.png" alt="articles/ and pages/ declare columns against framework's TablePage and depend on its ColumnDef and StatCard types; framework depends on neither" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The diagram, in words: &lt;code&gt;articles/&lt;/code&gt; and &lt;code&gt;pages/&lt;/code&gt; each declare their columns against &lt;code&gt;framework/&lt;/code&gt;'s &lt;code&gt;TablePage&lt;/code&gt; and depend on its &lt;code&gt;ColumnDef&lt;/code&gt; / &lt;code&gt;StatCard&lt;/code&gt; types; &lt;code&gt;framework/&lt;/code&gt; depends on neither of them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dependency arrow only points one way. &lt;code&gt;framework/&lt;/code&gt; knows nothing about articles or pages — it can't, and that's enforced by it having no imports from the domains. Each domain owns its own descriptor and its data-fetching, and depends &lt;em&gt;inward&lt;/em&gt; on the framework's types. A new domain can't accidentally couple itself to another domain through the shared table, because the shared table has no domain in it to couple to.&lt;/p&gt;

&lt;p&gt;This is what makes "data-driven" more than a slogan. The domain layer declares &lt;em&gt;what&lt;/em&gt; a screen is; the framework layer decides &lt;em&gt;how&lt;/em&gt; it renders. The boundary between them is a set of serialisable-ish descriptors, not a call graph.&lt;/p&gt;

&lt;p&gt;Here's what a domain screen actually looks like once the skeleton exists — the entire articles list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ColumnDef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Category&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updatedAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sortable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatusChip&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statCards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StatCard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Published&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLISHED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JSX for the grid. No pagination. No loading state. The screen is a list of columns and two reducers over the rows. Everything else is inherited.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: a zero-diff refactor
&lt;/h2&gt;

&lt;p&gt;Here's the part that tells you whether the boundary is in the right place.&lt;/p&gt;

&lt;p&gt;I didn't build the skeleton first and then screens on top of it. I had a working, hand-written articles table already built and rendering live data. To validate the abstraction, I &lt;em&gt;rewrote the existing screen to render through it&lt;/em&gt; — and watched for changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The proof you drew the boundary correctly is that refactoring an existing&lt;br&gt;
screen through the abstraction produces zero visual diff.&lt;/strong&gt; If the screen&lt;br&gt;
changes, the abstraction is imposing opinions the screen didn't ask for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The articles table looked and behaved identically before and after — same columns, same counts, same status pills, same sort behaviour. That's not an anticlimax; it's the whole result. A refactor that changes nothing the user can see, while collapsing a hand-written component into a ten-line descriptor, is the signal that the skeleton captured exactly the screen's structure and none of its incidental detail.&lt;/p&gt;

&lt;p&gt;Then the second screen — pages — cost a descriptor and nothing else. Here's the whole thing — the same &lt;code&gt;TablePage&lt;/code&gt;, fed a different descriptor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ColumnDef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Route&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updatedAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statCards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StatCard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put it next to the articles descriptor above and the entire difference between the two screens &lt;em&gt;is&lt;/em&gt; the difference in their data: pages drop the status column — they have no lifecycle status — and keep one stat card instead of two. No new component, no second grid, no fifth reimplementation of pagination. The skeleton was already written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the data typo-proof
&lt;/h2&gt;

&lt;p&gt;There's an obvious objection to declaring screens as data: a descriptor is just an object literal, and an object literal is where typos go to hide. I hit exactly that. A pages column read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;routeSlug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Route&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// the real key is `slug`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiled, it ran, and the column was blank for every row — the grid looked up &lt;code&gt;row["routeSlug"]&lt;/code&gt;, got &lt;code&gt;undefined&lt;/code&gt;, and rendered nothing. No error, no crash. A silent bug that looks like a backend problem.&lt;/p&gt;

&lt;p&gt;The fix is to make the descriptor's keys answer to the row type. Instead of &lt;code&gt;field: string&lt;/code&gt;, bind it to the row's own property names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the one line that changes in ColumnDef&amp;lt;Row&amp;gt;:&lt;/span&gt;
&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// was: field: string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;"routeSlug"&lt;/code&gt; is not a valid &lt;code&gt;field&lt;/code&gt; — the compiler rejects it and lists the keys that &lt;em&gt;are&lt;/em&gt; valid. The typo becomes a build error, not a runtime mystery. Columns that genuinely have no backing property — an actions button, a row number — opt out through a separate variant, so "no field" is a decision you can see, never an accident.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Data-driven only pays off if the data is typed as tightly as code would be.&lt;/strong&gt;&lt;br&gt;
A descriptor you can typo freely is just a config file waiting to rot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That &lt;code&gt;keyof&lt;/code&gt; binding is small, but it's what earns "screens as data" the right to be trusted — and it's the same generated row types (straight from the API's OpenAPI schema) doing double duty: rename a field on the backend and every stale descriptor turns into a compile error on the next codegen, instead of a blank column at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discipline: name what you did &lt;em&gt;not&lt;/em&gt; abstract
&lt;/h2&gt;

&lt;p&gt;The failure mode of every framework is that it doesn't know when to stop. "Data-driven" slides into a configuration language that reinvents the host framework badly, and now you maintain a worse React inside React.&lt;/p&gt;

&lt;p&gt;So the useful half of this work is the list of things I deliberately left concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Column headers are plain strings&lt;/strong&gt; — even though the localisation primitive already exists in the codebase (&lt;code&gt;LocalizedText&lt;/code&gt;, a value that's either a literal string or a reference to a translation resolved per-locale). Wiring headers through it is a one-line type change — &lt;code&gt;header: string&lt;/code&gt; becomes &lt;code&gt;header: LocalizedText&lt;/code&gt; — one I'd make the day a second language becomes real, not a speculative layer baked in on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stat cards recompute on every render&lt;/strong&gt;, over the currently-loaded page of rows. For an internal admin tool with modest datasets, a &lt;code&gt;filter&lt;/code&gt; over a few hundred rows per render is free, and the simplicity is worth more than a memoised aggregate I'd have to invalidate correctly. That's an explicit &lt;em&gt;data-driven over performance&lt;/em&gt; call, made with eyes open — and the kind of thing to revisit the day the numbers say to, not before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The grid library shows through in the descriptor&lt;/strong&gt; — &lt;code&gt;flex&lt;/code&gt;, &lt;code&gt;minWidth&lt;/code&gt;. I chose not to invent a layout abstraction over the top of it, because a leaky one costs more than the honesty of naming the underlying grid's own knobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a place a purist would abstract further. Each would have added a layer I can't yet justify with a real requirement. The boundary is only in the right place if you can say, out loud, where it ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The take-home
&lt;/h2&gt;

&lt;p&gt;A screen declared as data is not automatically better than a screen written as code. It's better when you have &lt;em&gt;many screens that share a shape&lt;/em&gt; and a team that will keep adding to them — which is exactly the situation an internal admin tool is always in, whether or not anyone admitted it at the start.&lt;/p&gt;

&lt;p&gt;The two things worth stealing, independent of the stack: draw the boundary as a &lt;strong&gt;one-way dependency&lt;/strong&gt; — domains depend inward on a framework that knows nothing about them — and validate it with a &lt;strong&gt;zero-diff refactor&lt;/strong&gt; of a screen you already trust. If the screen changes, you've abstracted the wrong thing. If it doesn't, you've earned the next screen for the price of a descriptor.&lt;/p&gt;

&lt;p&gt;If you've drawn this boundary somewhere different — or think the whole descriptor move is a trap — I'd genuinely like to hear where it broke for you. The door's open at &lt;a href="https://cordata.tech/en/contact" rel="noopener noreferrer"&gt;cordata.tech/contact&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cordata.tech/en/blog/one-skeleton-many-screens" rel="noopener noreferrer"&gt;cordata.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
