"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.
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) |
yes | no | no | no | no | no | yes |
| Read/write with a read-only mode (e.g. crystaldba postgres-mcp) |
no* | partial | partial | — | no | no | yes |
| Read-only DB role + text-to-SQL (the common closed pattern) |
yes | no | no | — | no | varies | usually not |
| Semantic layer (Cube, dbt Semantic Layer) |
yes | n/a** | n/a** | yes | no | yes | core OSS |
| MCP security gateway (TrueFoundry, MintMCP, …) |
policy | no | no | — | no | no | varies |
| Statement guard (this project) |
yes | yes | yes | yes | yes | yes | yes |
* 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).
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.SELECT can still read a file (pg_read_file), open a socket (dblink), or run a shell (xp_cmdshell). Does anything stop those?orders, or LG_001_01_ORFICHE? Naming shapes both safety and accuracy.A comparison that only flatters one row is marketing. Here's the honest version:
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.
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.
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.
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.
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.
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.