← All articles Guides

Connect Postgres to an AI Agent Without Handing It Raw SQL

OBTO Team · Insights from the Glass Box

Your operational data lives in Postgres. Your AI agent, out of the box, has no idea it exists. Closing that gap is what separates an assistant that writes plausible-sounding answers from one that can tell you which fifty accounts churned last quarter — and be right every time.

There is an easy way to connect the two, and there is a right way. They are not the same. This guide covers both, then lands on the pattern that lets an agent read your database without becoming a liability.

The shortcut everyone tries first

The fastest path is to hand the agent your connection string and let it write SQL. Models are genuinely good at text-to-SQL now, so a demo comes together in an afternoon: the agent turns "top accounts by overdue balance" into a SELECT, runs it, and reads the rows back in plain English. It feels like magic, and for a demo, it is.

Then it meets production, and the cracks show:

Text-to-SQL is a great party trick. It is a poor foundation for a system you have to trust.

The pattern that holds up: tools, not raw SQL

Instead of giving the agent a database, give it a menu. Expose Postgres through a small set of typed tools, each one a named capability with defined inputs and outputs:

The agent never sees SQL. It picks a tool, fills in the parameters, and gets structured data back. The query is written once, by you, and runs server-side with its parameters bound — so there is no injection surface and no path to a table you did not expose. This is the Model Context Protocol pattern, and it is the same approach in our guide to building an MCP tool: a tool is a contract, not a blank check.

A read tool is a few lines of server-side code. The shape that matters:

// list_overdue_invoices — a typed, read-only Postgres tool
async function listOverdueInvoices({ min_days_overdue = 30, limit = 50 }) {
  const sql = `
    SELECT id, account, amount_due, due_date
    FROM invoices
    WHERE status = 'open'
      AND due_date < NOW() - ($1 * INTERVAL '1 day')
    ORDER BY due_date ASC
    LIMIT $2`;
  return await db.query(sql, [min_days_overdue, limit]);
}

Parameters are bound ($1, $2), the row count is capped, and the only verb in play is the one you wrote. The agent gets exactly the capability you intended, and nothing adjacent to it.

Four rules for a connection an agent cannot abuse

  1. Connect with a read-only role. Create a dedicated Postgres user that can SELECT from the specific tables your tools need, and nothing else. If a tool only reads invoices, its role cannot see the users table. Least privilege is the backstop that catches every other mistake.
  2. Bind parameters, bound results. Every query is parameterized; every query carries a LIMIT and a statement timeout. A runaway request should fail fast and cheap, not lock a table or flood the context window.
  3. Keep the connection string server-side. The credential lives in the tool's runtime as an encrypted secret. Not in the prompt, not in the client, not in the model's context. The model asks for data; it never holds the keys.
  4. Log every call. Record which tool ran, with which parameters, what executed, how many rows came back, and what it cost. When an agent does something surprising, that log is the difference between a five-minute fix and a guessing game.

Where the connection string actually lives

This is the detail most tutorials skip. If your framework expects DATABASE_URL in an environment variable sitting next to the model loop, you have coupled your credentials to your inference. On OBTO, a tool is server-side code with its own secret store: the connection string is encrypted at rest and read at runtime by the tool, not the model. The agent calls list_overdue_invoices; the platform resolves the secret, runs the query, and returns rows. The credential never travels with the conversation.

Hosting matters too. A Postgres tool is only useful if your agent can reach it wherever it runs, which is the job our guide to hosting a remote MCP server walks through.

What you can see afterward

Because every tool call is a discrete, logged event, you get an audit trail without building one. OBTO's Glass Receipt records each call as a line you can query:

{
  "tool": "list_overdue_invoices",
  "params": { "min_days_overdue": 60, "limit": 50 },
  "rows_returned": 12,
  "ms": 38,
  "cost": { "total": 0.0008 }
}

That is not only useful for debugging. It is how you prove — to a teammate, an auditor, or yourself — exactly what an agent touched in your database and when. We made the case for that kind of trail in our piece on agent observability: the same plumbing that connects Postgres safely is what makes the connection accountable.

Doing it on OBTO

OBTO is model-agnostic and MCP-native, so the Postgres tools you build work with Claude, GPT, or whatever client your team prefers. You are not rebuilding the connector every time you switch models. You describe the tool, ship it, and own it: the code, the secret, and the receipt all stay on infrastructure you control, whether that is our cloud or your own cluster. And because pricing is per application rather than per seat, adding teammates to an agent that reads your database does not inflate the bill.

If you want to see it end to end, the getting-started guide takes about ten minutes, and the free Builder tier includes Glass Box tracing — enough to wire your first read tool to a real table and watch the receipts come in.

Connecting a database to an AI agent is not hard. Connecting it so you can sleep at night is the part worth doing well, and it comes down to a single decision: give the agent tools, not a terminal.

Frequently asked questions

Can an AI agent query a Postgres database?

Yes. The reliable way is to expose specific, typed tools (like get_customer or list_overdue_invoices) that run parameterized queries server-side, rather than letting the model write and execute raw SQL. The agent calls a tool by name; your code runs the query.

Is it safe to give an AI agent access to my database?

It is safe when the agent reaches the database only through scoped tools backed by a read-only role, with parameterized queries, row limits, and server-side credentials. It is risky when you hand the model a connection string and let it generate arbitrary SQL.

Should I let an AI agent write its own SQL?

For exploration, it is fine. For production systems you depend on, no. Generated SQL is non-deterministic and can read or modify anything the connection allows. Wrapping queries in fixed, reviewed tools gives the same answers without the blast radius.

How do I stop an AI agent from deleting or changing data?

Connect with a Postgres role that only has SELECT on the tables your tools need. Even if a prompt tries to coax the agent into a destructive query, the database itself rejects it. Pair that with logging so every call is visible.

Where should the database connection string live?

Server-side, as an encrypted secret read by your tool at runtime — never in the prompt, the client, or the model context. On OBTO the platform resolves the secret when a tool runs, so the credential never travels with the conversation.

Give your agent tools, not a terminal

Build a scoped Postgres tool, keep the credentials server-side, and get a receipt for every query — on day one.

Get started

More from the OBTO blog