Postgres MCP Servers Compared: Read-Only Posture, Auth and Auditability

A Postgres MCP server is a process that exposes a PostgreSQL database to an AI client such as Cursor, Claude Desktop or Claude Code through the Model Context Protocol, as tools the model calls instead of a raw connection. In production three properties matter most: default write posture, how the agent authenticates, and whether you can tell afterwards who ran what. I compare six servers on those three. I maintain one of them (pREST), so weigh that column accordingly.

Table of Contents

What a Postgres MCP server does

An MCP client (the IDE or chat app) starts or connects to a server, calls tools/list to learn what it can do, and then calls tools/call with arguments the model fills in. For Postgres, the tools are usually some mix of “list tables”, “describe table”, “run SQL” and, in the platform-tied servers, “create branch” or “apply migration”.

Two transports show up. Stdio servers are local processes the client launches and talks to over stdin/stdout, which is why most configs are a command plus args. HTTP servers sit somewhere else and the client config holds a URL. The transport decides where your database credentials end up, which is the whole security story in this post.

The six servers

Anthropic’s reference server (archived)

The original @modelcontextprotocol/server-postgres is a stdio server with one tool, query, which the README describes as executing “read-only SQL queries against the connected database”, with “All queries are executed within a READ ONLY transaction”. It also publishes table schemas as resources. The connection string is passed as a command-line argument, so it lives in your client config.

It now sits in the servers-archived repo, whose README says these servers “are no longer maintained”, that “No security updates or bug fixes will be provided” and, in capitals, “NO SECURITY GUARANTEES ARE PROVIDED FOR THESE ARCHIVED SERVERS”. The main servers repo still links to it as “Read-only database access with schema inspection”. It is the simplest of the six and the one I would not run anywhere that matters, for a reason covered below.

Postgres MCP Pro (crystaldba/postgres-mcp)

Postgres MCP Pro is the most capable server here for database work itself. The README lists database health checks (index health, connection utilization, buffer cache, vacuum), index tuning that will “explore thousands of possible indexes to find the best solution”, EXPLAIN plan review and schema-aware SQL generation. None of the other five do index tuning or plan analysis.

It connects with a DATABASE_URI connection string and supports stdio and SSE transports. It has two access modes. Restricted mode “Limits operations to read-only transactions and imposes constraints on resource utilization (presently only execution time)” and is described as suitable for production; unrestricted mode “Allows full read/write access to modify data and schema”. The README shows both modes passed explicitly and does not name a default, so set --access-mode=restricted yourself rather than assuming. It is MIT licensed with about 3.2k stars. The last tagged release is v0.3.0, published in May 2025, though the repo has had commits since.

Supabase MCP

Supabase MCP is tied to Supabase projects and is primarily a hosted HTTP server at https://mcp.supabase.com/mcp, with a local variant at http://localhost:54321/mcp when you run the Supabase CLI. Auth is OAuth 2.1 by default; for clients without OAuth, the docs say to “create a scoped personal access token (PAT) and pass it as a header to the MCP server” as Authorization: Bearer.

Three URL parameters shape what the agent can do. read_only=true will “Execute all queries as a read-only Postgres user”. project_ref=<id> will “Scope to a specific project (disables account tools)”. features= limits the tool groups (database, debugging, development, functions, account, docs, branching, and storage, which is off by default). Read-only is opt-in. Apache 2.0, about 2.9k stars. Because it is hosted, release tags on the repo say less about maintenance than whether the endpoint is up.

Neon MCP

Neon’s server is “a bridge between natural language requests and the Neon API”, so it manages Neon projects and branches as well as running SQL. The hosted server at mcp.neon.tech authenticates with OAuth, and the README adds that it “also supports authentication using an API key in the Authorization header if your client supports it”. A local install with an API key exists too, though Neon’s docs steer you to the hosted server.

Tools include run_sql, run_sql_transaction, create_branch, delete_branch, prepare_database_migration, complete_database_migration and get_connection_string. Appending ?readonly=true “restricts the server to read operations”; with OAuth you can pick the read-only scope during authorization. Neon is blunt about where this belongs: the server is “intended for local development and IDE integrations only. We do not recommend using the Neon MCP Server in production environments.” MIT, about 650 stars, and no GitHub releases are published.

pgEdge Postgres MCP server

pgEdge introduced its server in December 2025 as a beta that “only supports readonly mode”, with write support “with appropriate safeguards, including a readonly by default switch” on the roadmap. It is written in Go, runs over stdio or HTTP, and the HTTP mode has “TLS support, user and token auth”. It can “Connect to multiple Postgres instances from the same MCP server” using named entries like devdb, stagingdb, proddb, and keeps a connection pool.

The repo README now says “All queries run in read-only transactions by default”, configurable per database, and mentions token expiry and SHA256 token hashing. It is under the PostgreSQL License, has about 220 stars, and has a v1.0.0 tag. It is the closest to pREST in posture (credentials server-side, read-only first, multi-database), with a general SQL tool where pREST has structured selects.

pREST /_mcp

pREST is a Go REST API for Postgres that, since v2.1.0, serves MCP at /_mcp on the same port as the REST API. The docs describe GET /_mcp for discovery and POST /_mcp for JSON-RPC 2.0 with initialize, tools/list and tools/call, protocol version 0.1. Tools are prest.list_databases, prest.list_schemas, prest.list_tables, prest.describe_table, prest.select_table and a generated prest.select.{database}.{schema}.{table} per table the caller may read, with columns, filters, order_by, limit and offset arguments. There is a cap of 100 rows per select.

There is no SQL tool. “No insert, update, delete, or DDL tools” is the documented posture, and the model cannot send free-form SQL through MCP either. That is the safety argument and also the biggest functional gap next to Postgres MCP Pro or even the archived reference server. Auth is pREST’s existing JWT or basic auth; access.tables and per-user [[access.users]] rules apply, and “Tool discovery only lists tables and columns the caller can read”. The v2.0.0 multi-database registry means tools are generated per alias (v2.0.0 and v2.1.0 overview). Clients that only speak stdio use the prest-mcp adapter, which forwards JSON-RPC to POST /_mcp and nothing more (adapter and plugins). MIT, around 4k stars, and the latest release is v2.4.0 from July 27, 2026. Everything from v2.0.0 to v2.4.0 landed in July 2026, which is a fast cadence; whether that is a good sign or a sign of churn depends on your appetite.

Comparison table

Star counts are rounded from each repo’s GitHub page at the time of writing. “Conn string in client?” asks whether the AI client’s config file holds database credentials.

Anthropic reference Postgres MCP Pro Supabase MCP Neon MCP pgEdge pREST /_mcp
Transport stdio stdio, SSE HTTP (hosted or local CLI) HTTP (hosted); local option stdio, HTTP HTTP; stdio via prest-mcp
Read-only by default? Yes, READ ONLY transaction (bypassed, see below) No default documented; restricted mode available No; read_only=true opt-in No; readonly=true opt-in Yes, read-only transactions by default Yes, no write tools exist
Auth None at MCP layer; Postgres role None at MCP layer; Postgres role OAuth 2.1 or scoped PAT OAuth or API key header User and token auth, TLS JWT or basic auth on /_mcp
Conn string in client? Yes, as CLI arg Yes, DATABASE_URI No No, but get_connection_string tool exists No, server config No, pREST config
Per-table permissions Postgres grants only Postgres grants only Project scoping, feature groups, read-only user Read-only scope, project/branch level Per-database read-only setting access.tables per table and field, per user
Multi-database One per server entry One DATABASE_URI Per project (project_ref) Neon projects and branches Multiple named databases Registry aliases, tools per alias
Write safety Transaction flag only Restricted mode parses SQL to reject COMMIT/ROLLBACK, adds time limit Read-only Postgres user when flagged; mutating tools otherwise Write tools disabled when flagged; migrations and branch delete otherwise Read-only transactions, configurable No write tools; REST on same server may still write
Maintenance Archived, unmaintained ~3.2k stars, last release v0.3.0 (May 2025), commits since ~2.9k stars, hosted endpoint ~650 stars, hosted, no GitHub releases ~220 stars, v1.0.0 ~4k stars, v2.4.0 (Jul 27, 2026)

How a READ ONLY transaction gets bypassed

The reference server’s “READ ONLY transaction” line is a good example of why the default write posture has to be read with the implementation open. Datadog Security Labs published a case study in August 2025 showing that the server passed the model’s SQL to client.query(sql), which accepts several statements separated by semicolons. A query of COMMIT; DROP SCHEMA public CASCADE; ends the read-only transaction and runs the drop before the server’s own ROLLBACK arrives. The same trick works for COMMIT; SET statement_timeout TO 1; to degrade every later query on that connection.

Their timeline says the fix landed in the repository on May 29, 2025 while the package was being deprecated, and that “@modelcontextprotocol/server-postgres v0.6.2 remains unpatched at NPM” with around 21,000 weekly downloads. The patched approach was a prepared statement, which refuses multiple statements, plus discarding the connection after each call. If you still have this server in a config file, that is the reason to remove it.

Postgres MCP Pro’s README names the same hole and explains that restricted mode parses SQL with pglast and will “reject any SQL that contains commit or rollback statements” before the read-only transaction ever sees it. That is the right instinct, and it is also a reminder that a server which accepts arbitrary SQL has to defend a grammar. pREST sidesteps the grammar by not having a SQL tool; the model can only choose columns, filters, ordering and limits on tables it is allowed to see. I prefer that trade for agent access, and I accept that it rules out “explain this slow query” workflows.

Supabase wrote up the other half of the problem in Defense in depth for MCP servers (September 2025). Beyond writes, they describe prompt injection through data already in the database: text in a row that instructs the model to read something else and leak it, which works even with Row Level Security in place. Their mitigations were read-only mode, project scoping, feature groups, wrapping query results with warnings, and manual approval of tool calls, with the honest line “These approaches reduced risk but did not eliminate it.” Their first recommendation is “Never connect AI agents directly to production data.” Neon says the same in its README. I agree for anything with a general SQL tool. For a server with structured, permission-scoped reads, I think a production read replica with a read-only role is defensible.

Where the connection string lives

This is the row in the table I would weigh most for a team.

With the reference server and Postgres MCP Pro, the Postgres connection string sits in each developer’s claude_desktop_config.json or .cursor/mcp.json. Every agent on the team connects as the same database role unless you mint a role per person, and the only audit trail is whatever Postgres logs for that role. Rotating the password means touching every laptop.

With Supabase and Neon the client holds an OAuth session or a token for the platform, and the platform holds the database credentials. Access can be revoked per user. Neon’s get_connection_string tool is worth knowing about, because an agent can still ask for the string and paste it somewhere.

With pgEdge and pREST the server holds the connection and the client holds a URL plus a token. On pREST, that token is a JWT or basic credential checked by the same middleware as the REST API, and every MCP call is an HTTP request through that stack, so it appears in the same access logs as everything else. v2.4.0 also added OpenTelemetry spans per JSON-RPC call and per tool invocation, so a trace shows POST /_mcp down to the SQL statement. For a read-only role to pair with it, the docs provide the SQL, with the caveat that “REST on the same server may still allow writes”, so the Postgres role is the durable safety net.

Two client configs side by side

Postgres MCP Pro, from its README, running in Docker. Note where the credential is:

{
  "mcpServers": {
    "postgres": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm", "-e", "DATABASE_URI",
        "crystaldba/postgres-mcp",
        "--access-mode=unrestricted"
      ],
      "env": {
        "DATABASE_URI": "postgresql://username:password@localhost:5432/dbname"
      }
    }
  }
}

I would change unrestricted to restricted before saving that file.

pREST through the prest-mcp stdio adapter, from the MCP tutorial:

{
  "mcpServers": {
    "prest": {
      "command": "prest-mcp",
      "env": {
        "PREST_MCP_URL": "https://api.example.com/_mcp",
        "PREST_MCP_TOKEN": "your-jwt-here"
      }
    }
  }
}

The token is an API credential checked by pREST and scoped by [[access.users]] rules; the database password stays in pREST’s config on the server. An HTTP-capable client can skip the adapter and point at /_mcp directly.

Claude Code accepts either shape. Its MCP docs show claude mcp add --transport http <name> <url> --header "Authorization: Bearer your-token" for remote servers and claude mcp add --transport stdio <name> -- <command> for local ones, and the Postgres example in those docs is a stdio server taking a connection string, which puts it in the first group above.

What I would pick

For a DBA-style session on a development database, where you want index suggestions and plan review, Postgres MCP Pro in restricted mode, since nothing else in this list does that work.

If you are already on Supabase or Neon, use their server with the read-only flag and project scoping, and keep agents off production the way both vendors tell you to. The platform-level auth and revocation are worth more than any feature.

If you are self-hosting Postgres and want agents reading production data without database credentials on laptops, pgEdge or pREST. Choose pgEdge if you want a general SQL tool with read-only transactions; choose pREST if you want the model restricted to permission-scoped selects, with table and field ACLs, and you may also want the REST API that comes with it. I would rather not defend a SQL grammar filter after the fact, which is why pREST’s MCP has none, and the same instinct shaped the MCP server in Vault, where every delete goes to the OS trash whether the GUI or the agent asked.

Do not use the archived reference server. It is unmaintained and the npm package still carries the injection Datadog described.

FAQ

Which Postgres MCP server is read-only by default?

pgEdge and pREST are read-only by default. pgEdge runs all queries in read-only transactions unless you change that per database, and pREST’s /_mcp has no write tools at all. The archived Anthropic reference server used a READ ONLY transaction, but a COMMIT; in the SQL escaped it. Postgres MCP Pro, Supabase and Neon all need an explicit flag or scope to become read-only.

Can I use an MCP server without giving the agent a connection string?

Yes. Supabase, Neon, pgEdge and pREST all keep the database credentials on the server side; the client holds an OAuth session, an API key or a JWT. Only the reference server and Postgres MCP Pro require the Postgres connection string in the client config. Neon exposes a get_connection_string tool, so an agent there can still retrieve one if the scope allows.

Is MCP safe for production databases?

Supabase and Neon both say not to connect agents to production, and I agree for any server with a general SQL tool, because prompt injection through stored data and read-only bypasses have both happened. A server that only offers permission-scoped structured reads, pointed at a read replica with a read-only Postgres role, is a much smaller surface and the setup I would accept. Treat the Postgres role as the last line of defense regardless of which server you pick.

Which works with Claude Code and Cursor?

All six. Claude Code supports stdio and HTTP transports through claude mcp add, and Cursor reads .cursor/mcp.json with a command entry; recent versions also accept a URL. Hosted servers (Supabase, Neon) are HTTP entries; the reference server and Postgres MCP Pro are normally stdio commands; pgEdge and pREST offer both, with pREST using the prest-mcp adapter for stdio-only clients.

Do I need a separate MCP process?

For stdio servers, yes: the client launches the process locally each time. Supabase and Neon run the process for you as a hosted endpoint. pREST serves /_mcp from the same binary and port as its REST API, so there is no extra service to deploy, though stdio-only clients still run the small prest-mcp adapter locally to bridge to that HTTP endpoint.

Does pREST’s MCP server do index tuning or EXPLAIN plans?

No. pREST exposes catalog discovery and column-filtered selects with a 100-row cap, and nothing else. If you want index recommendations, plan review or health checks, Postgres MCP Pro is the server built for that, and the two can run side by side against the same database with different roles.