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

> 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.

Source: https://schemity.com/blog/database-mcp-server-sql-access-or-schema-only/

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](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres) 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 `COMMIT` ended the read-only transaction, the `DROP` ran in autocommit, and the next query failed with `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"](https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-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:

```sql
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 customers` failed with `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.
- `SELECT email, card_last4 FROM customers` still returned the rows, because reading them is exactly what this role is for.
- `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](https://schemity.com/blog/postgres-role-read-schema-not-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](https://schemity.com/database-mcp-server/) with 18 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](https://schemity.com/doc/ai-assisted-design/) takes one command, and other MCP hosts take a config entry.

![Schemity's MCP server drawer: Running on http://127.0.0.1:7332/mcp, the server enabled on port 7332, the bearer token hidden behind Reveal, Copy and Regenerate, Test connection reporting Server answered with 16 tools, and under Connect a host the Claude Code command claude mcp add --scope user schemity -- "/Applications/Schemity.app/Contents/MacOS/schemity" --mcp](https://schemity.com/images/blog/database-mcp-server-sql-access-or-schema-only/mcp-drawer.webp)

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.

![A Claude Code session asking which tables reference orders and what check constraints customers has, answered through three calls to the Schemity MCP server against a shop diagram connected to a live PostgreSQL database: a table of the 11 tables that reference orders.id with their column and nullability, customers listed with its columns and one index and no check constraints, a note that every relation is inferred from column names because the database declares no foreign keys, and two gaps, unindexed coupon_redemptions.order_id and tickets.order_id and a customers.email that is not unique](https://schemity.com/images/blog/database-mcp-server-sql-access-or-schema-only/agent-get-schema.webp)

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.

![A Claude Code session asking how many rows customers has with count_rows, on a shop diagram connected to PostgreSQL as a role with no table grants: Schemity ran count_rows on customers and the database refused it with permission denied for table customers](https://schemity.com/images/blog/database-mcp-server-sql-access-or-schema-only/agent-count-rows-refused.webp)

## 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 `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.
- Never: your application's login, an owner role, or a `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](https://schemity.com/blog/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](https://schemity.com/blog/reverse-engineer-legacy-database-group-by-domain/).

## Frequently asked questions

### Can an AI agent write through a read-only MCP server?

Yes, if the read-only part is only a transaction. A server that wraps each query in `BEGIN TRANSACTION READ ONLY` and passes the agent's SQL through as-is can be escaped with `COMMIT` followed by a write in the same query string. A login role that owns nothing and holds no write privileges holds, because the database refuses writes to your tables whatever the transaction says.

### What database permissions should an AI agent have?

For designing or reviewing a schema, none on the tables: `CONNECT` on the database and `USAGE` on the schema let it read the full structure from `pg_catalog`. If it must read rows, give it a role with `SELECT` on the tables it needs and ownership of nothing, and enforce a time limit outside the SQL it sends, since the agent can change `statement_timeout` itself. Never give it the application's login or an owner role.

### Does an MCP server send my database data to the AI model?

Whatever a tool returns goes into the agent's context, and with a cloud-hosted model that means the model provider receives it. A SQL tool that returns customer rows sends those rows. A schema-only tool sends table and column names, types, keys and constraints, which are usually far less sensitive than the data.
