Build1 distinct publisher3 min readUpdated
Its read-only guarantee is a check that the query text starts with SELECT. Data-modifying CTEs, leading comments and stacked statements all walk straight through it.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The npm package most agent stacks reach for when they want an LLM to query Postgres, @modelcontextprotocol/server-postgres, last shipped a release on 4 December 2024 and is marked deprecated by the registry itself [1][2]. It was still downloaded 475,790 times in the thirty days to 9 August 2026, roughly 15,900 pulls a day of an unmaintained component sitting between an agent and a production database about twenty months after its last commit landed [3][4][5].
Abandonment on its own is a patching problem. The safety model is the operational one, and it is worth reading before you decide how urgent this is.
According to a teardown published on dev.to by the author of a Rust reimplementation, the archived server's read-only enforcement is a single string comparison: reject anything whose trimmed, upper-cased text does not start with SELECT [6]. String matching cannot see structure, so three ordinary constructions pass it. A data-modifying CTE begins with WITH, and the parser behind it sees a DELETE [7]. A leading block comment survives trim(), so a commented DROP TABLE reads as a non-SELECT to nobody [8]. And a stacked batch such as SELECT 1; DROP TABLE users; presents SELECT to the check while the database executes both statements [9].
Credit where the original earned it: every query is wrapped in BEGIN TRANSACTION READ ONLY and always rolled back, which is a genuine second layer [10]. It is not a complete write barrier. The same teardown reports that PostgreSQL runs pg_import_system_collations() inside a read-only transaction without raising SQLSTATE 25006, inserting 874 rows into pg_collation in the author's tests [11]. gin_clean_pending_list() rewrites index structures [12], and pg_backup_start() puts the server into backup mode and survives DISCARD ALL [13]. A rollback undoes tuples, not side effects that live outside transaction semantics.
The replacement is useful less as a product than as a specification of the layers the original lacks. SQL is parsed with sqlparser and the AST node type decides, so a WITH clause containing a DELETE is rejected because the node is Delete regardless of the leading text; multi-statement batches are refused outright [14]. The author says a fuzz harness runs millions of mutations against the validator and every bypass found is retained in a MUST_REJECT corpus that runs on every commit [15]. Below that, SET default_transaction_read_only = on is applied at connection checkout, which is the layer that does not depend on the parser being correct [16]. The server refuses to start as a network listener if its role can write, is a superuser, or holds BYPASSRLS [17]. statement_timeout is set to 30s and idle_in_transaction_session_timeout to 10s [18], and an EXPLAIN (FORMAT JSON) cost ceiling refuses expensive plans before execution [19]. Result rows are wrapped in a delimited, escaped block marked untrusted, with invisible and bidirectional characters stripped, on the reasoning that a cell value flows into the agent's context and is therefore an injection vector [20]. Errors never echo schema details [21].
That is all one source, written by someone selling the alternative. The three bypass classes cost five minutes to verify against a scratch database; the fuzz corpus and the collation row count are not independently confirmed here.
Two things to check this week. First, run WITH x AS (DELETE ...) SELECT * FROM x against whatever path your agent actually uses, and see what comes back. Second, look at the role your MCP server connects as: if it can write, the string check is the only thing standing between a prompt and your tables [6].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
A data-modifying CTE such as WITH deleted AS (DELETE FROM users WHERE id = 1 RETURNING *) SELECT * FROM deleted; passes the prefix check because the text starts with WITH, while the parser sees a DELETE.
trim() removes whitespace but not comments, so /* harmless comment */ DROP TABLE users; is not caught by the prefix check.
With multiple statements, the check sees SELECT 1 while the database executes both, as in SELECT 1; DROP TABLE users;
PostgreSQL executes pg_import_system_collations() inside SET TRANSACTION READ ONLY without raising SQLSTATE 25006; in the author's tests it inserted 874 rows into pg_collation.
gin_clean_pending_list() rewrites index structures.
pg_backup_start() puts the server into backup mode and survives DISCARD ALL.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Concrete, checkable primitives from one first-party source
The load-bearing diagnostic claims are specific and independently checkable outside the cluster: an npm deprecation flag, a dated last release, a 30-day download total, a reproduced prefix check, and three named bypass shapes that follow from PostgreSQL semantics. The read-only side-effect claims carry a concrete test result (874 rows into pg_collation). Against that, everything is one self-published item by the author of the competing implementation, with no repository link, advisory, or third-party verification in the supplied material, and the rebuild's own guarantees rest on assertion alone.
Heavy use of the abandoned package; none evidenced for the fix
Adoption evidence is strong on one side of the story and absent on the other. The deprecated package shows sustained real-world pull - 475,790 downloads in thirty days, roughly twenty months after its final release - which is a registry-level usage signal even though downloads overstate distinct deployments. The Rust rebuild has no disclosed release, registry presence, download figure, user, or deployment anywhere in the cluster, so the remediation half of the story is at zero measured adoption.
Diagnosis solid, remediation claims run ahead of shown evidence
Mildly overstated overall. The critique is understated if anything - the bypasses are real and the read-only side-effect examples are better than the usual hand-waving. The overstatement sits in the framing and the fix: the headline's 'tens of thousands of installs' is extrapolated from npm download counts that include CI and mirrors, and the rebuild is presented as safe-by-default on the strength of the author's own description, with a million-mutation fuzz harness and MUST_REJECT corpus asserted but not evidenced, no external review, and no maintainership commitment that would distinguish it from the abandonment it criticises.
Author markets own replacement on own platform
The single source is a self-published dev.to post whose author built and is promoting the Rust rebuild that the article positions as the answer to the flaws it documents. Every unfavourable finding about the incumbent increases the appeal of the author's project, and the piece contains no disclosure, competing implementation comparison, or external review. This does not make the technical claims wrong - the npm and PostgreSQL facts are checkable - but the selection and framing of evidence is plainly interested.
One interested source, but checkable claims
Confidence is limited by structure rather than by internal inconsistency: one publisher, one item, written by an interested party, with no repository artifact or third-party confirmation supplied. It is held up by the fact that the decisive claims - deprecation, last release date, download volume, prefix-check bypasses, read-only transaction side effects - are the kind that any reader can verify against npm and a PostgreSQL instance, and by the article's own hedges (estimates are not runtimes; framing, not a cure).
build
The agent asks, the gateway decides: why read-only is not a security boundary1 distinct publisher
build
A default that is not a guard: tinycolor2's palette functions never return on analogous(-1)1 distinct publisher
build
A system prompt is not a GRANT: read-only agents have to be enforced below the model1 distinct publisher
build
98ms repo maps: what moving symbol indexing out of Node actually buys an agent1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026