safe-postgres-mcp
Allows read-only querying and schema exploration of a PostgreSQL database, with enforced read-only transactions, statement timeout, and row limits.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@safe-postgres-mcplist all tables in the database"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
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 ( | No — it's fast-fail UX + a second line |
| 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 | Yes — for statement stacking |
| The engine rejects any write ( | Yes — this is the real guarantee |
Row cap + | 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-mcpThe order matters: keep another flag (
--transport stdio) between--env KEY=valueand the server name, otherwise the CLI parses the name as anotherKEY=valuepair. Everything after--is handed to the server untouched.
Verify it's connected:
claude mcp list
claude mcp get safe-postgresAdd --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.jsonWindows:
%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 buildclaude mcp add --env DATABASE_URL=postgres://user:pass@host:5432/dbname \
--transport stdio safe-postgres -- node /abs/path/to/safe-postgres-mcp/build/index.jsThe 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 |
| Run a single read-only SQL statement inside a |
| Executes SQL (read-only tx) |
| Return the query plan via |
| Plans the query; never executes it |
| List all non-system schemas with their owner. | — | Catalog read |
| List tables, views, and materialized views in a schema with approximate row counts (from planner stats — fast, no full scan) and on-disk size. |
| Catalog read |
| Full description of one table/view: columns (type, nullability, default), primary key, foreign keys, and indexes. |
| 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 — | Rejected by the |
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 |
Data-modifying CTE — | Dedicated hidden-write scan of CTE bodies, backstopped by the |
Comment / casing smuggling — | 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 — |
|
Oversized result set blowing up memory / agent context | The result is streamed through a server-side cursor that stops at |
Accidental persistence | Every transaction ends in |
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 ( |
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 |
| Yes | — | — | Postgres connection string. Point it at a least-privilege read-only role. |
| — | — | — | Fallback used only when |
| No |
|
| Per-statement timeout in ms. A slow query is cancelled, not run forever. |
| No |
|
| Hard cap on rows returned by |
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 ONLYguarantee enforced by the database, installed with onenpxcommand 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 runThe 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 testAmong 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.
GitHub: github.com/samuel-cabral
Email: samuelcabral.mail@gmail.com
Licensed under MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
- dataOAuthco.thinair
PostgreSQL, MySQL, and SQL Server in one session. 26 read-only MCP tools for AI agents.
Text-to-SQL MCP server: read-only queries on PostgreSQL, MySQL and SQL Server with your real schema
Hosted PostgreSQL MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA 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 npmMIT
- FlicenseNot gradedqualityFmaintenanceA read-only MCP server that enables AI agents to explore database schemas and execute safe queries on PostgreSQL and MySQL.-
- AlicenseNot gradedqualityDmaintenanceRead-only MCP server for PostgreSQL, enabling schema discovery, table metadata, and safe SELECT queries via READ ONLY transactions.7 npmMIT
- AlicenseNot gradedqualityCmaintenanceA hardened, read-only Postgres MCP server that enables LLMs to safely query databases without write, DDL, shell, or credential exposure.MIT