I closed the basou introduction with a promise: that there's a companion project, Kozou, for the side that tells an agent what a database means, and that I'd introduce it separately. This is that post.
In one line, Kozou is an open-source tool that gives an AI agent the meaning of your PostgreSQL database. The README opens with it: "Give your AI agent the meaning of your PostgreSQL database, not just its columns." It reads the meaning sitting in your schema — COMMENT ON text, view definitions, type information — and hands it to the agent over MCP (the standard protocol for connecting AI agents to external tools).
Columns it can read, meaning it can't
Let an AI agent loose on a database and you notice something fast. It's good at reading schema — it lists tables, lines up columns and types, assembles a plausible-looking JOIN. And it still gets the answer wrong. Plausibly wrong.
The demo schema bundled with Kozou's quickstart (a small online store) reproduces this failure well. The orders table has an amount_total numeric(12,2) column. A total. Asked for revenue, an agent reaches for SUM(amount_total) — the natural move. But that column is a stale denormalized cache the application stopped maintaining; it can disagree with the line items, and it mixes in test orders. The status column holds 'paid', 'refunded', and 'chargeback' among others, but only 'paid' counts as a sale — the other two reverse one.
None of that knowledge appears in the column names, the types, or the constraints — in the structure itself. It normally lives in a data dictionary, a team wiki, or someone's head. The agent writes a plausible query from the structure in front of it. What fills the gap between plausible and correct is this "meaning."
The meaning already has a home: the database
Before reaching for anything new, one thing is worth establishing. PostgreSQL has had a native place to write meaning all along.
COMMENT ON. Kozou adds a small set of conventions on top of it. The three that carry meaning are @ai / @policy / @example — there's also @widget for the Admin UI, among others. Here's the real thing from the demo schema (reflowed for readability):
COMMENT ON TABLE orders IS 'Customer orders.
@ai: An order is recognized revenue only when status = ''paid''
AND is_test = false AND deleted_at IS NULL and its customer is not
soft-deleted; value each line at order_items.unit_price
(the captured price), not products.list_price.
@ai: The vw_recognized_revenue view already applies every one of these
rules — start there for any revenue question.
@policy: ''refunded'' and ''chargeback'' reverse a sale;
never count them as revenue.';
Then views. If the knowledge of "how to compute revenue correctly" lives in someone's head, defining it as a view turns it into a named, executable concept. The demo's vw_recognized_revenue is the source of truth for recognized revenue: excluding test orders, accounting for soft deletes, valuing each line at the captured unit price — all baked into the definition.
Writing meaning into the database itself is a habit that predates Kozou and pays off on its own. The moment you write it, the meaning lives in the same place as the schema and migrates alongside it — a primary source in its own right.
But writing it isn't the same as delivering it to the agent. Over a raw connection an agent can read COMMENT if it digs through the catalog — but whether it digs is up to the agent. What tells it where things are written, which view is authoritative, which rules are advisory and which are enforced — a bare connection has none of that.
Handing that meaning over — this is Kozou
First, what Kozou doesn't do to your database. It doesn't stand up a new metadata store. It only reads the COMMENT and view definitions already in your DB — it creates no tables of its own and runs no DDL (Kozou is migration-tool-agnostic; it compiles the schema your migrations produce). Remove it, and you're back to plain PostgreSQL.
With that settled: Kozou reads the PostgreSQL schema and hands the COMMENT ON text, view definitions, and type information to the agent over MCP — without summarizing or paraphrasing (the README's word is verbatim), structured by tag. Here's what the agent sees:
-
list_tables/describe_table— the table list, and the full schema + COMMENT for one table -
list_views/describe_view— a view's columns, purpose, underlying tables, and the view definition SQL itself -
list_concepts/get_concept_context— treats a view as a domain concept, returning its related tables and recommended query path -
describe_functions— lists the exposed RPC actions -
search_schema— metadata search across names, labels, COMMENT bodies,@ai/@policynotes, and enum members
For example, sending search_schema the word "revenue" brings back both the view named vw_recognized_revenue and the "do not use this for reporting" COMMENT on amount_total — one hit on a name, one on a body. You find where the meaning relevant to that word lives without enumerating and describe-ing the whole schema.
The default is describe-only — reading. The execution tool, call, appears only when you explicitly enable it in the config file (kozou.config.yaml, under server.mcp.execution) and name the role it runs as. It isn't a CLI flag — a plain kozou mcp is always describe-only; being able to execute is a state the operator opts into through configuration.
Here's a fragment of what describe_table("public.orders") actually returns (real excerpt from the demo schema, abridged to the relevant fields and reflowed for readability):
{
"name": "amount_total",
"dataType": "numeric(12,2)",
"aiDescription": "Do NOT use this for reporting — it is a stale cache
the application stopped maintaining, can disagree with the line items,
and includes test orders. Compute revenue from vw_recognized_revenue
instead."
},
{
"name": "status",
"enumValues": ["cart", "pending", "paid", "refunded", "chargeback"],
"aiDescription": "Only 'paid' is a captured sale; 'cart' and 'pending'
are not sales yet; 'refunded' and 'chargeback' reverse a prior sale."
}
The trap from earlier arrives right next to the column, as a note to the agent. An @ai: line from the COMMENT ON becomes aiDescription, a @policy: line becomes policy. The meaning you wrote comes back in the words you wrote it in, structured. An agent holding that note can head for the view declared authoritative — vw_recognized_revenue — instead of guessing its way through amount_total.
Relations work the same way. get_concept_context derives JOIN candidates from the real foreign keys among a view's underlying tables, and attaches the COMMENT on the FK constraint as the JOIN's purpose. If you've written a COMMENT on the FK constraint, you get a suggestion like this (this fragment is illustrative — the demo schema doesn't comment its FK constraints, so calling it as-is returns an empty purpose):
"joinSuggestions": [
{
"table": "public.customers",
"on": "orders.customer_id = customers.id",
"purpose": "the customer who placed the order"
}
]
A machine can derive the ON condition. But "what is this JOIN for" is the meaning a human wrote in the FK constraint's COMMENT. Kozou hands over both, as one suggestion.
There's a deliberate line here. Kozou hands meaning over; it doesn't enforce it. @policy is advisory for the agent; the actual access control stays on the PostgreSQL side — privileges and row-level security (RLS). Kozou tells the agent whether RLS is on, but never surfaces the policy expression itself. Supplying meaning and enforcing permissions are kept separate.
One definition, many faithful forms
MCP is the biggest differentiator, but it's one outlet among several. From the same read of the schema, Kozou also generates an Admin UI, REST + OpenAPI, Markdown docs, and TypeScript types. From one source — schema plus comments — it derives every form a human or an AI needs; no duplicate definitions, no drift. That "one source, many uses" stance is a post of its own, so I'll save it for another time.
flowchart LR
S[PostgreSQL schema<br>COMMENT ON / view definitions] --> K[Kozou]
K --> MCP[MCP context<br>AI agent]
K --> UI[Admin UI]
K --> API[REST + OpenAPI]
K --> DOC[Markdown docs]
K --> TS[TypeScript types]
Start in ten minutes
git clone https://github.com/kozou-dev/kozou
cd kozou/examples/quickstart
cp .env.example .env
docker compose up
That brings up PostgreSQL seeded with the trap-laden demo schema, plus kozou dev — the Admin UI on http://localhost:3333 and an MCP server on http://localhost:3334/mcp.
To try it over stdio against your own DB, without the Docker stack:
DATABASE_URL=postgres://user:pass@localhost:5432/mydb npx -y kozou mcp --stdio
Or to start it as your own project:
npx -p kozou create-kozou my-project
Per-client MCP setup, the remote-MCP (OAuth) guide, and the reference are all at kozou.org. I won't restate the steps — how to do it is what the docs are the source of truth for.
In summary
- An AI agent can read columns and types, but the meaning — which column is a trap, which view is authoritative — isn't in the names and types. That's where plausible, wrong queries come from.
- The meaning has a home in PostgreSQL already:
COMMENT ONand view definitions. Write the meaning into the DB first — a habit that predates Kozou and works on its own. - Kozou hands that meaning to the agent over MCP without summarizing or paraphrasing — a note beside each column, a purpose on each JOIN, a recommended query path for each concept. And it hands meaning over while leaving enforcement to PostgreSQL.
The repository is github.com/kozou-dev/kozou (Apache-2.0); the docs are at kozou.org. The latest release at the time of writing is v1.15.1. If something trips you up, I'd be grateful for an issue.
The Japanese version of this post — its "paired" article — is already live.
Top comments (2)
The strongest idea here is the view as an executable domain concept. A comment can say “recognized revenue,” but the view can make that definition testable.
The remaining risk is semantic drift inside the database itself:
COMMENT ONmigrates with the schema, yet it can still describe a rule the application no longer follows. I would pair these conventions with CI checks that referenced views/columns exist, contract fixtures that validate authoritative views, an owner plus reviewed-at/version field, and a migration rule that flags changed view definitions without a corresponding semantic review. The MCP result should also carry provenance—database/schema version, object identity, effective role, and the exact concept/view used—so an answer can explain which definition produced it. That turns “meaning near the data” into meaning that is reviewable, versioned, and auditable rather than another trusted text layer.Thanks, Mads — this is a really thoughtful read, and you've put your finger on
the honest risk: COMMENT ON travels with the schema, but nothing guarantees
it still matches what the application actually does. Drift is the real enemy.
The provenance point resonates most with me — carrying schema version, object
identity, effective role, and the exact view a given answer relied on. That's
what moves this from "meaning you trust" to "meaning you can audit." The CI
checks and a reviewed-at/owner field sit naturally alongside that.
Appreciate you taking the time to lay it out so clearly — genuinely useful.