Skip to content

Topic · A2

The Postgres MCP Server (and Why Anthropic's Was Archived)

Anthropic archived their reference Postgres MCP server in 2025 with a known SQL-injection bypass still in the wild. crystaldba/postgres-mcp is the replacement most teams ship. This is the comparison, the install, and the security model.

For most of 2024, the canonical way to give Claude (or any MCP client) access to a Postgres database was @modelcontextprotocol/server-postgres — Anthropic's reference implementation, shipped from the modelcontextprotocol/servers monorepo. In 2025 that changed: Anthropic archived the server. Six other reference servers were archived in the same sweep (GitHub, Slack, Puppeteer, Google Drive, SQLite, Brave Search). The archive notice was brief and pointed to "community alternatives" without naming one.

Two things make this story worse than a standard deprecation. First, the archived Postgres server has a known SQL-injection bypass — a COMMIT; DROP TABLE users; -- style payload could escape the supposed parameterization — that was never patched. Second, tutorials and example configs across the web still point to @modelcontextprotocol/server-postgres because they were written before the archive. New users installing today land on insecure-by-default infrastructure.

This page is the migration. What the replacement is, how to install it, how to lock it down, and what to know about row-level security and the broader trifecta of risks that any database-touching MCP server faces.

The replacement: crystaldba/postgres-mcp

crystaldba/postgres-mcp (repo) is the most-installed community Postgres MCP server in May 2026. It's the replacement we'd ship and the one cited by the Vulnerable MCP Project as a safer baseline.

What it does:

  • Connects to any Postgres database via a standard connection string
  • Exposes tools for query, schema_inspect, list_tables, describe_table
  • Supports read-only mode (no DDL, no DML)
  • Supports explicit table allowlist
  • Parameterizes queries properly — no string concatenation in SQL construction
  • Stdio transport by default (local subprocess)
  • Optional Streamable HTTP transport for remote use
The install:
# In your Claude Code MCP config
{
  "mcpServers": {
    "postgres": {
      "command": "uvx",
      "args": ["postgres-mcp"],
      "env": {
        "DATABASE_URL": "postgresql://readonly_user:pw@localhost:5432/mydb",
        "READ_ONLY": "true",
        "ALLOWED_TABLES": "users,orders,products"
      }
    }
  }
}
uvx is the uv Python package runner — installs and runs the latest postgres-mcp in a temporary environment. If you don't want uv, the server is also packaged on PyPI as postgres-mcp and on npm as @crystaldba/postgres-mcp.

The security model

Three layers, each cumulative.

Layer 1 — Postgres role. Create a dedicated role for the MCP server. Grant only what the agent needs. For read-only Q&A use cases, that's SELECT on specific tables. Do not point the MCP server at a superuser connection.
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 TABLE users, orders, products TO mcp_readonly;
-- That's it. No DELETE, no UPDATE, no INSERT, no DDL.
Layer 2 — MCP server flags. Set READ_ONLY=true and ALLOWED_TABLES to constrain at the application layer too. Defense in depth — if the role somehow has more permissions than intended, the MCP server still won't execute writes. Layer 3 — Network. Default to stdio transport. The MCP server runs as a subprocess of Claude Code, on your machine. Nothing is exposed to the network. If you need remote access (multi-machine deployments, hosted agent flows), use Streamable HTTP behind OAuth and add IP allowlisting at your network edge.

Row-level security is not free

If your Postgres database uses row-level security to enforce tenant isolation, the MCP server inherits whatever role you give it. A role that bypasses RLS (like a postgres superuser or a role with BYPASSRLS) means the agent sees every tenant's data regardless of the agent's intended scope.

The fix is the same as for any application: the MCP role should have RLS enforced. The role should be tagged with the right session variables for the tenant on connect, either via SET LOCAL or via Postgres' current_setting() machinery. The MCP server doesn't do this for you. If your RLS depends on per-request context, you need either a connection-pooler pattern or a custom MCP server that knows your tenancy model.

This is why the Supabase team explicitly says supabase-community/supabase-mcp is for development and testing, not production. They're being honest — RLS through MCP is a non-trivial design problem and most setups don't get it right by default.

The lethal trifecta and Postgres MCP

Simon Willison's lethal trifecta names the three properties that together make an MCP server architecturally risky:

  1. Access to private data
  2. Exposure to untrusted external content
  3. Ability to exfiltrate via tool calls
A Postgres MCP server has property 1 by definition. Property 2 enters whenever the agent reads anything — a Slack message, a PR comment, a webpage — that could contain prompt-injection. Property 3 is whatever else the agent can do.

The mitigation: scope each property narrowly. Read-only Postgres reduces the blast radius. Trusted-content-only contexts (no web fetch, no user-submitted content) reduce property 2. Disabling email/Slack/HTTP tools in the same session reduces property 3. If you can't reduce any of the three, assume worst-case.

Comparison with supabase-community/supabase-mcp

If you're on Supabase specifically, the calculus is different. supabase-community/supabase-mcp (repo) wraps Supabase's REST and RPC layer with auth-aware queries. It respects the auth context you pass — if you connect with a user-scoped JWT, RLS applies as Supabase intends.

Propertycrystaldba/postgres-mcpsupabase-community/supabase-mcp
ConnectionDirect PostgresSupabase REST/RPC
RLS handlingManual via role configAutomatic if auth context passed
Production-readyYes (with role + flag config)Officially dev/test only
AuthConnection string credentialsSupabase JWT
Hostable scopeAny PostgresSupabase-hosted only
The choice: self-hosted Postgres → crystaldba. Supabase-hosted → supabase (with the "dev/test" caveat in mind).

What this MCP server should never do

A short list of patterns that look like good ideas and aren't:

Don't give it write access by default. Even if your use case is "let the agent fix data," the failure mode of an agent gone wrong is far worse for writes than reads. Read-only first; flip to write only with explicit user confirmation. Don't connect it to your production database. A local dev replica or a read-replica with sanitized data is the right target. We have seen multiple production incidents where an agent issued queries with broken WHERE clauses against prod and locked tables. Don't expose it over the public internet. Stdio transport is the default for a reason. If you need remote access, put OAuth, IP allowlists, and rate limits in front of it. Don't share the connection string in committed config files. The MCP config is in ~/.claude/ or similar — easy to commit by accident. Use env vars or a secret manager.

Where this fails

No MCP server is a substitute for proper access control. The hardening above is the bare minimum. Production agent access to databases needs auditing, query logging, and incident response. The MCP server is plumbing; the security posture is a system property. The replacement landscape is shifting. crystaldba/postgres-mcp is the de facto replacement today but the ecosystem is young. We re-evaluate quarterly. Other contenders: pgedge/pgedge-mcp, syahiidkamil/postgres-full-access-mcp (we'd avoid the latter — "full access" is exactly what you don't want by default). Old tutorials still recommend the archived server. Search results for "claude postgres mcp" surface posts from 2024 that say npx @modelcontextprotocol/server-postgres. Treat any tutorial that references that package as out-of-date. RLS through MCP is genuinely hard. The "use a per-tenant role with SET LOCAL" pattern requires a connection-pooler or custom logic. Most teams' first attempt is wrong. If multi-tenancy is your concern, plan for engineering effort.

What to read next

Sources

Frequently asked

Why was the official Postgres MCP server archived?
Anthropic archived modelcontextprotocol/server-postgres in 2025, along with five other reference servers (GitHub, Slack, Puppeteer, Google Drive, SQLite, Brave Search). The Postgres server specifically had a known SQL-injection bypass via 'COMMIT; DROP TABLE' pattern that was never patched in the archived branch. The archive notice points to community replacements without endorsing one — crystaldba/postgres-mcp has emerged as the de facto replacement.
Is the archived Postgres MCP server still safe to use?
No. The SQL-injection bypass remains in the published code. We see installs still pointing at the archived repo because tutorials reference it and the migration path was never clearly documented. If you have @modelcontextprotocol/server-postgres in your config, replace it.
What's the difference between crystaldba/postgres-mcp and supabase-community/supabase-mcp?
Different scope. crystaldba/postgres-mcp connects to any Postgres database via connection string. supabase-community/supabase-mcp wraps the Supabase REST and RPC layer with auth-aware queries — it's Supabase-specific and respects row-level security. Use crystaldba for self-hosted Postgres; use supabase for Supabase-hosted projects.
Can I make a Postgres MCP server read-only?
Yes. crystaldba/postgres-mcp supports a read-only flag and an explicit table-allowlist. We recommend read-only as the default for any agent setup that doesn't have a specific reason for write access. The SQL-injection risk drops dramatically when the connection cannot execute DDL or modify rows.
Does the Postgres MCP server expose my database to the internet?
Only if you configure remote transport. The default stdio transport is local-only — the MCP server runs as a subprocess of Claude Code or your agent client, on your machine. Use Streamable HTTP transport if you need remote access, and put it behind auth.
How does the Postgres MCP server handle row-level security?
It doesn't, unless your Postgres role is configured to. The MCP server uses whatever role you put in the connection string; if that role bypasses RLS, the agent bypasses RLS. The fix is to create a dedicated MCP role with the minimum permissions and let Postgres enforce RLS at the role level.

Related topics