Skip to content

Build1 publisher3 min readPublished Updated

The Postgres MCP server in tens of thousands of installs stopped shipping in December 2024

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

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying The Postgres MCP server in tens of thousands of installs stopped shipping in December 2024
Generated illustration

What happened

  • @modelcontextprotocol/server-postgres last shipped a version on 4 December 2024.
  • The package is marked deprecated on npm; the registry states it is no longer supported.
  • In the thirty days to 9 August 2026 the package was downloaded 475,790 times.
  • 475,790 downloads over thirty days is approximately 15,900 downloads per day.
  • The interval between the last published release (4 December 2024) and the end of the download window (9 August 2026) is about twenty months.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

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

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories