PostgreSQL over MCP, or an assistant querying your database
Connecting a database to an assistant sounds like obvious convenience. Instead of writing a query you ask about last week's order count and get an answer along with the query that produced it.
The convenience is real, and the risk larger than with any other tool of this class. A production database is a place where a mistake shows up not as a worse answer but as deleted data.
This text opens with a story illustrating that, since it concerns a tool still used by tens of thousands of people weekly despite having been retired.
A read only mode that was not
The official reference server for PostgreSQL, published alongside the protocol, offered a mode restricting an assistant to reading. It looked sensible and carried a flaw that removed that restriction entirely.
The mechanism wrapped a query in a transaction marked read only. The text arriving from the model went whole into the driver's simple query call, which executes several statements separated by semicolons in one round trip, so nothing prevented closing that transaction and running further statements in the same string.
A query beginning by committing the current transaction, followed by any operation, sufficed. The protection vanished, and the next statement executed with the connection's full privileges, dropping a schema together with all its contents included.
The project was archived in 2025 and the packages marked as deprecated. It is nevertheless still downloaded tens of thousands of times a week, since tutorials from two years ago point at it, and the deprecation notice is seen only by whoever looks for it.
The practical conclusion is simple and worth repeating: if a package by that name appears in your configuration, replace it. Replacements fixing that flaw exist and remain maintained; the tool side fix is sending the query as a prepared statement, since that path refuses to carry several statements at once.
What this history teaches
Drawing conclusions broader than one package pays off, since they concern a whole class of tools.
The first: a restriction written into a tool's code is not a security control. A read only mode implemented by wrapping a query depends on whether the query can be extended, and with text arriving from a model it always can.
The second: the real restriction belongs to the database rather than the tool. An account with read only privileges will not perform a write regardless of what arrives in the query and how cleverly it is phrased.
That second conclusion has a limit worth knowing, since a layer to move the restriction into does not exist everywhere. The filesystem server guards its permitted directories with a check in its own code, and that check holds, because it compares a resolved path, a finite thing, rather than query text, which can always be extended. The difference is therefore not whether the check lives in the tool's code, but whether it checks a closed value or a language.
The third: presence in a tool catalogue means neither currency nor safety. The registry, covered in the piece on the server catalogue, stores descriptions and verifies names; it performs no code review.
The fourth, the most uncomfortable: a tool can be official and simultaneously unfit for use. A publisher's signature speaks to origin rather than to quality or to whether anybody still maintains it.
How to do this safely
Three layers, each working independently of the others. Have all three, since each fails differently.
The first is a separate database account with read only privileges and only on the tables the assistant should see.
CREATE ROLE assistant_read LOGIN PASSWORD 'password';
GRANT CONNECT ON DATABASE shop TO assistant_read;
GRANT USAGE ON SCHEMA public TO assistant_read;
GRANT SELECT ON orders, products TO assistant_read;That is the only protection working regardless of what arrives in a query. If the assistant should see only orders and products, the employee data table lies beyond its reach at the database level rather than at the level of good intentions.
The second layer is an execution time limit. A query without one can block a database for minutes, and a model does not know that joining three large tables without a condition will run for an hour.
ALTER ROLE assistant_read SET statement_timeout = '10s';The third is a separate instance or a read replica. An assistant querying a replica never touches the database serving users, so even a badly built query will not affect the application.
Three obvious and skipped things join those. Masking columns holding personal data, since an assistant reading a users table passes their details to the model. Recording executed queries, so you know what was asked. And disabling production database access in the development environment, since that is where hastily copied configuration most often lands.
Remember too that PostgreSQL offers access restriction at row level, not only at table level. On a multi tenant database where one table holds many customers' data, that is the only way for an assistant to see just the right customer's rows, and it works regardless of how the query is phrased.
What an assistant can genuinely do with a database
Describing the uses concretely pays off, since they determine whether the risk is justified.
The most useful is reading the structure. Asking which tables hold order data and how they relate saves an hour of reading a schema when entering an unfamiliar project.
The second is writing queries. A model that knows the structure produces sensible analytical queries faster than recalling window function syntax takes. Do read what it produced, though, since a query returning a number does not tell you whether it counts what you asked about.
The third is diagnostics. Asking for an execution plan, for missing indexes, or for the fastest growing tables gives answers that otherwise require going through several tools.
What not to do: modifying data. Letting an assistant fix one record is tempting, and the difference between fixing one and fixing all of them is a missing condition the model will not notice, since the query executes correctly.
Schema migrations belong to a person too. Not because a model cannot write them, but because reviewing a structural change before it runs is cheaper than restoring from a backup.
A sensible arrangement runs like this: the model writes the query and the migration, a person reads and runs them. The assistant holds read access, so it sees the structure and the data needed to write sensible code, while execution belongs to whoever bears the consequences. That separation costs a dozen or so seconds per operation and removes an entire category of problems.
Result size and the model's context
A separate problem, less dangerous than the previous one and more common in daily work: a query returning too much data.
The model receives the result as text entering its context, so a query returning five thousand rows will either fill the whole available context or be cut off midway. In the first case you pay for tokens and lose room for the rest of the conversation; in the second the model answers from a fragment without knowing something is missing.
Three things limit that. The first is a default row limit on the server side, independent of what the model wrote in its query. The second is an instruction stating plainly to use aggregation rather than fetching rows for quantitative questions. The third is returning the total count of matching rows alongside a sample, so the model knows it sees a slice.
Watch the width too. A query fetching every column from a table holding forty of them spends context on data nobody needed, and with text heavy tables it does so very quickly.
The same applies to reading the structure. A schema with two hundred tables passed in full occupies more room than the rest of the conversation, so a sensible server exposes a table list separately and a single table's description on request, rather than sending everything at once.
Recording runs and reviewing them
The last layer, easy to forget, which during an incident determines whether anybody knows what happened.
Record the executed queries along with timestamps, a session identifier, and who asked. The database can do that itself, through a setting logging statements for a chosen account, so it need not be built into the tool.
That record serves three purposes. The first is diagnosis after the fact, when somebody notices something running slower. The second is reviewing what people actually ask, which after a month usually shows that ten questions repeat constantly and deserve converting into named operations. The third is spotting a situation where the model started asking about things outside its scope, because it read something that prompted it.
Set an alert on queries exceeding a set duration too. That is the simplest signal that somebody is querying the production database in a way nobody anticipated, and the only one that arrives on its own.
Servers to choose from
| Option | Strength | Weakness | Pick it when |
|---|---|---|---|
| A replacement for the official one | The flaw fixed, a compatible interface | Community maintained | Replacing an existing configuration |
| A server with performance analysis | Query diagnostics, restricted modes | More concepts to learn | Working on database performance |
| A database provider's server | Integrated with the rest of the service, authentication | Works only with that provider | A database in that provider's cloud |
| Your own server | Exactly the queries you permit | You write and maintain it | A narrow scope and high requirements |
The last row deserves more attention than it usually gets. Rather than letting an assistant run arbitrary queries, you can expose a few named operations: checking an order's status, summarising sales for a period, listing products low in stock. The model calls them with parameters, and you control every query.
That approach is safer and usually sufficient, since in practice an assistant asks about a dozen or so things rather than anything at all. The tools for building such a server are covered in the piece on the protocol's toolkit.
Managed database providers, including those covered in the pieces on Neon and Supabase, offer their own servers integrated with their platform's authentication, which simplifies setup when working in their ecosystem.
Common mistakes
The first is using the retired reference server. Its read only mode can be bypassed, and the project receives no fixes.
The second is relying on a read only mode provided by the tool rather than by the database. Account privileges are the only restriction a query cannot circumvent.
The third is connecting an assistant on the account the application uses. That account holds write privileges because the application needs them, so the assistant receives them along with it.
The fourth is no execution time limit. One query without a condition can load the database for minutes, and a model will not estimate the cost before running it.
The fifth is access to tables holding personal data without need. The contents of rows read enter the model's context, so narrow the scope to the tables genuinely required.
The sixth is permitting data modification. The difference between fixing one record and all of them is a missing condition the model will not notice, since the query executes correctly.
The seventh is not limiting the number of rows returned. A query handing back five thousand records fills the model's context with data nobody read, and costs money on every such call.
FAQ
Is the official PostgreSQL MCP server safe?
No. It was archived in 2025 and marked deprecated, and its read only mode can be bypassed: a query can close the protective transaction and perform any operation with the connection's full privileges. Replacements fixing that flaw exist.
How do I ensure an assistant only reads?
Through a separate database account with read only privileges and only on selected tables. That is the only protection working regardless of a query's content, since the database refuses a write whatever arrives from the model.
Can an assistant delete data?
It can, if it connects on an account holding such privileges. On a read restricted account it cannot, and that is exactly why connecting an assistant on the application's account is a serious configuration mistake.
Is granting production database access worthwhile?
For reading, through a restricted account and preferably against a replica, yes. Directly against the database serving users, better not, since one badly written query can load it for minutes and an execution time limit does not always act in time.
Is writing your own server better?
Under high requirements usually yes. A few named operations with parameters, instead of arbitrary queries, gives full control over what reaches the database and in practice covers most of what an assistant asks anyway.
The flaw is analysed in a research team's write up, and the archived code sits in the repository of retired servers.