Skip to content

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.

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/DBNAME

You 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=require

Providers that present a private CASupabase 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-verify

Don’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 --stdio runs the published kozou package without a global install; the -y skips the install prompt. If you have already installed kozou globally, you can set "command": "kozou" and drop "kozou" from args.
  • DATABASE_URL is read by the bundled CLI through the ${DATABASE_URL} placeholder in kozou.config.yaml. Set it in the env block as shown rather than relying on the client’s ambient environment.
  • stdio is the default transport, so --stdio is explicit but matches the default. The server installs a SIGHUP handler that refreshes its cached schema, so a long-lived process can pick up DDL or COMMENT changes without a restart.

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:

Terminal window
claude mcp add kozou \
--env DATABASE_URL="postgres://USER:PASSWORD@HOST:5432/DBNAME" \
-- npx -y kozou mcp --stdio

Everything 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:

OSPath
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.

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:

Terminal window
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DBNAME \
npx kozou mcp --http --port 3334

The 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:

Terminal window
claude mcp add --transport http kozou http://localhost:3334/mcp

Or, 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" }
}
}

By default the HTTP server binds to 127.0.0.1 (loopback only). You can change the listener with --host and --port:

FlagDefaultWhat it does
--port <n>3334TCP port the HTTP server listens on.
--host <addr>127.0.0.1Interface 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.

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.

The server exposes seven tools, all read-only context providers:

ToolReturns
list_tablesTable names with their labels, descriptions, and a planner row-count estimate.
describe_tableThe full schema and COMMENT for one table: columns, types, nullability, primary key, foreign-key relations, and check constraints.
list_viewsView names with their labels and purposes.
describe_viewA view’s columns, purpose, the tables it depends on, and its SQL definition.
list_conceptsThe domain concepts, each backed by a view.
get_concept_contextA concept’s related tables, a preferred query source, join suggestions, and example queries.
describe_functionsThe 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.

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.

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.

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.

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 rowSecurity signal 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.

If the agent doesn’t list the kozou tools after you connect, work through these in order:

  1. Run the server by hand. The fastest check is to start it yourself and read the error:
    Terminal window
    DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DBNAME npx -y kozou mcp --stdio
    It should start and wait on standard input. The server connects to the database lazily, so it starts and lists its tools even when DATABASE_URL points 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 missing npx, a wrong package name, a broken install, or a bad config file (a parse error in kozou.config.yaml, a missing required field, or an empty/absent DATABASE_URL) — and the client only reports that the server “failed,” not why, so this is where you read the real error.
  2. Check the status (Claude Code). claude mcp list reports each server as connected or failed; claude mcp get kozou shows the resolved command and environment.
  3. Use an absolute path for npx. If the client can’t find npx (a spawn ENOENT error), put the full path in command — find it with which npx (for example /usr/local/bin/npx).
  4. 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>.
  5. Restart after editing. Claude Desktop and Cursor read their config at launch — quit and reopen the app fully. Claude Code reloads .mcp.json at the start of a session.
  6. The first run can be slow. npx -y downloads the package the first time it runs; if the client times out, set MCP_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.

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.

  • 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.