MCP server-postgres is deprecated: what to use instead

@modelcontextprotocol/server-postgres is archived, deprecated on npm, and its read-only mode can be escaped. What each alternative enforces, and how to switch.

@modelcontextprotocol/server-postgres was the reference Postgres server from the Model Context Protocol project. It is now deprecated on npm, its code sits in an archived repository, and its read-only mode can be escaped with a semicolon. npm still counted 435,651 downloads of it in the 30 days to 18 October 2026, and that undercounts: npm's API reports zero downloads for every package on 13 to 15 October.

If it's in your Claude, Cursor or VS Code config, this post covers what happened to it, what each replacement actually enforces, and how to switch. One of those replacements is Midplane, which we build. We describe the others the way their own docs and source code do, and we say when one of them fits better than ours.

Which one fits

If you wantLook at
An agent that only reads, where every table you grant is fine to read in fullA read-only role, with any server
Help tuning and diagnosing Postgres: indexes, query plans, health checksPostgres MCP Pro
One server for Postgres, MySQL, SQL Server and other enginesDBHub
An agent that manages a Supabase project, not just queries itSupabase MCP
An agent working on Neon development branchesNeon MCP
A fixed set of queries you write in advanceGoogle MCP Toolbox
Entra ID sign-in on AzureMicrosoft postgres-mcp
An agent on real data, with a per-table policy, masking, held writes and an audit trailMidplane (ours)

What happened to server-postgres

  • It's deprecated on npm. All eight published versions carry the same notice, and npm prints it when you install:

    npm warn deprecated @modelcontextprotocol/[email protected]: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
  • Its code is archived. It now lives in modelcontextprotocol/servers-archived, a read-only repository whose description is "Reference MCP servers that are no longer maintained". The main servers README lists it under Archived. The same README still uses it in its example Claude Desktop config, which may explain some of the installs.

  • The npm package was never patched. The last release, 0.6.2, dates from 4 December 2024. The fix for the bypass below was merged in the main repository on 29 May 2025 (PR #1889), the same day Datadog dates the archiving, and it was never published. We unpacked the 0.6.2 tarball on 23 October 2026: it still hands the agent's SQL straight to client.query().

Is there an official Postgres MCP server now? Not from the MCP project, and as far as we can find, not from the PostgreSQL project either. Several database vendors ship their own: Supabase, Neon, Google and Microsoft among them. The rest are community and commercial projects.

The read-only bypass

The primary write-up is Datadog Security Labs' MCP vulnerability case study: SQL injection in the Postgres MCP server, by Santiago Mola, published 21 August 2025.

The server's one tool, query, opened a read-only transaction, ran the agent's string, and rolled back. It sent that string to node-postgres with no parameters, so the driver used Postgres's simple query protocol, which runs every statement in the string. This is what the database received when the agent's input had a COMMIT in it:

BEGIN TRANSACTION READ ONLY;
-- the agent's input, sent as one string:
SELECT 1; COMMIT; DELETE FROM orders;
-- the server's cleanup:
ROLLBACK;

The COMMIT ends the read-only transaction. The DELETE then runs on its own, outside any transaction, and commits. By the time the ROLLBACK arrives, there's nothing left to undo. Datadog's example payload was COMMIT; DROP SCHEMA public CASCADE;. They also showed a second problem: connections went back to the pool unchanged, so COMMIT; SET statement_timeout TO 1; changed settings for later queries that reused that connection.

Per Datadog's timeline, the bug was first reported on HackerOne in November 2024, and Zed's fork (@zeddotdev/postgres-context-server 0.1.4) shipped a patch in April 2025. The upstream fix switched to prepared statements and discarded each connection after use.

We reproduced the bypass on 23 October 2026, running 0.6.2 against Postgres 17 in Docker with a 1,000-row orders table:

query callConnected as a superuserConnected as a SELECT-only role
DELETE FROM ordersRefused: cannot execute DELETE in a read-only transactionRefused the same way
SELECT 1; COMMIT; DELETE FROM ordersSucceeds; orders now has 0 rowsRefused: permission denied for table orders

The transaction was never the boundary; the role was. The maintainers reached the same conclusion, and the patch that fixed the bug also added this caution to the README: "If you need to enforce read-only access for security, create a separate database user with only SELECT permissions instead of relying on this server's built-in restrictions."

If you can't switch today

Give the server a role that can only read, and put that role in its connection string. As an admin:

CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER ROLE mcp_readonly SET statement_timeout = '30s';
  • The grants cover today's tables only. For tables created later, run ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly; as the role your migrations use.
  • In a database created before Postgres 15, every role can create tables in public unless someone revoked that, and upgrading doesn't change it (release notes). Run REVOKE CREATE ON SCHEMA public FROM PUBLIC; there.
  • The timeout is a default the agent can change with SET, not a limit it can't get around.

This closes the write half of the bypass. It doesn't fix the shared connection state, and the role can still read every row of every table you grant it.

What the alternatives enforce

We checked each option against its own docs and source on 23 October 2026. "Read-only" means something different in almost every one of them.

OptionRead-only is enforced byIn its own words
A read-only role, nothing elsePostgres privileges(It's Postgres: the database refuses what the role can't do)
Postgres MCP ProRestricted mode: a pglast parse checked against allowlists, then a read-only transactionRestricted mode is "suitable for production environments"
DBHubreadonly = true on the tool: a keyword allowlist, then BEGIN READ ONLYConnect with "a least-privilege, read-only database user"
Supabase MCPread_only=true: queries run as a read-only Postgres userConnect to production "only when the task requires production evidence"
Neon MCPOff by default; when on, run_sql runs in a read-only transaction"We do not recommend using the Neon MCP Server in production environments."
Google MCP ToolboxFixed, parameterized tools you write; a session-level lock on Cloud SQL and AlloyDB onlyIts execute_sql tool "shouldn't be used for production agents"
Microsoft postgres-mcppostgres_mcp_query is documented as read-only; the mechanism isn't published"The server doesn't decide what's allowed — your database does."
MidplanePostgres's own parser, a per-table policy, one statement per call, read-only transactionsA role of its own is "a second lock"

A read-only role, and nothing else

Every option below should sit on top of one, because the database enforces grants whatever the server in front of it does. A role can be finer than whole tables, with column grants such as GRANT SELECT (id, plan) ON customers TO mcp_readonly and row-level security. What it doesn't give you is a record of what the agent asked for and why, masking that still lets the agent join on a column, or a way to let some writes through once a person has looked at them.

Postgres MCP Pro

crystaldba/postgres-mcp is MIT-licensed and written in Python. --access-mode defaults to unrestricted, which allows full reads and writes, so start it with --access-mode=restricted. In restricted mode it parses each statement with pglast and validates the tree against allowlists of statement types, node types and functions, then runs it in a read-only transaction with a 30-second limit. The README is candid about the limits: "If you have unsafe stored procedure languages enabled on your database, then our read-only protections could be circumvented," and "We may add measures in the future to make sure that restricted mode is safe to use with production databases." Its real strength is the rest of its toolbox: index tuning, query plans and health checks.

DBHub

bytebase/dbhub is MIT-licensed, made by Bytebase, and speaks PostgreSQL, MySQL, MariaDB, SQL Server, Oracle and SQLite. Read-only is now a setting on the execute_sql tool in its TOML config (readonly = true); the old --readonly flag exits with an error. On Postgres, a read-only call passes a keyword allowlist and then runs inside BEGIN READ ONLY, a second layer added in 0.22.6. It also has max_rows and a query_timeout. Its docs say what read-only doesn't cover: a superuser "can still read or write server files and run commands".

Supabase MCP

supabase/mcp goes through Supabase's Management API rather than a direct database connection. With read_only=true, it hides the write tools and runs queries as a read-only Postgres user. project_ref scopes it to one project, and feature groups trim which tools exist at all. On 4 September 2026, Supabase replaced the line "Don't connect to production" in its docs with the guidance quoted above.

Neon MCP

neondatabase/mcp-server-neon is now a hosted server at mcp.neon.tech. Read-only mode is off by default. When it's on, the server hides its write tools and runs run_sql in a read-only transaction. Without roleName, "SQL runs as the database owner", so pick a role.

Google MCP Toolbox for Databases

googleapis/mcp-toolbox is Apache-2.0, and its distinctive mode is the opposite of open SQL. You write each tool in tools.yaml as a fixed statement, and its parameters run as a prepared statement: the agent fills in values but can't change the query. The exception is template parameters, which can stand in for names and which its docs say make a tool "more vulnerable to SQL injections". The prebuilt tools include an open execute_sql.

Microsoft postgres-mcp

microsoft/postgres-mcp is MIT-licensed and in preview. It works with any Postgres, and picks Microsoft Entra ID sign-in automatically for Azure profiles. Writes go through a separate tool, which profiles allow unless you set access_mode: ro. The repository holds docs only, and the npm package ships a compiled binary, so we couldn't check how read-only is enforced.

Midplane

midplaneai/midplane is MIT-licensed, and it's the one we build. It is a gateway between your agents and Postgres. It parses each statement with Postgres's own parser (libpg_query, compiled to WebAssembly), denies anything it can't parse or resolve against the catalog, and checks the result against a per-table policy that denies by default. Each call is one statement, and reads run in read-only transactions with a timeout. Writes need a WHERE (what that does and doesn't catch), and writes hidden inside a WITH are denied. On top of that, it can mask columns inside the query (masking), hold writes for a person's approval (approvals and taint), and write an audit record to disk before each statement runs (audit).

Switching from server-postgres to Midplane

This is the local mode from the docs: the policy lives in a file, and nothing talks to Midplane Cloud. You need Node 24.16 or newer.

Before. The config most people copied, straight from the servers README:

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
    }
  }
}

After.

  1. Create the read-only role from the section above (the docs call it midplane_gateway: prepare your database).

  2. Write midplane.yaml next to where you'll run the gateway:

    # midplane.yaml: the policy lives in this file; no Midplane Cloud.
    audit:
      file: midplane-audit.db
    auth:
      issuer: my-laptop
      public_key_file: midplane-verify-key.json
    databases:
      main:
        dsn: { env: DSN_MAIN }
        policy:
          table_access:
            default: deny            # a table not listed is closed
            tables:
              public.orders: read
              public.customers: read

    default: read would open every table the policy doesn't list, which is what server-postgres gave you. Listing tables is the safer start. Every key is in the configuration reference.

  3. Make a signing key and a token, then start the gateway:

    export DSN_MAIN='postgres://mcp_readonly:change-me@localhost:5432/mydb'
    npx midplane keygen --out .
    npx midplane token --config midplane.yaml --key midplane-signing-key.json \
      --sub [email protected] > token.txt
    npx midplane local --config midplane.yaml

    The token is read-only by default and valid for an hour; --ttl changes that. Keep midplane-signing-key.json secret, because it mints tokens.

  4. Point your agent at it. For Claude Code:

    claude mcp add --transport http midplane http://127.0.0.1:7433/mcp \
      --header "Authorization: Bearer $(cat token.txt)"

Clients that start servers from a JSON file can run the gateway over stdio instead:

{
  "mcpServers": {
    "midplane": {
      "command": "npx",
      "args": ["midplane", "local", "--config", "/absolute/path/to/midplane.yaml", "--stdio"],
      "env": {
        "MIDPLANE_TOKEN": "paste the contents of token.txt",
        "DSN_MAIN": "postgres://mcp_readonly:change-me@localhost:5432/mydb"
      }
    }
  }
}

That file holds the connection string and the token, and the gateway runs as your own user, so an agent with a shell or access to your files could read them. Beyond a demo, serve over HTTP, with the gateway running as another user or on another host.

We ran these steps against the same Docker database with Midplane 0.22.0. What the agent will notice after the switch:

  • Four tools instead of one: query, list_tables, describe_table and check_approval.
  • One statement per call. The bypass from above is denied before it reaches the database: "it contains 3 statements".
  • A query returns at most 1,000 rows by default (limits.max_rows), and statements time out after 30 seconds.
  • EXPLAIN, SET, SHOW and transaction control are refused.

What Midplane doesn't do

  • It's Postgres only.
  • It isn't a tuning tool. It refuses EXPLAIN, so for index advice and query plans, Postgres MCP Pro is the better fit.
  • Local mode can't approve writes. A write the policy holds is refused, and there's no dashboard. Approvals need a gateway linked to Midplane Cloud, which is invite-only for now (get started).
  • It only sees what goes through it. If the agent also has psql and a superuser URL in its shell, the gateway never sees those statements.

Whatever you pick, the minimum is the same: a role of its own, SELECT only unless you mean to allow writes, and a server that sends one statement at a time.

Get access

Which AI will query your database?

Pick all that apply (optional).

Where does your Postgres database run?

Pick one (optional).

We use your details only to reply. Privacy policy