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.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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.
    163
    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.
    26
    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

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/samuel-cabral/safe-postgres-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server