cordata-catalog-mcp
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cordata-catalog-mcpwho owns fraud/transactions_scored and what LF-tags are on its columns?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cordata-catalog-mcp
A read-only Model Context Protocol server that answers questions about datasets — who owns them, what columns they have, how those columns are classified, and whether a Lake Formation grant would reach them — and never returns a row of the data itself. It serves the example catalog that ships with cordata-pipeline-runtime.
It is the runnable version of a design written up in two posts on cordata.tech. Fabric + Mesh on AWS — LakeFormation, DataZone, and one governance plane lays out the reference topology this server is modelled on, with one AWS account per data domain and a governance account that owns the LF-tag ontology and the grants, and closes its § 3 by proposing an MCP catalog tool. The catalog is the API — a governed mesh over MCP designs that tool: which questions it answers and where its schemas come from, why read-only has to be enforced rather than annotated, and how authorization works. Where this README says "§ N of the post", it means that second post.
Verified against MCP revision 2026-07-28, the Python SDK mcp 2.3, and cordata-pipeline-runtime 0.2. MCP has changed substantially between revisions, so check those three numbers before trusting a detail below against a newer stack.
Start it
uvx cordata-catalog-mcpThat starts the server over stdio, which is how a desktop or terminal MCP client launches a local server. To give it to a client, add it to the client's server configuration:
{
"mcpServers": {
"cordata-catalog": {
"command": "uvx",
"args": ["cordata-catalog-mcp"]
}
}
}In Claude Code the same thing is one command: claude mcp add cordata-catalog -- uvx cordata-catalog-mcp.
Over stdio there is no token. The client that started the process is the only party that can talk to it, so every domain is visible. Serving it over HTTP needs a token issuer and is described below.
Related MCP server: tableau-graphql-mcp
What it answers
Five tools, each a lookup or an evaluation against a record the governance plane already holds:
Tool | Answers |
| Which domains exist, and who owns their datasets |
| One dataset's owner, contract and columns, each column with its effective LF-tags |
| Whether a tag set is valid against the governance-owned ontology |
| Whether a tag-based grant expression would reach a dataset, and which columns |
| Which datasets carry given tag values, for example |
LF-tags are Lake Formation's key–value classification labels; a grant written as a tag expression reaches every table or column whose tags match it, which is how the reference topology controls access without per-table grants.
Two resources sit beside the tools: the ontology as cordata://ontology/lf-tags, and each dataset as the template cordata://domain/{domain}/dataset/{table}.
get_dataset returns the runtime's own Metadata and Contract models from pipeline_runtime.descriptor, so the SDK derives the tool's outputSchema from the same Pydantic classes the pipeline executor validates descriptors with. Nothing in this repository restates them. resolve_lf_tag runs the executor's own policy.resolve(), so a tag set fails here exactly as it would fail in a pipeline run.
find_datasets can say which datasets hold customer data, and in which columns. It cannot say where one person's rows are, because finding those is a query against the data, and this server never reads data.
Where the columns come from
Ownership, contracts and the ontology all come from cordata-pipeline-runtime. Column lists do not, because a pipeline descriptor does not carry one: in a deployment they live in each domain's Glue catalog. Here columns.json stands in for that catalog. Each column's tags are checked against the ontology when the server starts, and CI runs the runtime's three example pipelines and fails if a column name or type in columns.json differs from the Parquet they write.
The IBAN question, on this example
§ 1 of the post opens with a domain engineer who is tagging a new column and asks which sensitivity level applies to one holding an EU IBAN. The answer should come from how comparable columns are already classified, not from what a model knows about IBANs. Against this example, it goes like this.
Find the precedent. find_datasets with {"subject_type": "customer"} returns two datasets that hold customer data, fraud/transactions_scored and policy/claims_deduped, each with an IBAN column. get_dataset on the first shows how that column is tagged and who to ask about it:
{
"metadata": {"name": "transactions-scored-daily", "domain": "fraud", "owner": "fraud-data@example.com"},
"contract": {"freshness_sla": "PT4H",
"lf_tags": {"sensitivity": "high", "residency": "eu", "subject_type": "customer"}},
"columns": [
{"name": "iban", "type": "VARCHAR",
"lf_tags": {"domain": "fraud", "sensitivity": "high", "residency": "eu", "subject_type": "customer"}}
]
}(trimmed to the one column)
Validate the proposal. resolve_lf_tag with {"sensitivity": "high", "residency": "eu", "subject_type": "customer"} returns the tag set unchanged, because every value is in the ontology. A value that is not fails with the permitted ones, which is the executor's own error:
lf_tag residency='eea' is not in the ontology; allowed: ['eu', 'ch', 'us', 'global']Validation catches a value nobody defined; it cannot catch a legal value that is wrong for this column. residency records where the data is stored, so an IBAN column in an extract replicated across regions should be global, and resolve_lf_tag accepts eu and global alike. That is the failure § 1 describes, and the reason the precedent lookup comes first.
Check who could read it. The analyst role in the reference topology may read every sensitivity except high. check_grant_expression with that role's expression, {"domain": ["fraud"], "sensitivity": ["public", "internal", "confidential"], "residency": ["eu"]}, returns {"reaches": false, "columns": []} for transactions_scored. In this example the whole table is classified high, so the role reaches nothing in it. The reference topology's Terraform tags only the iban column of its customer table high, and there the same role sees the table with the IBAN column left out. The IBAN columns here carry their own high tag as well, so that they stay high if a table's classification is ever lowered.
Read-only, and how that is enforced
Every tool carries readOnlyHint: true, but § 4 of the post argues that annotation is display metadata and enforces nothing. What enforces it here is that no handler writes: there is no write path to remove, the catalog is loaded once into an immutable mapping, and every object it hands out is a frozen Pydantic model. The tests call a write tool that does not exist and check that it returns an error and that the next read is unchanged.
Serving it over HTTP
uvx cordata-catalog-mcp --http --issuer https://idp.example --public-key issuer.pem \
--resource-url https://catalog.example/mcpOver HTTP every request needs a bearer token: a JWT access token in the RFC 9068 shape, signed ES256 by the one issuer named with --issuer, whose audience is this server's own URL (--resource-url, by default http://HOST:PORT/mcp). A token minted for any other resource is refused, which is the RFC 8707 audience binding the MCP authorization spec requires, and the server never passes a token on to anything else.
Access is per domain. A token needs catalog:read to reach the server at all, and catalog:{domain}:read for each domain it may see — catalog:fraud:read, for example. The responses follow § 5 of the post:
No token, or an invalid one:
401, with aWWW-Authenticateheader whoseresource_metadatapoints at/.well-known/oauth-protected-resource/mcp. That document is the RFC 9728 protected-resource metadata, and it names the issuer a client should get a token from.A request for a domain the token has no scope for:
403, witherror="insufficient_scope"andscope="catalog:{domain}:read", so the client knows which scope to ask for. The server does not answer with an empty result, which a client could not tell apart from a dataset with nothing in it.Lists —
list_domainsandfind_datasets— show only the domains the token may read. List results are markedcacheScope: private, because they differ per caller and a shared cache must not serve one caller's catalog to another.
The scope is checked again inside each handler on every call, so holding a token that read one domain grants nothing for the next request. The tests cover each of these against a throwaway issuer, a key pair generated in the test suite; no real identity provider is involved.
Deploying it against AWS
v1 makes no AWS calls; it serves the example data only. A deployment would read the ontology from lakeformation.list_lf_tags() in the governance account, the columns from each domain's Glue catalog, and ownership from DataZone. Three things about the identity chain were settled while designing it, and are recorded here because they are easy to get wrong:
Use
sts:AssumeRoleunder the server's own identity, neverassume_role_with_web_identity. Federating the caller's token into AWS would forward a token this server accepted as its own audience to a third party, which the MCP authorization spec prohibits. The caller travels as the session name and a session tag instead.Sanitise
RoleSessionName. STS accepts only[\w+=,.@-], up to 64 characters, and an OIDCsubcan contain|(Auth0 issuesauth0|abc123).Allow
sts:TagSessionin the reader role's trust policy. Without it the session tags are refused, and the caller's identity never reaches CloudTrail.
Because AWS never sees the caller's token, no IAM policy can condition on it, and the scope check stays in application code. That is the reason each reader role should be scoped to a single domain.
Development
pip install -e ".[dev]"
ruff check . && ruff format --check .
pytestThe column check needs a warehouse the runtime's example pipelines have written, from a clone of cordata-tech/pipeline-runtime at the installed version; .github/workflows/ci.yml shows the steps.
CORDATA_WAREHOUSE=../pipeline-runtime/example/warehouse python -m tools.check_columnsLicence
MIT. See LICENSE.
Available Tools
5 toolscheck_grant_expressionWould a grant reach a datasetBRead-onlyIdempotent
Would this tag-based grant expression reach this dataset, and which columns?
| Name | Required | Description | Default |
|---|---|---|---|
| table | Yes | Table name, e.g. transactions_scored | |
| domain | Yes | Owning domain, e.g. fraud or policy | |
| expression | Yes | Tag key to permitted values, e.g. {'sensitivity': ['high']} |
Output Schema
| Name | Required | Description |
|---|---|---|
| columns | Yes | The columns the expression reaches |
| reaches | Yes | Whether the expression reaches any column |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is fully covered by structured data. The description adds only the useful hint that the result includes matched columns, which is somewhat redundant with the existing output schema; it says nothing about how tag matching is resolved or whether results are cached.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with zero filler; the core question comes first and the return hint second. It is tight, though arguably too terse for a tool taking a required nested-object expression.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation, and annotations carry the mutation/safety story. However, for a tool with three required parameters including a nested tag-to-values object, the description provides no framing on how expressions are interpreted or what a negative result implies.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all three parameters (domain, table, expression) plus the nested tag-map shape are documented in the schema itself with examples. The description contributes no additional parameter meaning, which is the expected baseline when the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific evaluation action ('would this tag-based grant expression reach this dataset') plus an output dimension ('which columns'), so an agent knows this is a hypothetical grant-resolution check rather than a lookup. It doesn't need to differentiate from siblings like find_datasets or get_dataset, which are unrelated operations, but it also never names them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no explicit when-to-use guidance, no prerequisites, and no mention of alternatives such as get_dataset for actually fetching an existing dataset. The question phrasing weakly implies a policy-authoring/verification context, but the agent must infer that entirely.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_datasetsFind datasets by tagARead-onlyIdempotent
Which datasets carry these tag values — for example subject_type=customer.
Answers which datasets hold a kind of data, and in which columns. It cannot say where one person's rows are: that is a query against the data, and this server never reads data. Only domains this caller may read.
| Name | Required | Description | Default |
|---|---|---|---|
| tags | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| datasets | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, so the bar is lower. The description still adds real behavioral context beyond them: the server never reads data, and results are scoped to 'only domains this caller may read' – a permission constraint not visible in the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core question, and each sentence adds scope or a boundary. The fragmentary 'Answers which datasets hold a kind of data...' slightly restates the opening line, costing a point.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no prose. Between the purpose, the negative scope (no row-level data), and the permission scoping, an agent has everything needed to decide and call correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and the single parameter is a nested string map, so the description must carry the load. The example 'subject_type=customer' usefully illustrates the key=value shape, but it does not explain multi-tag semantics (AND vs OR, whether all tags must match).
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('which datasets carry these tag values') and clarifies the answer shape ('which datasets hold a kind of data, and in which columns'). It does not name any sibling (list_domains, get_dataset, resolve_lf_tag) to differentiate, so it falls short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Clear usage context is given: use it to map tag values to datasets/columns, and it explicitly excludes row-level queries ('it cannot say where one person's rows are'). However, no alternative tool is named for cases the agent might actually want, leaving routing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_datasetDataset lookupBRead-onlyIdempotent
Schema, ownership and policy tags for one published dataset.
| Name | Required | Description | Default |
|---|---|---|---|
| table | Yes | Table name, e.g. transactions_scored | |
| domain | Yes | Owning domain, e.g. fraud or policy |
Output Schema
| Name | Required | Description |
|---|---|---|
| columns | Yes | |
| contract | Yes | |
| metadata | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and destructiveHint=false, so the safety profile is covered. The description adds the meaningful constraint that only 'published' datasets are returned, but it says nothing about authorization scope or behavior for unpublished/nonexistent datasets. A 3 reflects modest added value over the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One compact sentence, front-loaded with the returned attributes, with no redundant or filler text. Every word carries information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description need not explain return values, and annotations cover safety semantics, so a short scope statement is nearly sufficient. It could still note lookup-by-domain+table behavior or failure behavior, which is the only gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% – both domain and table are documented with examples in the schema. The description adds no additional parameter meaning (e.g., format constraints or domain scoping rules), so the schema does the heavy lifting and the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names the concrete return payload (schema, ownership, policy tags) and scopes it to 'one published dataset', so the agent knows exactly what it retrieves. It does not, however, distinguish itself from siblings such as find_datasets or resolve_lf_tag, which a reader must infer.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no statement of when to use this tool versus alternatives like find_datasets (search) or list_domains. The phrase 'one published dataset' hints at a targeted lookup, but no exclusions or prerequisites are given.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_domainsDomains and ownersARead-onlyIdempotent
What domains exist, and who owns them. Only the domains this caller may read.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| domains | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and destructiveHint=false, so the safety profile is covered. The description adds a genuine behavioral trait beyond that: results are filtered to "only the domains this caller may read", which tells the agent the output is authorization-scoped and may be incomplete relative to a full domain list.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences, no filler, with the primary purpose front-loaded and the access-scoping caveat immediately after. Every clause carries information an agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and annotations carry the safety profile; the description therefore only needs purpose plus access scoping, which it provides. It stops short of noting any cap, pagination or ordering behavior, which is a minor residual gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so there is nothing for the description to disambiguate; the baseline for a parameterless tool is 4. The schema's empty properties object is consistent with the description's implied take-everything-visible semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States the resource (domains) and the returned attribute (owners) explicitly, so the agent knows it enumerates domains. The verb is implicit ("What domains exist") rather than a direct "List domains", and it does not differentiate itself from siblings like find_datasets or get_dataset, though those operate on a different resource so there is little real confusion.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied rather than stated: an agent can infer this is the enumeration tool for domains and that the caller's read permissions cap the result set. There is no explicit when-to-use/when-not guidance and no pointer to an alternative tool for a narrower lookup.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
resolve_lf_tagValidate a tag setARead-onlyIdempotent
Is this tag set valid against the governance-owned ontology?
Runs the same policy.resolve() the executor runs, and fails the same way.
| Name | Required | Description | Default |
|---|---|---|---|
| tags | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| tags | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and destructiveHint=false, so safety is covered. The description goes beyond them by disclosing behavioral fidelity: it replicates the executor's resolution path and failure mode, which is meaningful context for a validation tool. It does not, however, mention anything about ordering, partial validity, or the error shape beyond 'fails the same way.'
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences with the core question front-loaded, and the executor-parity note immediately after. The line break between them is slightly awkward but no sentence is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described, and annotations carry the safety profile. Combined with the executor-parity note, an agent has enough to call this correctly; only the tag-format detail for the lone parameter is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% for the single `tags` parameter, and the description adds only the phrase 'tag set' plus the ontology framing — it does not explain the map's key/value shape, tag syntax, or which namespace the keys represent. It compensates partially by grounding validation in a 'governance-owned ontology,' but the parameter remains underspecified.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description poses a precise question — is this tag set valid against the governance-owned ontology — which names a specific resource (tag set) and operation (validate/resolve). It is clearly distinct from the dataset/domain/grant siblings, though it never names them. Slightly indirect for stating purpose in question form rather than declaratively.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It implies usage by saying it 'runs the same policy.resolve() the executor runs, and fails the same way,' which tells an agent this is a pre-flight check for executor-time behavior. However, there is no explicit when-to-use/when-not guidance and no alternative sibling is named for comparison.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
5 tool updates
v0.1.0- First observed
check_grant_expression - First observed
find_datasets - First observed
get_dataset - First observed
list_domains - First observed
resolve_lf_tag
TDQS
Scored across 5 tools
Most tools have clearly distinct purposes: listing domains, validating tags, checking grant expressions, finding datasets, and fetching one dataset. However, resolve_lf_tag and check_grant_expression both involve policy evaluation and could be confused without careful reading of their descriptions.
All names follow a consistent snake_case verb_noun pattern: list_domains, resolve_lf_tag, check_grant_expression, find_datasets, get_dataset. Minor singular/plural variations are semantically appropriate rather than inconsistent.
Five tools is well-scoped for a read-only catalog and governance server. Each tool covers a distinct capability—domain listing, tag validation, grant checking, dataset search, and dataset retrieval—without redundancy.
The surface covers core read-only governance queries but lacks a way to enumerate all datasets (e.g., list_datasets by domain or without tags) and has no tools for listing tags, ontologies, or policies. These notable gaps could block an agent trying to browse the catalog comprehensively.
Maintenance
Related MCP Connectors
Read public model catalog metadata with freshness timestamps. No model execution or payments.
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Query OneLens cloud-cost data in natural language: breakdowns, trends, cost centers. Read-only.
Read-only AgentiScript concept search, catalog, authenticity, license, and approved asset discovery.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceEnables semantic search and exploration of a business glossary knowledge graph, allowing users to query terms, expand related concepts, trace dependencies, and resolve physical table mappings.-
- AlicenseAqualityCmaintenanceEnables querying Tableau lineage through the Metadata API via MCP, supporting impact analysis, where-used lookups, and content search. Provides read-only tools for answering natural-language questions about workbooks, datasources, fields, and dependencies.7212 PyPI1MIT
- AlicenseNot gradedqualityBmaintenanceEnables natural language queries across AWS multi-account security scan history, including infrastructure configurations, public exposures, and organizational relationships.MIT
- FlicenseNot gradedqualityCmaintenanceEnables composable search and filtering over an enterprise data catalog, with tools for lineage, sample data, profiling, history, quality rules, and AI-driven classification.-