# Postgres MCP Server Permissions & Controls, Explained

How the archived Anthropic Postgres MCP server and Postgres MCP Pro actually control SQL — a read-only wrapper with an unpatched injection still on npm, a restricted mode that's off by default, and the database role that outlasts both.

# The Postgres MCP server control model, explained

This page covers a pair, because the story is the pair. Anthropic's reference server, `@modelcontextprotocol/server-postgres`, was [deprecated on July 10, 2025 and archived](https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/) on GitHub, npm, and Docker Hub — and was still measured at [312k npm downloads a month](https://dev.to/spencerpauly/why-is-anthropics-archived-postgres-mcp-server-still-getting-312k-installs-a-month-3oeh) more than a year later, a number that *tripled* in the five months after November 2025. Its de-facto successor is [Postgres MCP Pro](https://github.com/crystaldba/postgres-mcp) (crystaldba/postgres-mcp, PyPI `postgres-mcp`), which fixed the reference server's central defect and added real controls. One server is a zombie with a known-unpatched injection in its published artifact; the other is maintained and defaults to full read/write. Between them they define what "native controls" means for the most-connected database in the MCP ecosystem.

*This page covers the servers' own controls. For the cross-server picture, see [the MCP server controls comparison](/mcp-controls); for what a control layer adds on top, [the coverage matrix](/coverage).*

## What's at stake on this surface

Both servers reduce to the same primitive: a tool that accepts an arbitrary SQL string (`query` in the archived server, `execute_sql` in Pro) and runs it against whatever the connection string reaches. Arbitrary SQL means `UPDATE`, `DELETE`, `DROP`, DDL, permission grants — the full authority of the connected role, exercised by whatever the model decides SQL should say next. There is no toolset to trim, no resource scoping, no per-table anything: the entire control question on this surface is *what can the SQL do*, and the two servers answer it very differently.

## The archived server: a read-only wrapper that lied

The reference server shipped one control: the `query` tool wrapped each submitted string in a read-only transaction — `BEGIN TRANSACTION READ ONLY`, run the input, `ROLLBACK` — relying on Postgres to reject writes inside it. [Datadog Security Labs showed the wrapper was bypassable](https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/): stacked statements beginning with `COMMIT;` terminate the read-only transaction from inside the query text, and everything after runs read-write. The read-only guarantee was a preamble the payload could end.

The timeline is the part to be precise about. Anthropic fixed it in the git repository on May 29, 2025 — prepared statements, which disallow stacked statements, plus destroying the connection after each call. **That fix was never published.** v0.6.2, the last release on npm and Docker Hub, predates it and remains unpatched; it's the version every fresh install gets. (The fix did ship somewhere: Zed's fork, `@zeddotdev/postgres-context-server`, released a Datadog-supplied patch in April 2025.) So the deployed state of the ecosystem's most-downloaded Postgres server is: one control, known-bypassed, in the artifact still being served — at roughly [21k npm downloads and 1k Docker pulls a week](https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/) even by the disclosure's count. If it's in your config with read-only expectations, the correct mental model is read-write.

## Registry gravity: why deprecation didn't uninstall anything

None of this is a maintenance failure. Anthropic deprecated responsibly — archived the repo with an explicit "no security guarantees" warning, marked every npm version deprecated. The [dev.to analysis](https://dev.to/spencerpauly/why-is-anthropics-archived-postgres-mcp-server-still-getting-312k-installs-a-month-3oeh) of why installs kept climbing anyway is a study in where warnings don't reach: `npx` runs a deprecated package without complaint; the archive notice lives at a different GitHub URL than the one old tutorials link; the npm deprecation message says only "Package no longer supported," naming no successor; and the package remains the top search result and the thing LLMs recommend, because the training data and the blog posts were written before July 2025. (That analysis has a disclosed interest — its author sells a competing product — but the download counts are npm's own.) Configs are copy-pasted, not resolved: deprecation changes what maintainers ship, and nothing about what agents are already wired to. A control model you're evaluating from current docs can be running from a two-year-old artifact.

## Postgres MCP Pro: restricted mode, defaulted off

[Postgres MCP Pro](https://github.com/crystaldba/postgres-mcp) is the maintained successor, and its `--access-mode=restricted` is a direct answer to the archived server's failure. It does three things:

1. **Read-only transactions** — the same wrapper, kept.
2. **SQL parsing before execution**, using the pglast library, rejecting any statement containing `COMMIT` or `ROLLBACK` — closing the exact smuggling path that broke the reference server. This is the meaningful upgrade: the transaction wrapper trusts Postgres to interpret the string; the parser inspects the string itself before Postgres ever sees it.
3. **Execution-time caps** — restricted mode limits query runtime ("presently only execution time" among resource constraints, per the README).

Two qualifiers, both from the project's own documentation. First, **the default is `unrestricted`** — full read/write; the source sets `default=AccessMode.UNRESTRICTED.value`, and the README's config examples pass `--access-mode=unrestricted` explicitly. A fresh install that never chose a mode has chosen one. Second, the README states plainly that **"if you have unsafe stored procedure languages enabled on your database, then our read-only protections could be circumvented"** — client-side parsing can't see what a function body will do. Honest scoping, and a pointer at where the real boundary has to live.

## The durable control: the role, not the wrapper

Both servers' read-only stories are attempts to enforce, from the client side of the wire, something Postgres enforces natively: `GRANT SELECT` to a role with nothing else, connect as that role, and read-only holds against stacked statements, stored procedures, parser gaps, and every future bug in either server — because the database refuses the write regardless of what the tool layer was talked into. That's the same layer-below logic that makes [Supabase's read-only mode](/mcp-controls/supabase) the strongest single control in this series; Supabase ships it as a flag because it controls the vendor side, while for a bare Postgres connection you build the role yourself. Postgres MCP Pro's community guidance points the same direction: restricted mode plus a SELECT-only role, the parser as a fast-fail in front of an enforcement the agent can't reach. On this surface, the connection string *is* the credential floor — spend your effort there before trusting anything above it.

## What doesn't exist

Same list as every server in this series, worth stating for the record: **no audit** — neither server writes any trail of what SQL an agent ran, under which mode, against which database ([74.9% of MCP servers document no audit mechanism at all](/blog/mcp-audit-gap); the database servers are the sharpest case, because "what ran" is exactly the question you'll be asked later). **No approvals** — no call can be held for a human; whatever confirmation your client shows is a client courtesy, gone in [yolo mode](/blog/what-survives-yolo-mode). **No per-statement policy** — access mode is one decision at launch; "SELECT freely, UPDATE with approval, never DROP" is not expressible. **No scoping below the connection string** — no per-table, per-schema, or per-database narrowing in either server beyond what the role already grants.

## Where the native model ends

The pair, graded together: the archived server's one control is broken in the shipped artifact and installs keep climbing anyway; the successor's real control is client-side parsing, defaulted off, with a documented bypass class; and the only enforcement that survives every failure above it belongs to Postgres, not to either server. The general lesson is the registry-gravity one — deprecation doesn't uninstall anything, and the control model in your config is whatever was true when the blog post you copied was written.

The practical posture: if `@modelcontextprotocol/server-postgres` is in any config you own, replace it — treat its read-only mode as absent. On Postgres MCP Pro, set `--access-mode=restricted` explicitly and don't rely on the default. Either way, connect as a role with only the grants the task needs — `GRANT SELECT` and stop — because the role is the one control the agent can't be talked out of. And for the per-statement policy, the approval that survives autonomy, and the record of what SQL actually ran, the native model has nothing to offer at any setting — that takes [a layer standing between the agent and the connection](/what-is-an-agentic-control-plane).
