safe-postgres-mcp

safe-postgres-mcp

A zero-config, read-only PostgreSQL MCP server that enforces read-only access at the database level using READ ONLY transactions, allowing AI agents to safely explore schemas and run SELECT queries without risk of mutation.

Category
访问服务器

README

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 query runs inside a BEGIN TRANSACTION READ ONLY with a statement timeout and a row cap. 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?

Most "read-only" database tools enforce read-only by scanning the SQL text for scary keywords. That is a filter, and filters get bypassed. As of early 2026, the original TypeScript reference Postgres MCP server (@modelcontextprotocol/server-postgres) took exactly this text-filter approach and has since been archived in the modelcontextprotocol/servers repo, moved to the community servers-archived list 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
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.


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 tool is read-only; nothing here can mutate your data.

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 (no ANALYZE). Inspect cost/joins/index usage before paying for the query. sql (string, required) Plans only, never executes
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 — SELECT 1; DROP TABLE users Statement splitter (literal/comment/dollar-quote aware) rejects any input with more than one statement
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 124 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 10 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

They only issue read-only queries, so a read-only replica is a perfectly safe target. CI (GitHub Actions) type-checks, builds, and tests on Node 20, 22, and 24 for every push and PR.

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选