All MCP Servers
Databases & StorageMCP Registrynpm7 toolsA · 87

PostgreSQL (hardened, read-only) MCP server

io.github.Eszetael · Updated

Read-only PostgreSQL over MCP. Writes refused at the parsed SQL, plus a READ ONLY transaction.

At a glance

PostgreSQL (hardened, read-only) is an MCP server in the Databases & Storage category. It runs locally over stdio via `npx -y postgres-mcp-hardened`, and needs DATABASE_URL. It exposes 7 tools, including explain_query, database_health and analyze_indexes. It scores A (87/100) on the MCP Skills Hub health score as of Oct 28, 2026.

Source 3GitHub stars

One-click install

Opens your editor and pre-fills the config. Your editor will ask before adding it.

How to install PostgreSQL (hardened, read-only) in your AI client

Setup for Claude Code, Claude Desktop, Cursor, Codex, Windsurf, Cline and VS Code — copy-paste ready.

How to install PostgreSQL (hardened, read-only) in Claude CodeAnthropic's agentic CLI

  1. Add the server

    bash
    claude mcp add postgres-mcp-hardened -e 'DATABASE_URL=<DATABASE_URL>' -- npx -y postgres-mcp-hardened
  2. Fill in your credentials

    Replace <DATABASE_URL> with your real value. Keep secrets out of any config file you commit.

  3. Verify it's connected

    bash
    claude mcp list
  4. Sharing it with your team?

    Add `--scope project` to write it to .mcp.json in the repository root, which you can commit (with the secrets left as environment variables).

  5. Use it in a session

    Ask Claude to use the tools this server exposes (e.g. explain_query, database_health, analyze_indexes). It discovers them automatically.

What does PostgreSQL (hardened, read-only) need from you?

API key — Connection string for the role the server connects as. Use a role that cannot write — the server refuses writes twice, but a read-only role is the layer that does not depend on us being correct. `--print-setup-sql` prints the SQL that crea…

  • DATABASE_URLrequiredsecretEnvironment variable

    Connection string for the role the server connects as. Use a role that cannot write — the server refuses writes twice, but a read-only role is the layer that does not depend on us being correct. `--print-setup-sql` prints the SQL that crea…

  • MCP_STATEMENT_TIMEOUToptionalEnvironment variable

    Server-side statement timeout, e.g. `5s`. A question that would pin the database is cancelled by PostgreSQL, not by hope.

  • MCP_ALLOW_TABLESoptionalEnvironment variable

    Comma-separated allowlist. A table off the list is refused by name, and hiding it inside a CTE, a view or a join does not help.

  • MCP_AUDIT_LOGoptionalEnvironment variable

    Path to the tamper-evident audit log. Entries are chained by hash and survive a restart; `--verify-audit` checks the chain against an off-host anchor.

  • DATABASE_URLrequiredsecretEnvironment variable

    Connection string for a role that cannot write.

  • MCP_ADDRoptionalEnvironment variable

    Address to bind, default 127.0.0.1:8080.

Pass secrets through your shell environment rather than committing them into a config file.

What tools does PostgreSQL (hardened, read-only) expose? (7)

Taken from the project's README — the server itself was not queried, so check its docs for the exact schemas.

  • explain_querythe execution plan; with `analyze` it runs the statement and reports real
  • database_healthcache hit ratio, connections (this database and the cluster), the longest
  • analyze_indexesunused indexes, duplicates, and tables scanned sequentially often enough
  • top_queriesthe heaviest statements, from `pg_stat_statements`.
  • security_posturewhat this deployment is actually able to do to your database, asked of
  • queryrun a read-only SQL query (validated, auto-`LIMIT`, cost-guarded). The response
  • list_schemas, `list_tables`, `describe_table` — progressive schema discovery

What the code can do

Read from the published package source — never executed. Parsed 4 of 4 source files.

This server can run other programs on your machine, read and write files and make network requests.

How it was published

  • Runs postinstall on install — this executes when you install the package, before you deliberately run anything.
  • Has a provenance attestation tying this artifact to the commit it was built from.
  • 1 npm maintainer — a single point of compromise · last published by eszetael_lab.

Maintainer of this package and think something here is wrong? Tell us and we'll correct it.

Before you install

Signals gathered from the source repo and package registry — not a safety verdict.

What it can reach

  • Runs code on your machine — Executes as a local process with your user's permissions, including filesystem access.
  • Downloads at launch — The package is fetched from its registry each run unless you pin a version.
  • Last commit 4 days ago — Actively maintained.
  • 3 GitHub stars — Very little community usage so far.
  • 210 weekly downloads — Regularly installed by other developers.

Client compatibility

Claude Code: NativeClaude Desktop: NativeCursor: NativeCodex CLI: NativeWindsurf: NativeCline: NativeVS Code: Native

Health score

A · 87

Maintenance, adoption, transparency and packaging — not a safety audit. How this is calculated

  • Maintenance30%100%

    Last commit 4 days ago.

  • Adoption20%36%

    3 stars, 210 weekly downloads.

  • Transparency20%100%

    4/4: ✓ public source repo, ✓ declared license, ✓ documentation, ✓ discoverable tool list.

  • Provenance15%100%

    Package and registry entry point at the same repository.

  • Packaging15%100%

    Publishes a pinned version (0.1.10).

Documentation

Full README

postgres-mcp-hardened

### 🚧 Version 0.1.10 — the rest of an outside review, and a fix that was too blunt Published: binaries for five platforms with checksums, Sigstore signatures and build provenance; .mcpb bundles for one-click install; an image on ghcr.io for amd64 and arm64; a package on npm; and an entry in the official MCP registry. 0.1.8 and 0.1.9 closed six bypasses that four independent reviewers found in 0.1.7, none of them found by us. Two days of our own adversarial work had come back mostly clean the day before. Passing the tests you thought to write is not the same as looking. The one that mattered most needs no privileges at all: with a column redacted, a join on it through USING answered whether a given value was present, which is a complete equality oracle against the least-privilege reader this project tells you to configure. 0.1.10 closes the three findings that were left open. With MCP_ALLOW_SCHEMAS=public, SELECT * FROM secret.salaries was refused while describe_table handed over every column, type and default of that same table. An ordinary connection string carrying no sslmode used the driver's prefer, which sends everything in the clear whenever the server declines TLS, and anyone on the wire can make it decline. And MCP_SSLROOTCERT added a private certificate authority to 242 public ones instead of replacing them, so an operator who believed they had pinned trust to their own issuer had not. The first version of that TLS fix was wrong in a way worth reading about. It asked "is this loopback" and demanded TLS from everything else, which took down six PostgreSQL version jobs, the conformance run, the container check and the adversarial corpus in a single push. All of them reach the database the way ordinary deployments do: postgres://user:pass@postgres:5432/db, a service name on a private network. Docker Compose and Kubernetes are not the public internet, and a server that demands TLS from a container on a bridge network is a server people switch off entirely. The question is now whether somebody untrusted can sit on the wire, not whether the address is loopback, and a real socket to a PostgreSQL reachable only by service name is in the test suite, because a rule about networks should be tested over a network. A resource limit is documented and not solved, in `THREAT_MODEL.md`: 49 bytes of SQL make PostgreSQL fold a constant into 5.9 GB of backend memory during planning, and a five second statement_timeout does not stop it. The obvious shapes are refused; the general problem is upstream of anything this server can do. Every change here was reproduced against a running server before being fixed, and each is in `CHANGELOG.md` with the query. Everything here is 0.1.x because nobody outside this project has run it against their own data.

The official Postgres MCP server was deprecated in 2024 and still gets 467k downloads a month. Its entire defence is one database-level read-only transaction — and that alone does not stop every write. This is a maintained Rust replacement with defence in depth.

A drop-in Model Context Protocol server that lets an AI agent query PostgreSQL — read-only, enforced at the database level, with real SQL validation, timeouts, cost limits, OAuth 2.1, and an audit trail. Speaks Streamable HTTP and stdio, and negotiates the MCP revision: 2026-07-28 (current, and the default since upstream released it on 2026-08-03), 2025-11-25, and 2025-06-18 — what most shipping clients still speak today. A client asks for what it knows; it is not negotiated down.

Try to break it — one command, no database

The read-only guard has an offline mode. Hand it a statement and it says what it decided: no database, no configuration, nothing installed permanently.

sh
npx postgres-mcp-hardened --validate "/* comment */ DROP TABLE users"
# REJECT: non-read-only statement: Drop

npx postgres-mcp-hardened --validate "SELECT 1; DROP TABLE users"
# REJECT: multiple statements are forbidden

npx postgres-mcp-hardened --validate "WITH d AS (DELETE FROM t RETURNING *) SELECT * FROM d"
# REJECT: non-read-only statement: non-read-only query (CTE / SELECT INTO / FOR UPDATE)

npx postgres-mcp-hardened --validate "SELECT * FROM orders WHERE id = 1"
# ALLOW

If something that writes comes back `ALLOW`, that is the most valuable thing anyone can send us. It needs no working exploit and no write-up — one line of SQL and "this should not be allowed" is a complete report. Anything that gets past the guard goes through `SECURITY.md`; everything else is an ordinary issue, and the bar for opening one is *this looks wrong to me*, not *I am certain*.

The fuzzer is deterministic and prints its seed, so whatever it finds reproduces on a machine that has never seen yours — a million mutations take about a minute:

sh
npx postgres-mcp-hardened --fuzz 1000000
# fuzz: 1000000 iterations, seed 1592594996, slowest validation 8 ms
# RESULT: 0 invariant violations

For the whole thing against a real database, docker compose -f examples/docker-compose.yml up -d brings up PostgreSQL with sample data and the server in front of it, connecting as a role that holds SELECT and nothing else.

Every bypass found so far lives in the MUST_REJECT corpus in src/validate.rs and runs on every commit, recorded with what it cost rather than tidied away. Yours would join them.

Why

@modelcontextprotocol/server-postgres is deprecated on npm (last publish December 2024) and still sees 475,790 downloads in the 30 days to 9 August 2026. Credit where it is due: its approach is not naive — it wraps each query in BEGIN TRANSACTION READ ONLY and always ROLLBACKs, which is a real defence and one this server now adopts as well.

Excerpt — the full document is on GitHub.

Maintain PostgreSQL (hardened, read-only)? Add a badge to your README

Shows its health grade, and links readers to install steps for every client.

markdown
[![PostgreSQL (hardened, read-only) on MCP Skills Hub](https://mcp-skills-hub.vercel.app/badge/io-github-eszetael-postgres-mcp-hardened)](https://mcp-skills-hub.vercel.app/mcps/io-github-eszetael-postgres-mcp-hardened)

Catalog data refreshed . Something wrong? Report it.