Connect an AI agent over MCP
Kozou ships an MCP server that
exposes your PostgreSQL schema as structured context for AI agents.
Once connected, an agent such as Claude Code or Claude Desktop can read
what your tables, views, and business concepts mean — the descriptions,
@ai notes, and example queries you wrote in COMMENT — through seven
read-only tools.
This page covers both transports: stdio, for a local agent that
launches the server itself, and HTTP, for Docker or remote use. For
the command reference, see kozou mcp. For what each
tool returns, see The three surfaces.
Before you start
Section titled “Before you start”You need a Kozou project with a reachable PostgreSQL database. The server
reads a DATABASE_URL connection string — the same one the rest of the
CLI uses, expanded into kozou.config.yaml through its ${DATABASE_URL}
placeholder. A generic connection string looks like:
postgres://USER:PASSWORD@HOST:5432/DBNAMEYou do not need to write any application code. The server reads your
schema and COMMENT text and serves them as-is.
Connecting to a hosted database (Supabase, Neon, RDS, …)? Managed
PostgreSQL requires TLS, and Kozou passes your DATABASE_URL to the driver
as-is without adding SSL — so append an sslmode parameter, or the server
rejects the connection.
Whether sslmode=require connects depends on the provider’s certificate. A
publicly-trusted cert (e.g. Neon) currently verifies against Node’s
built-in CA store, so require works as-is:
postgres://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=requireProviders that present a private CA — Supabase and AWS RDS among
them — won’t verify against that store, so require fails with a certificate
error. Use one of these instead:
# Secure — verify against the provider's own CA (download it from their dashboard):postgres://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=verify-full&sslrootcert=/path/to/ca.crt# Simplest — encrypt without verifying the certificate (avoid over untrusted networks):postgres://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=no-verifyDon’t have a database yet? Run the Quickstart demo first —
docker compose up brings up a seeded PostgreSQL you can point this guide at,
with the ready-made connection string postgres://kozou:kozou@localhost:5432/kozou.
The stdio path: Claude Code, Claude Desktop, Cursor
Section titled “The stdio path: Claude Code, Claude Desktop, Cursor”For a local agent, register kozou mcp --stdio as an MCP server in the
client’s config. The agent spawns the process on demand and talks to it
over standard input/output — nothing listens on a port.
Add an entry under mcpServers. This shape works for any MCP client that
follows the standard config format — Claude Code, Claude Desktop, and
Cursor all do — though each client keeps it in a different place (see
Where to register it below):
{ "mcpServers": { "kozou": { "command": "npx", "args": ["-y", "kozou", "mcp", "--stdio"], "env": { "DATABASE_URL": "postgres://USER:PASSWORD@HOST:5432/DBNAME" } } }}A few notes on this entry:
npx -y kozou mcp --stdioruns the publishedkozoupackage without a global install; the-yskips the install prompt. If you have already installedkozouglobally, you can set"command": "kozou"and drop"kozou"fromargs.DATABASE_URLis read by the bundled CLI through the${DATABASE_URL}placeholder inkozou.config.yaml. Set it in theenvblock as shown rather than relying on the client’s ambient environment.stdiois the default transport, so--stdiois explicit but matches the default. The server installs aSIGHUPhandler that refreshes its cached schema, so a long-lived process can pick up DDL orCOMMENTchanges without a restart.
Where to register it
Section titled “Where to register it”The JSON above is the shape; each client keeps it in a different place.
Claude Code (CLI) — the quickest path is claude mcp add, which writes
the entry for you:
claude mcp add kozou \ --env DATABASE_URL="postgres://USER:PASSWORD@HOST:5432/DBNAME" \ -- npx -y kozou mcp --stdioEverything after -- is the server command, passed through untouched. Add
--scope project to write a shared .mcp.json at the project root (checked
into git for your team) instead of your personal config, and run
claude mcp list to confirm it connects — it should report kozou … ✓ Connected.
Claude Desktop — open Settings → Developer → Edit Config (it creates
the file if needed) and add the entry under mcpServers. The file is at:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
Quit and reopen the app fully after saving — it reads the config at launch.
Once reopened, the 🔨 tools icon in the message composer should list a
kozou server with its tools.
Cursor — add the entry to .cursor/mcp.json in the project root (or
~/.cursor/mcp.json to make it global), or use Settings → Features → MCP →
Add New MCP Server. Restart Cursor after editing. Settings → Features → MCP
then shows kozou with a green dot and its tool count.
The seven tools then appear to the agent under the kozou server name. If they
don’t, work through When the tools don’t appear.
The HTTP path: Docker and remote
Section titled “The HTTP path: Docker and remote”For a containerized or remote setup, run the server over HTTP instead. This is the transport a standard MCP client speaks when it connects to a URL rather than launching a process:
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DBNAME \ npx kozou mcp --http --port 3334The HTTP server listens on port 3334 by default and serves the MCP
endpoint at /mcp. It also exposes POST /admin/refresh, the HTTP-mode
counterpart to the stdio SIGHUP handler, which invalidates the cached
schema so the next request re-reads your database.
kozou dev brings up this same HTTP server alongside the Admin UI — the
UI on port 3333 and the MCP HTTP server on 3334 — wired from
kozou.config.yaml. If you are already running kozou dev, the MCP
endpoint is live at http://localhost:3334/mcp without a separate
command. See kozou dev and
Generate an Admin UI.
To point a client at that URL, register it the same way as the stdio entry above but in HTTP form. With Claude Code:
claude mcp add --transport http kozou http://localhost:3334/mcpOr, hand-editing claude_desktop_config.json / .cursor/mcp.json, use the
URL shape instead of command / args:
{ "mcpServers": { "kozou": { "type": "http", "url": "http://localhost:3334/mcp" } }}Binding and exposure
Section titled “Binding and exposure”By default the HTTP server binds to 127.0.0.1 (loopback only). You can
change the listener with --host and --port:
| Flag | Default | What it does |
|---|---|---|
--port <n> | 3334 | TCP port the HTTP server listens on. |
--host <addr> | 127.0.0.1 | Interface to bind. |
The MCP HTTP transport ships unauthenticated by default. If you
bind to a non-loopback host, the server prints a loud warning: anyone who
can reach that address can read the database’s schema metadata. The tools
only expose schema metadata — no SQL execution and no data access, which
bounds the blast radius — but you should still keep the server on
loopback unless it sits behind a trusted network and an external
auth/proxy layer, or you turn on the OAuth resource-server mode
(server.mcp.http.auth), which requires a verified bearer token from
your own identity provider on every request — see
Remote MCP with OAuth.
Try it: see the difference
Section titled “Try it: see the difference”Once the kozou tools appear, ask your agent a question whose answer depends on
business rules, not column types. Against the Quickstart demo:
Using the kozou tools, what is our total recognized revenue? Explain which rows and columns you excluded and why.
A Kozou-informed agent calls describe_table / get_concept_context, finds the
vw_recognized_revenue view, and answers 120.00 — naming the internal test
order and the soft-deleted rows it left out. Ask the same question without the
tools and it returns a plausible wrong number (the demo’s obvious query is off by
4.8×). That contrast is the value Kozou adds.
What the agent can do once connected
Section titled “What the agent can do once connected”The server exposes seven tools, all read-only context providers:
| Tool | Returns |
|---|---|
list_tables | Table names with their labels, descriptions, and a planner row-count estimate. |
describe_table | The full schema and COMMENT for one table: columns, types, nullability, primary key, foreign-key relations, and check constraints. |
list_views | View names with their labels and purposes. |
describe_view | A view’s columns, purpose, the tables it depends on, and its SQL definition. |
list_concepts | The domain concepts, each backed by a view. |
get_concept_context | A concept’s related tables, a preferred query source, join suggestions, and example queries. |
describe_functions | The signatures of functions exposed as RPC actions (@expose: rpc), with their @ai and @policy advisory notes. |
With these, an agent reads not just the shape of your schema but its
meaning — and the recommended way to query it. describe_table,
describe_view, and get_concept_context each carry the AI-facing notes
you wrote with the @ai tag, so guidance like “for revenue figures,
prefer the view vw_orders_paid” reaches the agent directly. Example
queries written with @example surface through get_concept_context as
a list of { description, sql } entries — the recommended query paths for
a concept. (For how those tags are parsed, see
COMMENT conventions.)
For example, an agent asked about an orders table calls
describe_table to learn its columns and foreign keys, then
get_concept_context on a paid-orders concept to find the suggested
FROM source and an example revenue query — all without you pasting any
schema into the prompt.
Tell the agent what it may touch
Section titled “Tell the agent what it may touch”By default the describe tools are schema-wide — they show every table and
column regardless of who is asking. Opt in by pointing Kozou at a role
(introspection.respectPrivileges: true with a
role) and describe_table / describe_view additionally annotate each
relation with that role’s effective privileges: the table-level SELECT /
INSERT / UPDATE / DELETE it holds, plus per-column insertable /
updatable flags. So an agent knows, before it tries, that it may read orders
but not write them — the wedge a query layer that enforces but doesn’t explain
can’t give an agent.
Unlike the Admin UI’s privilege mode, the MCP tools
annotate rather than hide: a table the role cannot even SELECT still
appears, marked "select": false, so the agent is told its limits rather than
left to discover them by failing. It reuses the privileges Kozou already reads
(no extra queries) and is advisory only — enforcement always stays in PostgreSQL
(the role’s GRANTs and your RLS policies). kozou docs carries the same
information as a per-table Security section. When the opt-in call execution
tool is enabled, the annotated role is tied to the role calls run as, so what the
agent is told and what it can do never disagree.
Tell the agent a table is row-filtered
Section titled “Tell the agent a table is row-filtered”Table-level privileges answer may I touch this table at all. Row-level
security answers a finer question — which rows, and will this write be
accepted — and Kozou surfaces that too. describe_table reports a
rowSecurity signal on any table protected by
RLS: whether it
is enabled, whether it is forced (applies to the table owner as well), and
whether any policy exists. When RLS is on, the tool adds a plain-text note
telling the agent the rows it sees may be filtered and a write may be rejected —
so it stops treating a result as the whole story.
This signal is on by default: it is a structural fact about the table, not a
per-role privilege, so it needs no opt-in. It is reported on tables only — a
view carries no RLS flag of its own, and whether it masks its underlying tables
depends on security_invoker, so Kozou won’t claim a view is unfiltered. One
case worth knowing: a table with RLS enabled but no policy is default-deny —
non-owner roles see and write nothing — and the note says so.
Crucially, Kozou reads only the booleans — never the policy expressions
(USING / WITH CHECK). Those predicates encode your authorization model, so
they stay in the database and out of the agent’s context. Knowing a table is
row-filtered never lets the agent bypass the filter: PostgreSQL enforces RLS
regardless of whether the agent knows about it. kozou docs carries the same
line per table.
The read-only safety boundary
Section titled “The read-only safety boundary”None of the seven tools generate SQL, execute SQL, or write data. They
return schema metadata and COMMENT text and nothing else — describe_functions
lists the signatures of exposed RPC actions, but running an action belongs to
the REST surface, not to MCP. An agent
connected to Kozou over MCP can read what your schema means but cannot
mutate your data through this surface — there is no write path in the
tool set, on either transport.
This is also why the HTTP server can bind to loopback and warn loudly off
it: the worst case for an exposed MCP endpoint is disclosure of schema
metadata, not data loss. If your agent needs to read or write rows, that
belongs to a separate surface — the REST API (the in-house @kozou/api by
default since v1.0, or an external PostgREST if you opt out with
kozou dev --adapter postgrest) — not to MCP.
Pointing an agent at production safely
Section titled “Pointing an agent at production safely”Two deployment choices keep the blast radius small, and they compose with the signals above:
- Scope the introspection. Kozou only sees the schemas you point it at. Keep your most security-sensitive tables out of scope so the agent never reasons about them at all.
- Connect as a role with broad, read-mostly visibility. Because the
rowSecuritysignal is role-independent, the agent is still told which tables are row-filtered even when the connecting role’s own policies rarely filter it — honest context without handing the agent a privileged write path.
When the tools don’t appear
Section titled “When the tools don’t appear”If the agent doesn’t list the kozou tools after you connect, work through
these in order:
- Run the server by hand. The fastest check is to start it yourself and
read the error:
It should start and wait on standard input. The server connects to the database lazily, so it starts and lists its tools even when
Terminal window DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DBNAME npx -y kozou mcp --stdioDATABASE_URLpoints somewhere wrong (a bad host, port, or credentials) or the database is unreachable — that problem surfaces later, at the first tool call (see The tools appear but every call fails), not here. If instead the process exits immediately, the cause is the command, environment, or config — a missingnpx, a wrong package name, a broken install, or a bad config file (a parse error inkozou.config.yaml, a missing required field, or an empty/absentDATABASE_URL) — and the client only reports that the server “failed,” not why, so this is where you read the real error. - Check the status (Claude Code).
claude mcp listreports each server as connected or failed;claude mcp get kozoushows the resolved command and environment. - Use an absolute path for
npx. If the client can’t findnpx(aspawn ENOENTerror), put the full path incommand— find it withwhich npx(for example/usr/local/bin/npx). - Validate the JSON. A trailing comma or an unquoted key makes the whole
config fail silently. Check it with
python3 -m json.tool < <config-file>. - Restart after editing. Claude Desktop and Cursor read their config at
launch — quit and reopen the app fully. Claude Code reloads
.mcp.jsonat the start of a session. - The first run can be slow.
npx -ydownloads the package the first time it runs; if the client times out, setMCP_TIMEOUT=60000(Claude Code) or reopen once the download has finished.
Claude Desktop writes each server’s logs to ~/Library/Logs/Claude/ (macOS)
or %APPDATA%\Claude\logs\ (Windows) — the mcp-*.log files carry the
server’s own output.
The tools appear but every call fails
Section titled “The tools appear but every call fails”If the kozou tools are listed but every call returns “Schema is currently
unavailable”, the server started and reached the client fine — it just can’t
reach your database. This is a DATABASE_URL problem, not an MCP one: check the
host, port, and credentials, and, for a hosted database, the sslmode
parameter — the most common culprit (see Before you start).
Running the server by hand (step 1 above) prints the underlying PostgreSQL
error on standard error.
Where to next
Section titled “Where to next”kozou mcp— the command reference: flags, transports, and the refresh endpoint.- COMMENT conventions — how
@ai,@example, and the other tags shape what the agent sees. - The three surfaces — how MCP context fits alongside the Admin UI and the REST API.