cd /news/ai-agents/database-mcp-server-should-an-ai-age… · home › topics › ai-agents › article
[ARTICLE · art-142926] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↓ negative

Database MCP Server: Should an AI Agent Run SQL or Only Read the Schema?

A developer building the Schemity ERD tool demonstrated that the reference Postgres MCP server's read-only transaction guard can be bypassed by sending "COMMIT; DROP TABLE customers" as a single query string, since node-postgres forwards multi-statement text over the simple query protocol. The post recommends giving schema-focused AI agents an MCP server that exposes only schema tools with no SQL tool, and, when row access is required, relying on a database role that owns nothing rather than a READ ONLY transaction. The finding echoes a Datadog Security Labs case study from August 2025 on the same archived server.

by read7 min views2 publishedOct 1, 2026

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 ONLY transaction alone is not a guard: the reference Postgres MCP server relied on one, and a single COMMIT; DROP TABLE sent 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.

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.

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 customers returned every customer's email and card digits.INSERT INTO customers ... failed with cannot execute INSERT in a read-only transaction, as intended. COMMIT; DROP TABLE customers returned COMMIT and DROP. The relation "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.

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';

must be owner of table customers, and the table stayed. SELECT pg_sleep(10) failed after 5 seconds with canceling 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.COMMIT; CREATE TEMP TABLE t (x int) succeeded, because PUBLIC may create temporary tables by default. That touches none of your data; revoke TEMP on 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.

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.

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_schema returns the tables and views with their columns, keys, constraints, indexes and relations. get_dependencies lists the relations into and out of one table, get_context_views the domains the tables are grouped into, and get_data_dictionary the same schema as a document with its descriptions.propose_changes edits the diagram. The edits land as unsaved changes you review, and nothing is written to the database.analyze_impact reports 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_rows returns one number, such as how many rows of a column are NULL before it becomes NOT 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.

SELECT on 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.READ ONLY transaction 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.

── more in #ai-agents 4 stories · sorted by recency
── more on @schemity 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/database-mcp-server-…] indexed:0 read:7min 2026-10-01 · —