Read-only database access for AI agents, compared

"Give the agent read-only access" is one sentence and at least five different engineering decisions. Here's how the common approaches actually differ — and where each is the right choice.

A build note from erp-report-engine · by Eren Gülmez · this project is one row in the table below, and the note is honest about where the others are stronger.

If you want an AI agent to answer questions from a production database, you have to stop it from writing, reading files, or phoning home — while still letting it run real analytical SQL. There are several ways to draw that line, and they fail in different places. This compares them on the axes that decide whether "read-only" holds under a prompt injection.

Approach Read-only
by default
Checks the
statement
Blocks side-
effect functions
Fails
closed
Publicly
benchmarked
Semantic
entities
Open
source
Read-only transaction
(the archived reference
Postgres MCP server)
yesnononononoyes
Read/write with a
read-only mode
(e.g. crystaldba postgres-mcp)
no*partialpartialnonoyes
Read-only DB role +
text-to-SQL
(the common closed pattern)
yesnononovariesusually not
Semantic layer
(Cube, dbt Semantic Layer)
yesn/a**n/a**yesnoyescore OSS
MCP security gateway
(TrueFoundry, MintMCP, …)
policynonononovaries
Statement guard
(this project)
yesyesyesyesyesyesyes

* read/write by configuration; a read-only mode exists but is not the default.   ** a semantic layer never exposes raw SQL to the agent at all, so "checks the statement" doesn't apply — it's a different, also-valid answer (see below).

The last row's "yes"es are measured, not claimed. Run the same 28-attack corpus through the shortcuts the other rows lean on and the gap is a number: a starts-with-SELECT check refuses 6 of 28, a write-keyword blocklist 9 of 28 — and the blocklist also blocks a legitimate read, SELECT 'please delete this note', because the word is inside the string. Checking the statement itself refuses all 28 and blocks none. Every figure is computed from a live guard run on the benchmark page, and a test pins that the guard strictly beats each shortcut — so "checks the statement," "blocks side-effect functions," and "publicly benchmarked" are one measurement, not three adjectives.

What each column means

Where each approach is genuinely stronger

A comparison that only flatters one row is marketing. Here's the honest version:

Read-only transaction (the reference server)

Dead simple, and for a trusted, non-agent caller it's often enough. Its failure — COMMIT; DROP … walking out of the transaction — is a story about statement stacking, not about the idea being bad. It got archived because it faced untrusted agent input it wasn't built for.

crystaldba postgres-mcp

Stronger than this project at being a database tool. It ships real DBA capability — index tuning, EXPLAIN analysis, health checks — and a genuine read-only mode. If you're a developer who wants an agent to help operate a Postgres database, it's excellent and purpose-built. This project isn't a DBA tool; it's a read-only reporting/agent layer for the database behind an ERP, and it's cross-dialect (SQL Server first, because that's where Logo/Netsis/Mikro live). Different jobs.

Semantic layers (Cube, dbt)

Arguably the most correct answer of all — when you have the prerequisites. They never hand the agent SQL: the agent requests governed metrics by name, and the layer compiles them with access control baked in. dbt's own 2026 benchmark found that querying through a semantic layer beat text-to-SQL on accuracy (98–100% vs 84–90%), because "with text-to-SQL, failure looks like a plausible but incorrect answer; with the semantic layer, failure looks like an error message." The catch: they assume a data warehouse and a modeling layer (dbt) you've already built. This project targets the case where there is no warehouse — you point it straight at the operational ERP database — and its canonical entities are the same idea applied there.

MCP security gateways

Stronger at the things a single tool can't do: horizontal auth, RBAC, SSO, org-wide policy, SOC 2. If you're running many MCP servers across a company, a gateway is the right layer. It sits in front of servers like this one — it's complementary, not competing. What it doesn't do is understand SQL, so it won't tell a read from a pg_read_file.

Read-only role + text-to-SQL (the common closed pattern)

A least-privilege read-only database login is real defence — in fact this project insists on it as the outer layer, because a denylist is never provably complete. The difference is that a role is the only line in this pattern, and it's a coarse one: a read-only role on SQL Server can still be walked toward OPENROWSET file reads or linked-server pivots depending on configuration, and text-to-SQL against raw tables carries the silent-wrong-answer problem above. A statement guard plus a narrow role is belt and braces.

The point isn't that this project wins every row. It's that read-only for an agent — untrusted input, a tool that acts, an exfiltration channel one dblink away — needs the statement itself checked, failing closed, and provable. Most approaches were built for a trusted caller and are being asked to hold under conditions they weren't designed for. This one was built for those conditions, on the specific databases (Logo Tiger, Netsis, Mikro — all SQL Server) that global tools and OSS communities skip.

Don't take the table's word for the last row — paste an attack into the guard yourself, or read the reproducible benchmark. Both run the real code.