Database MCP Server: Should an AI Agent Run SQL or Only Read the Schema?
Disclosure: I build Schemity, a desktop ERD tool - this post is from our blog and uses it for the examples. TL;DR: For schema work an AI agent needs the structure, not the rows, so give it a database MCP server that ex
Disclosure: I build Schemity, a desktop ERD tool - this post is from our blog and uses it for the examples.
TL;DR: For schema work an AI agent needs the structure, not the rows, so give it a database MCP server that exposes the schema and no SQL tool. When a task does need rows, guard it with a database role that owns nothing. A
READ ONLYtransaction alone is not a guard: the reference Postgres MCP server relied on one, and a singleCOMMIT; DROP TABLEsent as one query ended it.
An AI agent doing schema work needs the structure of your database, not its rows, so the safest database MCP server for it is one that exposes the schema and has no SQL tool at all. When a task really does need data, the guard has to be the database's own permissions: a login role that owns nothing. A read-only transaction around the agent's SQL looks like the same guard, and it is not.
The difference is easy to show. The reference Postgres MCP server, which the Model Context Protocol project published as an example, runs every query inside BEGIN TRANSACTION READ ONLY. Every test below was run against PostgreSQL 18.3 in a throwaway container, with that tool's handler reproduced on the same pg driver (version 8.23.0) and a customers table holding emails and card digits.
What can a database MCP server let an agent do?
The options differ in what the agent sees and in what stops a write:
| What the MCP server offers | What the agent can read | What stops a write |
|---|---|---|
A SQL tool inside a READ ONLY transaction, as your usual login |
Every row | The transaction flag, which the agent's own SQL can end |
A SQL tool, logged in as a role with SELECT only |
Every row it has SELECT on |
The role's privileges |
| A SQL tool, logged in as a role with no table grants | Structure only, from pg_catalog
|
The role's privileges |
| No SQL tool, only schema tools | Structure, plus whatever counts the tools compute | There is no statement to write with |
The first row is the one most "read-only" database MCP servers start from, and it is the only one where the agent can undo the guard.
Is a read-only Postgres MCP server safe?
Not when the transaction is the only guard. The archived reference server's source handles a query in three calls: client.query("BEGIN TRANSACTION READ ONLY"), then client.query(sql) with the agent's text, then ROLLBACK. With no parameters, node-postgres sends that text over the simple query protocol, which accepts several statements in one string. Sent through the same three calls:
-
SELECT email, card_last4 FROM customersreturned every customer's email and card digits. -
INSERT INTO customers ...failed withcannot execute INSERT in a read-only transaction, as intended. -
COMMIT; DROP TABLE customersreturnedCOMMITandDROP. TheCOMMITended the read-only transaction, theDROPran in autocommit, and the next query failed withrelation "customers" does not exist.
This is not a new finding: Datadog Security Labs described the same escape in "MCP vulnerability case study: SQL injection in the Postgres MCP server" on August 21, 2025. The repository was archived on May 29, 2025, and its README now says "No security updates or bug fixes will be provided" for these servers. Other Postgres MCP servers are separate code, so read how yours runs the agent's SQL: a prepared statement accepts only one statement, while a raw query string passed through as it arrives accepts several.
Even without the escape, the first result is the larger problem for most teams. Whatever a tool returns becomes part of the agent's context, so with a cloud-hosted model the emails and card digits are now with the model provider too.
Why a database role is the guard that holds
The database checks privileges on every statement, whatever transaction it runs in, so writes to your tables are refused however the agent's SQL is shaped. The same three calls, logged in as a role with pg_read_all_data and a default timeout:
CREATE ROLE agent_ro LOGIN PASSWORD 'change-me';
GRANT pg_read_all_data TO agent_ro;
ALTER ROLE agent_ro SET statement_timeout = '5s';
-
COMMIT; DROP TABLE customersfailed withmust be owner of table customers, and the table stayed. -
SELECT pg_sleep(10)failed after 5 seconds withcanceling statement due to statement timeout, but only because the SQL did not change it.SET LOCAL statement_timeout = 0; SELECT pg_sleep(3)ran to the end: a role's timeout is a default, and any role can override it. A hard limit has to sit where the agent's SQL cannot reach, in the MCP server or a connection pooler. -
SELECT email, card_last4 FROM customersstill returned the rows, because reading them is exactly what this role is for. -
COMMIT; CREATE TEMP TABLE t (x int)succeeded, becausePUBLICmay create temporary tables by default. That touches none of your data; revokeTEMPon the database if it matters.
So a SELECT-only role fixes the write escape but not the data exposure. For an agent that works on the schema rather than the data, go one step further: a role with CONNECT on the database, USAGE on the schema and no table grants reads the entire structure from pg_catalog and is refused every row. Only from pg_catalog, though: information_schema hides every table the role has no privilege on, so a SQL tool that lists tables through it shows that role an empty database. That setup, and what the catalogue still reveals, is in a PostgreSQL role that reads the schema but not the data.
What does an agent need to understand your schema?
Most of what an agent is asked to do with a database is structural: explain how tables relate, find where a module's tables are, propose a new table or a column change, or check what a migration will break. None of that needs a row. It needs table and column names, types, nullability, keys, check constraints, indexes and comments, and for judging a change, row estimates and table sizes, which the catalogue holds too.
That is what to understand your schema means for an agent, and it is also the least sensitive part of the database to send to a model. A diagram of customers with an email column tells the model the column exists. A SELECT tells it every address.
How Schemity's database MCP server works
Schemity is database design software that reads your live database, shows the impact of every schema change before it runs, and keeps the diagram as a file in Git. It is also a local database MCP server with 16 tools, and none of them runs SQL the agent writes: analyze_migration_file accepts a migration file to analyse, and it is parsed, never executed. The agent reads the schema Schemity already holds and never receives the database credentials. Connecting Claude Code takes one command, and other MCP hosts take a config entry.
What the tools give an agent:
-
get_schemareturns the tables and views with their columns, keys, constraints, indexes and relations.get_dependencieslists the relations into and out of one table,get_context_viewsthe domains the tables are grouped into, andget_data_dictionarythe same schema as a document with its descriptions. -
propose_changesedits the diagram. The edits land as unsaved changes you review, and nothing is written to the database. -
analyze_impactreports what the pending migration would cost, and returns the SQL only when asked, with a parameter description telling the agent that Schemity never runs it. -
count_rowsreturns one number, such as how many rows of a column areNULLbefore it becomesNOT NULL. It takes a probe object rather than SQL and refuses the one kind of probe built from free text, so its query is assembled from quoted table and column names with no text from the agent in it. That, not the transaction, is what keeps agent SQL out; the read-only transaction and the 60-second timeout it also runs under on PostgreSQL are set by Schemity, where the agent cannot change them. It is a full table scan, so ask your agent to run it only when you want the number.
Every transport listens on 127.0.0.1 only and needs a token, and the HTTP one also checks the request's origin. Applying a migration stays a human action in the app. Connect the diagram with the no-grants role from the post above and count_rows is refused too, which leaves the agent with nothing but structure.
Which database MCP setup to choose
- Designing, reviewing or documenting a schema: a schema-only MCP server, or a SQL one logged in as a role with no table grants.
- Answering questions about the data: a SQL server logged in as a role with
SELECTon the tables it needs and owning nothing, a time limit the agent cannot unset, and only where sending those rows to your model provider is acceptable. - Never: your application's login, an owner role, or a
READ ONLYtransaction as the only protection.
For what the agent should be allowed to change once it has read the schema, see should AI agents write database migrations. Using the agent to sort a large legacy schema into domains is covered in grouping a legacy database by domain.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.

