Skip to main content
Glama
samuel-cabral

safe-postgres-mcp

safe-postgres-mcp

The safe default for giving an AI agent your Postgres. A zero-config, read-only PostgreSQL MCP server where read-only isn't a regex you hope holds — it's enforced by Postgres itself. Every agent-supplied query runs inside a BEGIN TRANSACTION READ ONLY with a statement timeout and a row cap, and is sent over the extended wire protocol, which cannot carry a second command. One npx command: your agent can explore a schema and run SELECTs, but it physically cannot write, cannot stack a second statement, and a runaway query is cancelled within the statement timeout (default 5s) so it can't hang your database.

CI License: MIT Node TypeScript


Why another Postgres MCP server?

Some "read-only" database tools enforce read-only by scanning the SQL text for scary keywords — a filter, and filters get bypassed. Others use the right primitive but stop too early: the original TypeScript reference Postgres MCP server (@modelcontextprotocol/server-postgres) wrapped each query in a BEGIN TRANSACTION READ ONLY and nothing more — no statement timeout, no row cap, no rejection of multi-statement input, and no bound on how much a single query could buffer into the agent’s context. It has since been archived in the official modelcontextprotocol/servers-archived repo with no maintained successor. Meanwhile popular DBA-oriented alternatives (e.g. Postgres MCP "Pro" / crystaldba) default to unrestricted read/write — read-only is an opt-in access-mode flag — and ship as a Python/Docker install that is friction for the Node/TS majority of MCP users. (Check each project's current docs; the ecosystem moves fast.)

This server inverts that. The guarantee doesn't live in a string matcher — it lives in the database engine.

Defense in depth, from the outside in:

Layer

What it does

Is it the guarantee?

Keyword pre-check

Rejects obvious writes (DELETE FROM …) and stacked statements before a round-trip, with a clear error

No — it's fast-fail UX + a second line

SET LOCAL statement_timeout

Postgres cancels a runaway query instead of hanging your DB

Resource guardrail

Extended wire protocol

Agent SQL is sent as a prepared statement, which by construction carries exactly one command — a smuggled ; COMMIT is refused by the server, not by a parser of ours

Yes — for statement stacking

BEGIN TRANSACTION READ ONLY

The engine rejects any write (INSERT/UPDATE/DELETE/DDL/…) at execution time

Yes — this is the real guarantee

Row cap + ROLLBACK

Truncates oversized result sets; never commits anything

Resource guardrail

The keyword check is deliberately a courtesy, not the wall. Casing tricks, comment smuggling, or a data-modifying CTE that slips past the text analysis still hit a Postgres READ ONLY transaction and fail with error 25006. Belt and suspenders — and the suspenders are bolted to the engine.

For your outermost layer, point DATABASE_URL at a dedicated least-privilege read-only role (see .env.example). This server is the second wall behind that role, not a replacement for it.


Related MCP server: MCP Server for Database

60-second quickstart

You need Node >= 20 and a Postgres connection string. Two of the most common clients:

Claude Code

claude mcp add --env DATABASE_URL=postgres://user:pass@host:5432/dbname \
  --transport stdio safe-postgres -- npx -y safe-postgres-mcp

The order matters: keep another flag (--transport stdio) between --env KEY=value and the server name, otherwise the CLI parses the name as another KEY=value pair. Everything after -- is handed to the server untouched.

Verify it's connected:

claude mcp list
claude mcp get safe-postgres

Add --scope project to share it with your team via a checked-in .mcp.json, or --scope user to enable it across all your projects.

Claude Desktop

Open Settings → Developer → Edit Config (or edit the file directly):

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "safe-postgres": {
      "command": "npx",
      "args": ["-y", "safe-postgres-mcp"],
      "env": {
        "DATABASE_URL": "postgres://user:pass@host:5432/dbname"
      }
    }
  }
}

Fully quit and reopen Claude Desktop to load it. If it doesn't appear, check ~/Library/Logs/Claude/mcp-server-safe-postgres.log (the server logs all diagnostics to stderr; stdout is reserved for the JSON-RPC channel).

Running from a local build

No npm publish required — build once and point any client at the absolute path:

git clone https://github.com/samuel-cabral/safe-postgres-mcp.git
cd safe-postgres-mcp && npm ci && npm run build
claude mcp add --env DATABASE_URL=postgres://user:pass@host:5432/dbname \
  --transport stdio safe-postgres -- node /abs/path/to/safe-postgres-mcp/build/index.js

The server fails fast: a missing/invalid DATABASE_URL or an unreachable database exits with an actionable message before the agent ever calls a tool.


Tools

Five small, curated tools — a focused read-only toolset, deliberately not a 14-tool management suite. Every agent-supplied query runs inside a READ ONLY transaction, so the engine refuses a write regardless of what the SQL says; the three introspection tools issue only fixed, parameterized system-catalog SELECTs.

Tool

Description

Parameters

Access

query

Run a single read-only SQL statement inside a READ ONLY transaction. A LIMIT is injected if you omit one; returns rows, field types, and a truncated flag.

sql (string, required)

Executes SQL (read-only tx)

explain_query

Return the query plan via EXPLAIN (FORMAT JSON) without running the query. ANALYZE/ANALYSE is rejected (it would execute the target), and the statement is sent as a prepared statement inside a READ ONLY transaction. Inspect cost/joins/index usage before paying for the query.

sql (string, required)

Plans the query; never executes it

list_schemas

List all non-system schemas with their owner.

Catalog read

list_tables

List tables, views, and materialized views in a schema with approximate row counts (from planner stats — fast, no full scan) and on-disk size.

schema (string, default public)

Catalog read

describe_table

Full description of one table/view: columns (type, nullability, default), primary key, foreign keys, and indexes.

table (string, required), schema (string, default public)

Catalog read

Introspection tools query the Postgres system catalogs with parameterized lookups — identifiers are never string-interpolated into SQL. Every tool returns both human-readable text and typed structuredContent matching its output schema, so an agent can consume either.


Safety model

Each row is a thing an agent (or a hostile prompt steering one) could try, and the mechanism that stops it.

Threat

Mitigation

Write / DDL — INSERT, UPDATE, DELETE, DROP, TRUNCATE, GRANT, …

Rejected by the READ ONLY transaction at execution time (Postgres 25006); also fast-failed by the keyword pre-check

Stacked-query injection — a second, destructive statement smuggled after a semicolon

Agent SQL travels over the extended wire protocol (a prepared statement), which can hold only one command — Postgres itself answers a second one with 42601. A literal/comment/dollar-quote-aware statement splitter rejects multi-statement input first, for a clearer error; the protocol is the part that cannot be talked around

Data-modifying CTE — WITH x AS (DELETE … RETURNING *) SELECT …

Dedicated hidden-write scan of CTE bodies, backstopped by the READ ONLY transaction

Comment / casing smuggling — /* SELECT */ DELETE …

Comments stripped (respecting string and dollar-quoted literals) before the head check; real enforcement is in the engine, not the text

Runaway query hanging the DB — SELECT pg_sleep(3600)

statement_timeout (per-transaction SET LOCAL, applied inside the tx, + pool-level default) cancels it; default 5s

Oversized result set blowing up memory / agent context

The result is streamed through a server-side cursor that stops at maxRows + 1 rows, so node-pg never buffers more than the cap — even if the query carries its own larger LIMIT. Excess is truncated and truncated: true is returned; default 500 rows

Accidental persistence

Every transaction ends in ROLLBACK — the server never commits

Identifier injection via introspection

System-catalog lookups are parameterized; no identifier interpolation

Disabling a guardrail via a bad env var

Config is validated with hard ceilings (MAX_ROWS ≤ 10,000, QUERY_TIMEOUT_MS ≤ 120,000ms); garbage values refuse to start rather than silently weakening a limit

What this does not do: it does not mask or redact PII in rows you are allowed to SELECT, and it does not substitute for database-level permissions. Grant the connecting role only what the agent should ever see; this server enforces read-only and bounded, not authorized.


Configuration

All configuration is environment variables — that's the whole point of zero-config. See .env.example.

Variable

Required

Default

Max

Description

DATABASE_URL

Yes

Postgres connection string. Point it at a least-privilege read-only role.

POSTGRES_URL

Fallback used only when DATABASE_URL is unset.

QUERY_TIMEOUT_MS

No

5000

120000

Per-statement timeout in ms. A slow query is cancelled, not run forever.

MAX_ROWS

No

500

10000

Hard cap on rows returned by query. Excess rows are truncated and flagged.


When to use this vs. alternatives

  • Use this when you want an agent to safely read a plain Postgres — RDS, Neon, Supabase-as-plain-PG, or self-hosted — with a READ ONLY guarantee enforced by the database, installed with one npx command and no YAML, no Docker, no Go binary.

  • Reach for a DBA-oriented tool (index tuning, health checks, hypothetical indexes) when you're doing performance engineering rather than agent-safe reads — and you're comfortable running it write-enabled.

  • Reach for a cloud-vendor server when you're fully inside that vendor's ecosystem (its auth, storage, and edge functions) and don't need neutral, portable Postgres access.


Development

npm ci
npm run build       # tsc -> build/, chmod +x the bin
npm run typecheck   # strict TS, no emit
npm test            # vitest run

The suite runs 137 unit + MCP wiring tests with zero external dependencies. Safety parsing (comment stripping, statement splitting, literal-aware CTE-write detection, LIMIT/FETCH row-cap logic, dollar-quote tags) is exercised directly, and the MCP layer is tested end-to-end over an in-memory client/server transport — rejection paths run fully without a live database, because the safety check fires before the connection pool is ever touched.

A further 24 integration tests run against a real Postgres and are skipped automatically unless a DB is provided:

DATABASE_URL=postgres://user:pass@localhost:5432/db npm test

Among them is a block that tests the central claim without going through the text filter at all: it opens a READ ONLY transaction on a raw connection and hands Postgres a plain CREATE/INSERT/UPDATE/DELETE/TRUNCATE, asserting SQLSTATE 25006 each time. Those tests would still pass if the keyword layer had a hole — which is the point: they measure the engine, not the filter. A second block pins known injection payloads (a $-in-identifier fake dollar tag, an unterminated literal) as regressions.

The write attempts are refused by the engine before Postgres checks permissions, so a least-privilege read-only role — or a read-only replica — is still a perfectly safe target. CI (GitHub Actions) type-checks, builds, and tests on Node 20, 22, and 24 for every push and PR, with a postgres:16-alpine service container, so the integration suite runs in CI rather than skipping itself; a step fails the build if those tests ever report as skipped.

Built on the official @modelcontextprotocol/sdk (STDIO transport, protocol 2025-06-18) and pg. Written in strict TypeScript.


About

Built by Samuel Cabral — senior full-stack engineer (Node.js · TypeScript · NestJS · React · PostgreSQL). I build MCP servers and Claude Code / agent integrations, with a bias toward safety, tests, and tooling that a team can trust in production.

Available for MCP and Claude Code integration work.

Licensed under MIT.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    A read-only PostgreSQL MCP server that enables AI agents to perform schema introspection and execute SELECT-only queries. It supports secure database connections through SSL and SSH tunnels while offering a structure-only mode to restrict query access.
    10 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Read-only MCP server for PostgreSQL, enabling schema discovery, table metadata, and safe SELECT queries via READ ONLY transactions.
    25 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A hardened, read-only Postgres MCP server that enables LLMs to safely query databases without write, DDL, shell, or credential exposure.
    MIT