Connect Postgres to an AI Agent Without Handing It Raw SQL
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:
- A generated query can
DROP,UPDATE, orDELETEas easily as it canSELECT. One creative prompt — or one injection buried in a row of your own data — and the agent has write access to everything the connection can reach. - A
SELECT *on a wide table can pull thousands of rows, blow past the context window, and quietly multiply the run's token bill. - The same question can produce different SQL on different days. Non-determinism is fine for brainstorming and dangerous for anything you report on.
- Your schema, and sometimes your credentials, become part of a prompt that gets shipped to a model provider.
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:
get_customer(customer_id)list_overdue_invoices(min_days_overdue)search_orders(status, date_range, limit)
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
- Connect with a read-only role. Create a dedicated Postgres user that can
SELECTfrom 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. - Bind parameters, bound results. Every query is parameterized; every query carries a
LIMITand a statement timeout. A runaway request should fail fast and cheap, not lock a table or flood the context window. - 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.
- 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.