PostgreSQL MCP Server

io.github.YawLabs由 GitHub 账号 YawLabs 发布v0.13.10更新于 Oct 31, 2026

PostgreSQL MCP server - query, schema introspection, explain, and health checks for AI assistants

概览

AI 生成的概览

本地 MCP 服务器,让助手查询 PostgreSQL 数据库、查看表结构、分析执行计划并做数据库健康检查。

可以做什么
  • 执行 SQL:在强制只读事务中运行只读语句,或运行带参数和行数上限的通用查询
  • 查看结构:列出 schema、表、视图、函数和扩展,描述表结构,并按名称搜索列
  • 分析性能:EXPLAIN 与 EXPLAIN ANALYZE 执行计划、用假设索引验证的索引建议、慢查询排行、全表扫描与未使用索引报告
  • 监控健康:连接与数据库统计、I/O 统计、锁等待、复制状态、表膨胀以及汇总的 DBA 检查
  • 管理会话与权限:列出角色、查看表权限,并按 PID 取消或终止后端连接
适用场景
适合助手需要直接浏览或查询 PostgreSQL 数据库、排查慢查询,或做锁、膨胀、索引等日常 DBA 巡检的场景。更适合开发环境和只读的生产访问,而不是执行结构迁移。
运行要求
通过 npx 以 stdio 在本地运行,需要 Node.js。必须提供目标数据库的 DATABASE_URL 连接串;可选变量可调整超时、行数上限、连接池大小、TLS 校验、审计日志和沙箱。部分诊断需要 pg_stat_statements 或 HypoPG 扩展,较新的 PostgreSQL 版本可返回更多字段。
安装前请注意
DATABASE_URL 含数据库凭据,应使用最小权限角色,因为角色才是真正的访问控制。只读模式不会阻止 pg_terminate_backend、pg_read_file 等函数,自动放行只读工具可能达到与 pg_kill 相同的权限。设置 ALLOW_WRITES=1 会允许 INSERT、UPDATE、DELETE 和 DDL,且 EXPLAIN ANALYZE 会真正执行语句。pg_kill 会改变会话状态。关闭 TLS 校验会削弱证书验证。

安装

在 SourceWeft 中

  1. 打开 控制台中的 PostgreSQL MCP Server,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

@yawlabs/postgres-mcp

[Add to Yaw MCP]

One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.

[npm version] [License: MIT]

Query a PostgreSQL database from Claude Code, Cursor, and any MCP client. Read-only by default - writes opt in via a single env var - so an agent can't silently drop your tables.

Built and maintained by Yaw Labs.

What's new in 0.12

An index advisor, opt-in audit logging, structured tool output, and support for current-revision MCP clients. Full detail in the CHANGELOG.

  • pg_index_advisor recommends indexes for a workload and keeps only the ones that measurably lower estimated cost. Candidates are costed with HypoPG hypothetical indexes, never created on disk, and the search knows that PostgreSQL 18's skip scan changes which multi-column indexes are useful.
  • Opt-in audit logging of the SQL tools send: one JSON line per audited statement, to stderr or a file. Bound parameter values are never recorded. Off by default, and not a complete record -- see Audit logging.
  • Structured tool output. Every tool declares an outputSchema and returns structuredContent alongside the unchanged text block, so anything reading the text today keeps working.
  • Both MCP protocol eras. The server used to speak only the legacy (2025) protocol revisions, which a client on the current 2026-07-28 revision fails against. It now serves both.

On 0.12.0? Upgrade. 0.12.1 closes a stacked-query hole in pg_index_advisor: SQL passed in its statements argument ran on a protocol that accepts several commands in one string, so SELECT 1; COMMIT; DROP SCHEMA public CASCADE; escaped the read-only transaction. The tool is annotated read-only, so hosts often auto-allow it. The same release stops pg_inspect_locks attributing a lock held in another database to whichever local table shares its OID, fixes the advisor's greedy search, and makes audit lines carry the tool field they were missing.

Coming from 0.10.x? 0.11.0 has three breaking changes: pg_seq_scan_tables, pg_unused_indexes and pg_top_queries return an envelope (read data.rows where you used to read data), pg_explain with analyze: true emits BUFFERS (pass buffers: false for the old output), and Node 22 is the floor. Details in the 0.11.0 changelog entry.

Backstory

Anthropic's reference Postgres MCP server, @modelcontextprotocol/server-postgres, was archived in May 2025 and marked deprecated on npm in July 2025. Anthropic has not shipped a replacement. Despite the deprecation, the last published version (v0.6.2) is still pulled ~20,000 times per week - a lot of agents are pointed at an unmaintained package.

That unmaintained package also has a known, publicly documented stacked-query SQL injection (Datadog Security Labs) that bypasses its BEGIN READ ONLY wrapper with input like COMMIT; DROP SCHEMA public CASCADE;. It has never been patched at npm.

A handful of community forks have appeared, but each fills a narrow slice:

  • @zeddotdev/postgres-context-server - Zed's fork, primarily a security patch on the original shape.
  • Postgres MCP Pro (Crystal DBA) - focused on index tuning and hypothetical-index / buffer-cache diagnostics.
  • AWS Labs Postgres MCP - tied to Aurora / RDS Data API + Secrets Manager.

None of them position themselves as a general-purpose daily driver you'd hand to Claude Code or Cursor against an arbitrary Postgres: modern introspection, perf helpers, role/privilege awareness, and a write-safety posture out of the box. That's the gap @yawlabs/postgres-mcp fills.

Why this one?

  • Read-only by default, with an unconditional read-only tool too - pg_query runs user SQL in a BEGIN READ ONLY transaction, so postgres itself (not string parsing) blocks writes; opt in to writes with ALLOW_WRITES=1. pg_readonly is a separate tool that stays read-only regardless of ALLOW_WRITES, so hosts that gate tools individually (Claude Code permissions, mcp.hosting) can auto-allow it -- paired with a least-privileged role, since READ ONLY bounds writes to the database rather than every side effect (details).
  • Role-based access as the primary control - the recommended posture is to use a least-privileged postgres role in DATABASE_URL (e.g. one with GRANT pg_read_all_data); postgres itself then enforces the boundary, no env var needed. See Configuring access.
  • Extended query protocol for all user SQL - pg_query sends user input with queryMode: 'extended', which restricts each request to a single statement. This closes the stacked-query injection class (COMMIT; DROP SCHEMA x CASCADE;) that defeated the reference server's BEGIN READ ONLY wrapper. Integration test asserts the rejection.
  • Parameterized queries - pg_query takes a params array for $1, $2, etc. No string-interpolated SQL in our code path.
  • Written from scratch, actively maintained - not a fork of the deprecated code. Unit + integration tests (npm test, npm run test:integration) run against a real Postgres; releases cut via release.sh.
  • Schema introspection built in - pg_list_schemas, pg_list_tables, pg_describe_table return columns, primary keys, foreign keys, and indexes without the agent having to remember pg_catalog joins.
  • EXPLAIN as a first-class tool - text or JSON format, with optional ANALYZE. ANALYZE for non-SELECT statements requires ALLOW_WRITES=1 and always rolls back, so the plan is real but the written rows don't persist. (What Postgres never rolls back still sticks: a sequence the statement advanced stays advanced.)
  • Perf diagnostics the deprecated server never had - pg_top_queries (from pg_stat_statements), pg_seq_scan_tables, pg_unused_indexes, pg_table_bloat, pg_inspect_locks, pg_replication_status. Answer "why is this slow?" in one tool call.
  • Health snapshot - pg_health returns version, db size, connection counts, and the 10 longest-running active queries in one call.
  • Role and privilege awareness - pg_list_roles and pg_table_privileges for the common "who can touch what?" questions.
  • Instant startup - ships as a single bundled file with zero runtime dependencies. No multi-minute node_modules install on every npx cold start.
  • Result truncation - large result sets are capped at POSTGRES_MAX_ROWS (default 1000) with a truncated: true flag, so a stray SELECT * FROM events doesn't blow out the model context.

Quick start

1. Create .mcp.json in your project root

macOS / Linux / WSL:

json
{  "mcpServers": {    "postgres": {      "command": "npx",      "args": ["-y", "@yawlabs/postgres-mcp@latest"],      "env": {        "DATABASE_URL": "postgres://user:password@host:5432/dbname"      }    }  }}

Windows:

json
{  "mcpServers": {    "postgres": {      "command": "cmd",      "args": ["/c", "npx", "-y", "@yawlabs/postgres-mcp@latest"],      "env": {        "DATABASE_URL": "postgres://user:password@host:5432/dbname"      }    }  }}

Why the extra step on Windows? Since Node 20, child_process.spawn cannot directly execute .cmd files (that's what npx is on Windows). Wrapping with cmd /c is the standard workaround.

2. Restart and approve

Restart Claude Code (or your MCP client) and approve the postgres MCP server when prompted.

3. (Optional) Enable writes

Read-only is the default. If you want the agent to be able to INSERT, UPDATE, DELETE, or run DDL, add ALLOW_WRITES=1 to the env block:

json
"env": {  "DATABASE_URL": "postgres://...",  "ALLOW_WRITES": "1"}

Prefer scoping this to dev/test databases - for production, leave writes off and use migration tools out-of-band.

Configuring access

The role in DATABASE_URL is the primary access control. Postgres has had a battle-tested permission system for 30 years; lean on it instead of relying on ALLOW_WRITES alone. A least-privileged role makes writes server-rejected no matter what tools or env vars are configured.

Read-only agent (recommended default):

sql
CREATE ROLE mcp_reader LOGIN PASSWORD 'change-me';GRANT CONNECT ON DATABASE your_db TO mcp_reader;GRANT USAGE ON SCHEMA public TO mcp_reader;GRANT pg_read_all_data TO mcp_reader;

Point DATABASE_URL at mcp_reader. Postgres rejects every write, every DDL, every privilege change - regardless of ALLOW_WRITES. No app-level guard to bypass; the database is the boundary.

Scoped write agent (dev/test or narrow production use):

sql
CREATE ROLE mcp_writer LOGIN PASSWORD 'change-me';GRANT CONNECT ON DATABASE your_db TO mcp_writer;GRANT USAGE ON SCHEMA public TO mcp_writer;GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mcp_writer;GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO mcp_writer;-- DDL not granted -- the agent can change data but not schema.

Set ALLOW_WRITES=1 so pg_query will issue writes, and rely on the role to keep the agent away from DDL and other schemas.

Per-tool gating in the host:

Tools split cleanly across two authority classes:

  • Auto-allow: pg_readonly (server-side BEGIN READ ONLY, unconditional), pg_index_advisor (only ever EXPLAINs the statements it is given, never with ANALYZE, inside BEGIN READ ONLY), plus the introspection and diagnostics tools (pg_list_*, pg_describe_table, pg_search_columns, pg_health, pg_inspect_locks, pg_table_bloat, pg_unused_indexes, pg_seq_scan_tables, pg_top_queries, pg_io_stats, pg_replication_status, pg_advisor, pg_table_privileges).
  • Always prompt: pg_query (writes when ALLOW_WRITES=1 is set and the role allows it), pg_explain (analyze: true executes the statement; with ALLOW_WRITES=1 that includes writes, which are rolled back, and without it the tool has the same reach as pg_readonly), pg_kill (changes session state; requires ALLOW_WRITES=1).

The split follows each tool's readOnlyHint annotation, and Claude Code's permissions block and mcp.hosting's per-tool toggle both honor it.

What READ ONLY does and does not cover. A BEGIN READ ONLY transaction blocks writes to the database -- INSERT/UPDATE/DELETE, DDL, nextval/setval. It does not block functions whose effect lands outside the table data. SELECT pg_terminate_backend(...), pg_cancel_backend, pg_read_file, lo_export, and COPY ... TO PROGRAM all run to completion inside pg_readonly, which means auto-allowing pg_readonly reaches the same capability that pg_kill puts behind ALLOW_WRITES=1. Every one of them still requires a privilege the DATABASE_URL role must actually hold (pg_signal_backend, pg_read_server_files, superuser), so the role is the control that bounds this tool, not the transaction mode. If you auto-allow pg_readonly, use a least-privileged role -- see Configuring access.

ALLOW_WRITES as defense-in-depth:

ALLOW_WRITES is a secondary belt-and-braces gate. Useful when:

  • You're on a managed database where creating a second role is awkward (some Supabase/Neon plans).
  • You want a single role that can write, but want the MCP server to refuse writes anyway during normal operation.

Otherwise, configure the role and stop relying on ALLOW_WRITES.

What can an agent do with this?

Once connected, the agent picks tools automatically based on what you ask. A few single-tool examples:

  • "Describe the users table" -> pg_describe_table -> returns kind, columns, PK, FKs, indexes.
  • "Which tables have a user_id column?" -> pg_search_columns with pattern user_id -> one call instead of iterating every table.
  • "This query is slow, why?" -> pg_explain with analyze: true -> returns the plan with actual row counts and timing.
  • "What's the slowest query we run?" -> pg_top_queries -> returns the top N from pg_stat_statements with mean/total/min/max times.
  • "Do we have any unused indexes?" -> pg_unused_indexes -> returns non-unique, non-primary indexes with zero or low scan counts + their size.
  • "Is pgvector installed?" -> pg_list_extensions -> yes/no with version.

The bigger leverage is multi-tool reasoning. A few real workflows:

  • Unstick a hung app. pg_inspect_locks returns blocked PID + blocking PID + the offending query, then pg_kill (ALLOW_WRITES=1 required) cancels the blocker. The agent can run both in one turn - it's the fastest path from "the app is frozen" to "back up."
  • Chase a slow page. pg_top_queries ranks the worst queries, pg_explain with analyze: true shows the plan for the top hit, pg_seq_scan_tables and pg_unused_indexes say whether the answer is "add an index here" or "drop a dead one there."
  • Oncall triage. pg_health checks connectivity + active-query count + database size; pg_inspect_locks and pg_replication_status confirm whether contention or replication lag is in play before paging the on-call DBA.

Tools

ToolDescription
pg_readonlyRun a SQL statement with no persistent data changes - always inside BEGIN READ ONLY, regardless of ALLOW_WRITES. The recommended tool for read access, and the one to auto-allow for ad-hoc SQL; pair it with a least-privileged role (why).
pg_queryRun a SQL query. Writes gated by the role in DATABASE_URL first, ALLOW_WRITES second. Supports parameterized queries via params. Result fields include dataTypeName (e.g. int4, jsonb) alongside dataTypeID.
pg_list_schemasList non-system schemas.
pg_list_tablesList tables (and optionally views) in a schema with estimated row counts. Paginated via limit/offset.
pg_describe_tableKind, columns, PK, outgoing FKs, incoming FKs (referenced_by), CHECK / UNIQUE / EXCLUDE constraints, indexes, and partition parent/children for a relation. Generated and identity columns are flagged (generated, identity, generation_expression) so an agent doesn't try to write to them. Constraints carry validated, plus enforced / has_period on PG18+.
pg_list_viewsList views and materialized views in a schema, including their SQL definitions.
pg_list_functionsList functions, procedures, and aggregates in a schema with signatures and return types.
pg_list_extensionsList installed extensions (pgvector, postgis, pg_stat_statements, etc.) with versions.
pg_search_columnsFind columns by name pattern across all user schemas. Case-insensitive, supports SQL LIKE wildcards.
pg_explainEXPLAIN or EXPLAIN ANALYZE for a SQL statement. Text or JSON output. Planner options: buffers (on by default with analyze), settings, verbose, wal, costs, timing, plus generic_plan (PG16+, plan a parameterized query with no values) and memory / serialize (PG17+). Optional hypothetical_indexes (requires the HypoPG extension) lets you ask "what would the plan be with these indexes?" without creating them on disk.
pg_index_advisorRecommend indexes for a workload and prove each one pays for itself first. Takes statements you pass or the top N from pg_stat_statements, harvests candidate columns from what the planner reports as filters / join keys / sort keys (no SQL parser - every token is intersected with the real pg_attribute column list), then costs each candidate with HypoPG hypothetical indexes and keeps only what measurably lowers estimated cost. Greedy and bounded via max_candidates / max_explains, so a big workload cannot run away; budget_exhausted flags a truncated search. Returns the CREATE INDEX (plus a CONCURRENTLY form), cost before/after, which statements each index helps, and the estimated size. PG18-aware: PG18 added B-tree skip scan, so a multi-column index whose leading column is never filtered is no longer useless - that classic prune is gated on the server version rather than applied blindly. Requires HypoPG; indexes are session-scoped and reset on every exit path.
pg_healthServer version, database size, connections against max_connections, active queries with wait events and transaction age, pg_stat_database rollup (deadlocks, temp files, cache hit ratio), table count.
pg_top_queriesTop N queries by total/mean execution time. Requires the pg_stat_statements extension. Returns stats_reset (from pg_stat_statements_info, a different clock from the other stats tools) and dealloc on extension 1.9+ - a non-zero dealloc means entries were evicted past pg_stat_statements.max, so the ranking is drawn from an incomplete population.
pg_seq_scan_tablesTables with heavy sequential scans - missing-index candidates. Returns the stats_reset window alongside the rows, since the counters mean nothing without it. last_seq_scan / last_idx_scan on PG16+.
pg_unused_indexesNon-unique, non-primary indexes with low scan counts - drop candidates. Also returns stats_reset: a recently reset counter makes every index look unused, which is how a load-bearing index gets dropped. last_idx_scan on PG16+.
pg_io_statsI/O observability: pg_stat_io read/write/extend/fsync counts, bytes and times per backend type and context (PG16+), plus in-flight async I/O handles from pg_aios and the active io_method (PG18+).
pg_inspect_locksWho is blocking whom right now (blocked PID, blocker PID, lock type, queries).
pg_list_rolesDatabase roles with login/superuser/createdb flags and group memberships.
pg_table_privilegesWho has SELECT/INSERT/UPDATE/DELETE/etc. on a table or whole schema.
pg_table_bloatTables with high dead-tuple ratios - VACUUM candidates.
pg_replication_statusReplication slots, connected replicas, and current WAL position.
pg_advisorRolled-up DBA lints in one call: sequence-exhaustion candidates, wraparound risk for both counters (per-database and per-table age(relfrozenxid) against autovacuum_freeze_max_age, and mxid_age(relminmxid) against autovacuum_multixact_freeze_max_age -- a lock-heavy workload can exhaust multixacts while xids look healthy; triggered_by says which), tables without a primary key, and (configurable) public tables with RLS disabled. The "what should I be looking at?" starting point.
pg_killCancel a running query or terminate a backend connection. Requires ALLOW_WRITES=1.

Configuration

All env vars are read from the MCP server's environment:

VariableDefaultPurpose
DATABASE_URL(required)PostgreSQL connection string.
ALLOW_WRITESunsetSecondary write gate for pg_query, pg_explain ANALYZE-of-writes, and pg_kill. Set to 1 or true (case-insensitive) to lift the BEGIN READ ONLY wrapper on the first two and let pg_kill run; any other value stays read-only, with a warning on stderr. The role in DATABASE_URL is the primary control - see Configuring access. Does not affect pg_readonly, which is unconditional.
POSTGRES_STATEMENT_TIMEOUT_MS30000Per-statement timeout.
POSTGRES_CONNECTION_TIMEOUT_MS10000TCP connect timeout. Without this, a dead host hangs until the OS gives up (~2 minutes).
POSTGRES_MAX_ROWS1000Cap on rows returned by pg_query.
POSTGRES_POOL_MAX5Max pool connections. Set to 1 for single-threaded backends (pglite-socket, PgBouncer transaction mode).
POSTGRES_SSL_REJECT_UNAUTHORIZEDunsetSet to false (or 0; case-insensitive) to skip TLS cert verification (for managed DBs using private-CA certs). Connection is still encrypted.
POSTGRES_APPLICATION_NAMEpostgres-mcpValue reported in pg_stat_activity.application_name, so agent traffic is identifiable to whoever is watching the database. An application_name in DATABASE_URL takes precedence over this.
POSTGRES_AUDIT_LOGunset (off)1, true or stderr turns on the audit log, one JSON line per audited statement, written to stderr unless POSTGRES_AUDIT_LOG_FILE is also set. 0, false or off is the explicit off. Case-insensitive. Any other value stops the server at startup, including an empty or whitespace-only one. See Audit logging.
POSTGRES_AUDIT_LOG_FILEunsetAppend the audit lines to this file instead of stderr. Setting it alone turns auditing on. The server refuses to start if the value is empty, if the file cannot be opened, or if POSTGRES_AUDIT_LOG is explicitly off.
POSTGRES_AUDIT_REDACTunset (off)1 or true logs each statement's first keyword plus a SHA-256 of its text instead of the SQL. 0, false or off is the explicit off: full SQL is logged. Does not turn auditing on by itself. Any other value stops the server at startup, including an empty or whitespace-only one, even with auditing off.
POSTGRES_MCP_RUNTIMEautoWhich JS runtime executes the server: auto (the newest oam at 0.19.1 or newer, else Node), oam (the same, but exit with an error instead of falling back to Node), node (always Node -- launched with oam run, it hands off to Node on PATH; refused when POSTGRES_MCP_SANDBOX is on). Case-insensitive; any other value behaves like auto. See Runtime.
OAM_BINunsetPath to an oam binary to use in preference to discovery, when it is 0.19.1 or newer. If it does not exist, is older, or will not run, the launcher says so on stderr and carries on with discovery.
POSTGRES_MCP_SANDBOXunset (off)1 or true runs the server under oam's --permission sandbox: filesystem and child processes denied, and network access limited to the one host and port the pg driver connects to -- from DATABASE_URL (its host= and port= parameters win), else PGHOST/PGPORT, else localhost:5432, the driver's own order. 0, false or off is the explicit off. Case-insensitive. Any other value stops the launcher at startup, including an empty or whitespace-only one: that is usually an env entry such as "POSTGRES_MCP_SANDBOX": "${SANDBOX}" whose variable is unset where your MCP client runs, and reading it as unset would run the server without the sandbox. Only a variable that is not set at all counts as unset. The sandbox never turns itself off quietly: when it cannot be applied -- POSTGRES_MCP_RUNTIME=node, no usable oam 0.19.1 or newer, an oam that fails to launch, or a database endpoint it cannot pin to one TCP host and port (a Unix socket, several hosts, a port outside 1-65535, a host that is not a plain DNS name or IP address, a DATABASE_URL that is not a readable postgres:// URL) -- the launcher exits with a message naming the fix. With DATABASE_URL unset the server gets no network access at all and its tools report the missing variable. Per oam's changelog, --permission did not cover all of fs and child_process until 0.9.1, and the port grant was not exact until 0.15.0. oam does not gate DNS lookups. A --permission or --allow-* flag in an inherited NODE_OPTIONS is removed from the sandboxed oam's environment, and that removal is the only guard: from oam 0.19.1 a list flag there adds its entries to the ones the launcher passes on the command line instead of yielding to them, so one left in place would widen the grant. The file audit sink cannot open under the sandbox; use the stderr sink. See Troubleshooting.

Supported Postgres versions

Tested on PostgreSQL 15, 17 and 18 in the integration matrix.

Works on PG13+, but note where upstream support actually sits: PG13 reached end of life on 2025-11-13 and PG14 does so on 2026-11-12. PG13/14 are not exercised here and are not a compatibility target going forward. PG12 and below are further out of support and some tools rely on columns that landed in PG13 (pg_replication_status reading wal_status, pg_top_queries reading *_exec_time).

Newer server versions unlock extra fields rather than being required. Every version-dependent column is gated on server_version_num and simply omitted on servers that predate it, so nothing errors -- you get a slightly thinner answer. The cut points that matter:

ServerWhat it adds
PG16+last_idx_scan / last_seq_scan in the stats tools (index/table staleness rather than a bare counter), pg_explain generic_plan
PG17+pg_explain memory and serialize
PG18+Generated-column form (stored vs virtual), NOT NULL constraint validity, conenforced / conperiod constraint metadata in pg_describe_table, relallfrozen freeze coverage in pg_advisor. BUFFERS is on by default with EXPLAIN ANALYZE server-side

If the version probe fails, the server assumes the oldest supported shape rather than emitting SQL a server might reject.

Runtime

The published postgres-mcp command is a small launcher that prefers the newest oam runtime it can find and falls back to Node.

If you do not have oam, nothing changes -- unless POSTGRES_MCP_SANDBOX is on, which needs oam and stops the launcher with a message when there is none. The fallback is not a re-exec: npm already started Node to run the launcher, so falling back is a plain import() of the server into that same process. It costs a few existsSync calls and no subprocess, and behaves identically to running dist/index.js under Node directly.

Which oam. Only oam 0.19.1 or newer -- the latest release -- is used. The launcher looks in the installed locations ($OAM_INSTALL_DIR when it is set, then %LOCALAPPDATA%\oam\bin and ~/.oam/bin on Windows, ~/.oam/bin elsewhere) and on PATH, asks every oam it finds for its version, and runs the newest; on a tie the installed copy wins. An older oam is passed over. On Windows only oam.exe counts; an oam.cmd / oam.bat shim is never run. The passed-over binaries and any shim are named on stderr only when no usable oam is found -- when a newer oam runs, nothing is printed about them. The fix printed then fits what was found: oam self-update for an oam that is too old, a check of the binary for one that would not run, and an install from oamjs.org only when there was no oam at all. An OAM_BIN that cannot be used is always named whenever the launcher looks for an oam, even when a newer one then runs. When a host launches the command with oam run on oam 0.19.1 or newer, the server runs inside that oam with no second one -- except when POSTGRES_MCP_SANDBOX is on, which needs a freshly launched oam and exits with an error when none can be launched. A host oam older than 0.19.1 never serves the server itself: it hands off to the newest usable oam, else (without the sandbox) to Node on PATH, else exits with an error.

If you do have a usable oam, the server runs under it. Verified equivalent on both runtimes: all 23 tools register, queries return identical rows and dataTypeName values, and the error paths match. oam supplies every node: builtin the driver needs, including net, tls, crypto, and dns (SCRAM auth and the extended query protocol both work).

Startup cost, measured. windows-arm64, 1.4 MB bundle, postgres-mcp version (full module init), every binary warmed first, mean of 12 runs:

pathstartup
standalone binary (oam compile)298ms
oam run dist/index.js306ms
node dist/index.js358ms
launcher -> Node (in-process)370ms
launcher -> oam (spawn)409ms

oam starts faster than Node here. What the launcher costs is the spawn: reaching oam means Node has already booted, and that hop (~100ms) is larger than oam's ~52ms advantage. So through the npm bin, the two land within ~40ms of each other, and POSTGRES_MCP_RUNTIME=node is a marginal win rather than a meaningful one. Those figures were taken with one oam on the machine: the launcher now runs --version on every oam binary it finds, so each extra copy adds a probe.

Either way it is a one-time cost per MCP session, not per tool call -- hosts spawn the server once and hold it open. If startup genuinely matters, the standalone binary avoids the launcher entirely and is the fastest option.

Earlier releases of this README reported ~650-900ms for Node and ~980-1290ms for oam, and advised opting out of oam on that basis. Those figures were measured against cold, freshly-built binaries and reflected the Windows on-access virus scanner rather than either runtime. They were wrong in both magnitude and direction. Corrected in 0.9.1.

jsonc
{  "mcpServers": {    "postgres": {      "command": "npx",      "args": ["-y", "@yawlabs/postgres-mcp"],      "env": {        "DATABASE_URL": "postgres://...",        "POSTGRES_MCP_RUNTIME": "node"  // opt out of oam      }    }  }}

Connecting to managed Postgres (Supabase, Neon, RDS, etc.)

Most managed databases require TLS but serve certs signed by a private CA that Node's default trust store doesn't recognize. The symptom is one of:

  • self signed certificate in certificate chain
  • unable to get local issuer certificate
  • unable to verify the first certificate

To allow the connection while keeping traffic encrypted, add POSTGRES_SSL_REJECT_UNAUTHORIZED=false to the env block:

json
"env": {  "DATABASE_URL": "postgres://user:pass@host:5432/db?sslmode=require",  "POSTGRES_SSL_REJECT_UNAUTHORIZED": "false"}

This disables certificate chain verification only -- the TCP connection is still TLS-encrypted end-to-end. For production setups where you can install the CA, prefer putting the cert in the Node trust store (NODE_EXTRA_CA_CERTS) over disabling verification globally.

Shaving a round trip on PG17+. Postgres 17 added direct TLS negotiation, which skips the plaintext SSLRequest handshake before the TLS one. The bundled driver supports it, so append sslnegotiation=direct to your DATABASE_URL:

postgres://user:pass@host:5432/db?sslmode=require&sslnegotiation=direct

It is opt-in rather than a default because a PG16-or-older server will reject the connection outright, and the saving is one round trip per pooled connection -- worth it on a distant managed database, invisible on a local one.

Audit logging

Off by default. Turn it on in the env block with POSTGRES_AUDIT_LOG=stderr, or with POSTGRES_AUDIT_LOG_FILE alone:

json
"env": {  "DATABASE_URL": "postgres://...",  "POSTGRES_AUDIT_LOG_FILE": "/var/log/postgres-mcp/audit.jsonl"}

The server then writes one JSON line per audited statement. Two captured from PostgreSQL 17 -- a parameterized query, then a failing one:

json
{"ts":"2026-09-12T19:26:42.587Z","tool":"pg_query","source":"user","sql":"SELECT $1::int AS n","params":1,"ms":2.827,"rows":1,"ok":true}{"ts":"2026-09-12T19:26:42.591Z","tool":"pg_readonly","source":"user","sql":"SELECT * FROM does_not_exist_xyz","params":0,"ms":2.217,"rows":null,"ok":false,"sqlstate":"42P01"}
FieldMeaning
tsISO 8601 time the line was written, just after the statement finished.
toolThe MCP tool that issued the statement. Absent only when the server's query functions are called outside a tool handler, e.g. when embedding it in-process.
sourceuser for SQL passed to pg_query, pg_readonly or pg_explain. internal for everything else: the server's own catalog queries (one pg_describe_table call writes several), pg_kill's signal call (the server writes that statement; the agent supplies only the pid), and every statement pg_index_advisor runs -- including workload SQL passed in its statements. Filtering on user does not show all agent-supplied SQL, and it does not show pg_kill calls: filter on tool for those. A kill sent as SQL (SELECT pg_terminate_backend(...) through a tool that takes SQL) is an ordinary user line under that tool's name.
sqlThe statement text as sent. For pg_explain that is the composed statement: EXPLAIN SELECT ..., or EXPLAIN (ANALYZE, BUFFERS) SELECT ... with options.
sqlKeyword, sqlSha256Replace sql when POSTGRES_AUDIT_REDACT is on: the first run of letters after any leading whitespace or (, uppercased (UNKNOWN when anything else comes first, such as a comment), and a hex SHA-256 of the full text. The keyword is the first word, not the effect: a CTE that deletes logs WITH, and every pg_explain line logs EXPLAIN.
paramsHow many bound parameters were passed. The count, never the values.
msWall-clock milliseconds for the audited step, not server execution time. A user line runs from the connection checkout to the statement's completion, so it includes waiting for a pooled connection, the BEGIN, pg_explain's hypothetical-index setup, and the row-cap cursor's round trips; internal lines can include waiting for a pool connection or behind the same call's other catalog queries.
rowsnull on failure. Otherwise the count postgres reported, or the rows returned when it reports none (SET logs 0). A capped SELECT reports at most POSTGRES_MAX_ROWS + 1, the extra row being how truncation is detected; statements that cannot use the cursor (EXPLAIN, SHOW, ... RETURNING) report their full count even when the response is truncated.
okWhether the statement ran and succeeded. false also covers a statement that was never sent because the call failed first -- the connection was refused, BEGIN failed, or pg_explain could not create a hypothetical index -- and the line then carries that failure's code; PostgreSQL's own log has no record of such a statement. For agent SQL true does not mean it committed: the COMMIT that follows is not logged, so a write whose commit fails still reads true.
sqlstateOnly when ok is false and the error carries a code. Usually a five-character SQLSTATE such as 42P01, but a failed connection records a Node error code instead (ECONNREFUSED, ENOTFOUND); a connect timeout records none, and neither does a missing DATABASE_URL.

Never logged, in any mode: bound parameter values (only their count) and error messages (only the code), since postgres quotes offending values back in its messages. Statement text is logged as written, so pass sensitive literals as params to pg_query, pg_readonly or pg_explain. pg_index_advisor takes no params; write $1-style placeholders in its statements instead (planned with GENERIC_PLAN, PostgreSQL 16+).

Values are parsed strictly. A value outside the accepted sets in the Configuration table -- yes, on, hash -- stops the server at startup instead of reading as off, even for POSTGRES_AUDIT_REDACT with auditing off. An audit control that quietly disabled itself would be worse than none. Case and surrounding whitespace are ignored. A variable that is set but empty, or only whitespace, stops the server too: that is usually an env entry such as "POSTGRES_AUDIT_REDACT": "${AUDIT_REDACT}" whose variable is unset where your MCP client runs (on Windows, variables from a bash or WSL profile do not reach a server launched via cmd), and reading it as unset would log full SQL. Only a variable that is not set at all counts as unset, so to leave either flag off, remove it from the env block or set it to 0.

The file sink. POSTGRES_AUDIT_LOG_FILE alone turns auditing on, and it wins over POSTGRES_AUDIT_LOG=stderr without a warning when both are set. As with the other two variables, an empty or whitespace-only value is an error, not unset. The path must name a file in an existing directory: the file is created if missing, opened for append at startup, and held open for the life of the process. Use an absolute path; a relative one resolves against whatever directory your MCP client launches the server from. A write that fails later (full disk) never fails the query: one warning goes to stderr and later lines keep trying.

On stderr, audit lines are mixed with the startup banner and warnings, so skip lines that are not JSON; the file sink holds only audit lines. stdout is never used -- it is the MCP protocol channel.

Redaction hides text, not guesses. Identical statements hash identically, so you can still count and correlate them -- and for the same reason, anyone holding the log can confirm a guessed statement, so a short literal inside a predictable query is recoverable. Bound parameters are the safe place for sensitive values.

Under POSTGRES_MCP_SANDBOX the sandbox denies the filesystem, so the file sink cannot open and the server exits at startup (could not be opened for append: Access to this API has been restricted). Use POSTGRES_AUDIT_LOG=stderr there; all three audit variables pass through the sandbox.

What the trail does not show.

  • Which backend pg_kill signalled. Each signal writes one line -- tool: "pg_kill", source: "internal", params: 1, and sql either SELECT pg_cancel_backend($1) AS signaled or SELECT pg_terminate_backend($1) AS signaled -- but the pid is a bound parameter, so only its count is logged (PostgreSQL's own log can record the value; see the end of this section). If the target was one of this server's own statements and the signal took effect, that statement writes its own ok: false line (normally sqlstate 57014 for a cancel and 57P01 for a terminate), and ts is all that pairs the two. Measured against local servers over loopback TCP, the target's line usually carried the same millisecond as the pg_kill line or the next, and terminating a CPU-bound target took up to a few hundred milliseconds; a target that is slow to reach an interrupt check can take longer still. The target's line is normally the later of the two, occasionally the earlier. Treat closeness as evidence, not proof: a statement timeout writes 57014 too. Under POSTGRES_AUDIT_REDACT both modes read sqlKeyword SELECT, so tell a cancel from a terminate by hashing the two texts above -- the exact text with no trailing newline, single-quoted in a shell so that $1 survives.
  • Whether a pg_kill signal took effect. ok describes pg_kill's own statement, and the signaled value it returned is not logged. ok: true covers a signal that landed, a pid that is not a backend (postgres answers false), and a cancel that changed nothing: postgres answers true once the signal is sent, not once it has taken effect, and a session that is idle in a transaction, for one, ignores it. ok: false with 42501 is a refusal: the role may not signal that backend. ok: false with 57014 or 57P01 on the pg_kill line itself means pg_kill's own statement was cancelled or terminated, as happens when the pid names the connection pg_kill is running on; after such a terminate that backend is gone, even though the line reads ok: false. A call refused for want of ALLOW_WRITES=1 sends nothing and writes nothing.
  • The scaffolding around agent SQL (BEGIN READ ONLY, the row-cap savepoint and cursor, the closing COMMIT / ROLLBACK), the pg_type lookup behind dataTypeName, and the hypopg_create_index / hypopg_reset calls behind pg_explain's hypothetical_indexes. None of these gets a line of its own. A BEGIN, a savepoint or a hypopg_create_index that fails does show, on the statement's own ok: false line (see ok above), because the statement is never sent; a pg_type lookup that fails does not, because the statement already ran and the lookup only decorates its result. pg_index_advisor is the exception: everything it sends is logged, including its transaction control and the EXPLAIN of each workload statement. The server-version probe some tools run first, SELECT current_setting('server_version_num') AS v, is an ordinary internal line: once per successful reading, which is then cached for the life of the process (calls that race the first reading share it), and on every probing call while the server is unreachable, since a failure is not cached.
  • The statement a call never got as far as composing. Every tool records a refused connection as the ok: false line of the first statement it was about to send, under its own tool, with sqlstate holding the Node error code or absent for a connect timeout: the agent's SQL for pg_query and pg_readonly; for pg_explain the EXPLAIN itself, unless a version-floored option (settings, wal, generic_plan, memory, serialize, buffers without analyze) or hypothetical_indexes was passed, when the line is the version probe's or the HypoPG check's and the EXPLAIN, composed only after them, is not on it; the signal statement for pg_kill; and the version probe or the first catalog query for the rest, including the tools that share one connection across several queries (pg_describe_table, pg_health, pg_advisor, pg_replication_status, pg_seq_scan_tables, pg_unused_indexes, pg_io_stats), whose first query is the one that checks the connection out.

PostgreSQL's own logging is the complete server-side record: log_statement = 'all' records every statement that reaches execution, and log_min_error_statement, at its default error, adds statements that fail earlier. It also records exactly what this trail keeps out -- bound parameter values and full error messages.

Troubleshooting

DATABASE_URL is not set - Your MCP client is launching the server without the env var. On Windows especially, env vars set in bash / PowerShell profiles are not inherited by MCP servers launched via cmd. Put DATABASE_URL directly in the env block of .mcp.json.

password authentication failed - Check the username, password, and that the user has CONNECT privilege on the database. URL-encode special characters in the password (@ → %40, # → %23, / → %2F).

SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string - The password in your connection string is empty or became null after URL decoding. Re-check your connection string.

canceling statement due to statement timeout - A single query exceeded POSTGRES_STATEMENT_TIMEOUT_MS (default 30s). Increase it, narrow the query with WHERE, or add an index. This is working as designed -- the timeout exists so a runaway query cannot hang the agent.

Write blocked: this server is in read-only mode - You asked the agent to write via pg_query but ALLOW_WRITES is not set. Either add ALLOW_WRITES=1 to the env block of .mcp.json and restart your MCP client (dev/test DBs), or - cleaner for production - use a role with INSERT/UPDATE/DELETE grants in DATABASE_URL and keep ALLOW_WRITES unset. See Configuring access. Note that pg_readonly always rejects writes; if you want writes, the call has to go through pg_query.

Error: [postgres-mcp] POSTGRES_AUDIT_... or Error: [postgres-mcp] audit log file ... could not be opened for append at startup - An audit variable is misconfigured, and the server exits with a stack trace rather than guess. The Error: line says which case you hit: an unrecognized value (it lists the accepted ones), a variable that is set but empty (usually an unexpanded variable in the MCP config -- remove it from the env block, or give it a real value: 0 or 1 for the two flags, a path for the file), a file set while POSTGRES_AUDIT_LOG is off, or the OS error from opening the file -- ENOENT usually means its directory does not exist, and Access to this API has been restricted means POSTGRES_MCP_SANDBOX=1. See Audit logging.

postgres-mcp: POSTGRES_MCP_SANDBOX=... refusing to start without the sandbox - The sandbox was requested but cannot be applied, so the launcher stops rather than run the server without it. The first line names the case and the last line the fix: POSTGRES_MCP_RUNTIME=node (Node has no --permission); no usable oam 0.19.1 or newer (the indented lines list what was found and why each was passed over); an oam that failed to launch; or a database endpoint that cannot be pinned -- a Unix socket (oam can dial one, but the sandbox only grants a TCP host and port; connect over TCP, or unset POSTGRES_MCP_SANDBOX), several hosts, a port outside 1-65535, a host with whitespace or non-ASCII characters (use its xn-- form), or a DATABASE_URL that is not a readable postgres:// URL. DATABASE_URL itself is never printed. postgres-mcp: POSTGRES_MCP_SANDBOX="..." is not a recognized value means the value is wrong: 1 or true turns the sandbox on, 0, false or off turns it off.

Connection pool exhaustion with PgBouncer transaction mode or pglite-socket - These backends don't support concurrent queries on a single connection. Set POSTGRES_POOL_MAX=1 in the env block.

First query is slow, subsequent queries are fast - Expected. The pg driver lazily establishes the first connection; subsequent queries reuse the pool.

Development

Run the full suite (unit + integration) against a real Postgres:

bash
DATABASE_URL='postgres://user:pass@host:5432/db' POSTGRES_MCP_INTEGRATION=1 npm run test:integration

The integration suite assumes a disposable database -- it creates and drops a test_fixture schema. Don't point it at anything you care about.

To run the POSTGRES_MCP_SANDBOX path against a real oam as well -- a handshake and select 1 through the launcher under --permission, then a dial to the port next to the pinned one, which must be refused with ERR_ACCESS_DENIED -- set OAM_BIN beside DATABASE_URL. That file is skipped unless both are set, and it never writes:

bash
OAM_BIN=/path/to/oam DATABASE_URL='postgres://user:pass@host:5432/db' npm run test:integration

To also run the destructive tests (REVOKE / restricted-role path), add POSTGRES_MCP_DESTRUCTIVE_TESTS=1. Only safe on a disposable cluster:

bash
DATABASE_URL='postgres://user:pass@host:5432/db' POSTGRES_MCP_INTEGRATION=1 POSTGRES_MCP_DESTRUCTIVE_TESTS=1 npm run test:integration

Windows: integration tests via WSL2

Native Postgres on Windows ARM64 is fragile (UCRT runtime gaps, missing ARM64 builds). The reliable path is a disposable Ubuntu under WSL2 with the integration suite running inside WSL (WSL2's NAT blocks the Windows host from reaching :5432, so don't try to run the tests from PowerShell):

powershell
wsl --install -d Ubuntu --no-launch# reboot, then:wsl -d Ubuntu -u root bash -c "apt-get update && apt-get install -y nodejs npm rsync"wsl -d Ubuntu -u root bash /mnt/c/path/to/postgres-mcp/scripts/wsl-pg-setup.shwsl -d Ubuntu -u root bash /mnt/c/path/to/postgres-mcp/scripts/wsl-test-matrix.sh

wsl-pg-setup.sh installs PG15, PG17 and PG18 from the PGDG apt repo (ports are auto-assigned by pg_createcluster -- typically 17 on 5432, 18 on 5433, 15 on 5434), sets the postgres password to postgres, and creates postgres_mcp_test in each. wsl-test-matrix.sh rsyncs the working tree into /root/postgres-mcp, runs npm ci once, and runs the integration suite against every cluster found via pg_lsclusters.

Running these from Git Bash instead of PowerShell? Prefix both script invocations with MSYS_NO_PATHCONV=1. Git Bash rewrites the /mnt/c/... argument before wsl.exe sees it, so the script arrives as C:/Users/<you>/scoop/apps/git/<ver>/mnt/c/... and bash exits with "No such file or directory" having run nothing. Also avoid piping either script into tail/head -- the pipeline's exit status is the last command's, so a failing matrix reports success.

Tear down when finished: wsl --unregister Ubuntu.

License

MIT © 2026 YawLabs

[Follow @YawLabs on X]

来源:README.md,提交 01f8597

工具

23

pg_readonly

Run read-only SQL

Run a SQL statement with no persistent data changes. Always executes inside a `BEGIN READ ONLY` transaction regardless of `ALLOW_WRITES`, so postgres itself rejects any INSERT/UPDATE/DELETE/DDL and the transaction is always rolled back. Use this whenever the goal is to read - SELECT, EXPLAIN, SHOW, VALUES, WITH ... SELECT, etc. Scope caveat for hosts that auto-allow this tool: `READ ONLY` constrains writes to the DATABASE, not every side effect. Functions whose effect is outside the table data - `pg_cancel_backend` / `pg_terminate_backend`, `pg_read_file`, `lo_export`, `COPY ... TO PROGRAM` - are NOT blocked here and are NOT behind the `ALLOW_WRITES` gate that `pg_kill` sits behind. They still require the privileges the `DATABASE_URL` role holds, so a least-privileged role (e.g. `pg_read_all_data`) is what actually bounds this tool. Use `params` for parameterized queries to avoid SQL injection. Params can be strings, numbers, booleans, null, arrays (for postgres arrays / ANY), or objects (for json/jsonb columns). Large result sets are truncated to POSTGRES_MAX_ROWS (default 1000) with a `truncated: true` flag.

输入参数sql必填params

pg_query

Run SQL query

Run a SQL query against the configured PostgreSQL database. Postgres itself is the primary safety gate: the role in `DATABASE_URL` enforces what queries can succeed. The recommended posture is a least-privileged role (e.g. one granted `pg_read_all_data`), which makes writes server-rejected regardless of any env var. `ALLOW_WRITES=1` is a secondary belt-and-braces gate - it lifts the in-server `BEGIN READ ONLY` wrapper, but it cannot grant privileges the role lacks. Useful for managed databases where creating a second role is awkward. For read-only access where you want the guarantee in the tool name, prefer `pg_readonly`. Use `params` for parameterized queries to avoid SQL injection. Params can be strings, numbers, booleans, null, arrays (for postgres arrays / ANY), or objects (for json/jsonb columns). Dates and UUIDs can be passed as ISO strings. Large result sets are truncated to POSTGRES_MAX_ROWS (default 1000) with a `truncated: true` flag.

输入参数sql必填params

pg_list_schemas

List schemas

List non-system schemas in the database. Excludes `pg_catalog`, `information_schema`, and other `pg_*` internals.

无需输入参数。

pg_list_tables

List tables in a schema

List tables (and optionally views) in a schema. Returns name, type (table/view/materialized view/foreign), and estimated row count (from `reltuples`; null = no ANALYZE yet on PG 14+; 0 may mean empty or unanalyzed on PG <= 13). Paginate via `limit`/`offset` on very large schemas.

输入参数limitoffsetschemaincludeViews

pg_describe_table

Describe table

Describe a relation: kind (table / view / materialized_view / partitioned_table / foreign_table), columns (name, type, nullable, default, `generated`, `identity`), primary key, foreign keys (outgoing), `referenced_by` (other tables whose FKs point at this one), `constraints` (CHECK / UNIQUE non-PK / EXCLUDE), indexes, and partition info (`partition_of` parent, `partitions` children). Works on views and materialized views too -- PK/FK/constraint/index lists will simply be empty for a plain view. Use `kind` to disambiguate before assuming you can write to the relation. Generated columns (`generated`: 'stored' / 'virtual') and `identity`: 'always' columns are NOT writable -- omit them from INSERT/UPDATE column lists; a generated column's expression is reported as `generation_expression`, never as `default_value`. On PostgreSQL 18+ constraints also report `validated` / `enforced` / `has_period`, and columns report `not_null_validated` -- a NOT VALID not-null constraint means `nullable: false` can still hide NULLs.

输入参数table必填schema

pg_list_views

List views with definitions

List views and materialized views in a schema with their SQL definitions. Use this over `pg_list_tables` with `includeViews: true` when you want the view body, not just names.

输入参数schemaincludeMaterialized

pg_list_functions

List functions and procedures

List functions, procedures, and aggregates in a schema. Returns name, arguments, return type, kind (function/procedure/aggregate/window), and implementation language.

输入参数schema

pg_list_extensions

List installed extensions

List installed PostgreSQL extensions. Returns name, version, schema, and description. Useful to check for pgvector, postgis, pg_stat_statements, uuid-ossp, etc. before writing queries that rely on them.

无需输入参数。

pg_search_columns

Search columns by name

Search for columns by name across all user schemas. Supports SQL LIKE patterns (`%` matches any substring, `_` matches one character). Case-insensitive. Use this instead of iterating `pg_describe_table` when the user asks 'which tables have X'.

输入参数limitschemapattern必填

pg_explain

Explain query plan

Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE - for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Writes executed during EXPLAIN ANALYZE are rolled back, so you can inspect a plan for an INSERT/UPDATE/DELETE without persisting the rows -- but only what Postgres rolls back is undone: a sequence the statement advanced (nextval, a serial or identity column) stays advanced, and any side effect of a function the statement called that lands outside table data (pg_terminate_backend, an advisory lock) persists. Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement). Planner options (all optional): `buffers` reports shared/local/temp block hits and is the fastest way to tell a bad plan from a cold cache - it defaults to TRUE whenever `analyze` is true (matching PostgreSQL 18, which turns it on for you), pass `buffers: false` to suppress it; requesting it WITHOUT `analyze` needs PostgreSQL 13+. `verbose` adds output columns and schema-qualified names. `settings` (PostgreSQL 12+) lists planner GUCs set away from their defaults - the usual explanation for a plan that looks impossible. `wal` (PostgreSQL 13+) reports WAL generated and `serialize` (`none`|`text`|`binary`, PostgreSQL 17+) charges the cost of building the result rows; both require `analyze`. `memory` (PostgreSQL 17+) reports memory used by the PLANNER, so it works with or without `analyze` - use it alone to ask why planning a statement is expensive. `generic_plan` (PostgreSQL 16+) plans a parameterized statement WITHOUT values for its $1/$2 placeholders and cannot be combined with `analyze` or `params`. `costs` and `timing` default to true (as in postgres); set either to false to drop those columns, and note `timing` only applies with `analyze`. Options that need a newer server than the one connected are rejected with an expli

输入参数sql必填walcostsformatmemoryparamstiminganalyzebuffersverbosesettingsserialize另有 2 个

pg_index_advisor

Recommend indexes for a workload

Recommend indexes for a workload, and prove each one pays for itself before recommending it. Give it `statements` (the SQL you care about) or let it take the top N from `pg_stat_statements`; it plans each statement, generates candidate indexes, costs them with HypoPG hypothetical indexes, and returns only the ones that measurably cut estimated cost. How candidates are generated, and the honest limit: this tool has NO SQL parser and does not read your SQL text. It EXPLAINs each statement and harvests the columns the PLANNER reports as filters, join keys, and sort keys, then intersects those tokens with the real column list from `pg_attribute` -- so a candidate can never name a column that does not exist. The extraction is deliberately loose (a token matching a real column name on a different table can slip through); HypoPG is the arbiter, and anything that does not lower cost is discarded. Column ORDER within each candidate is equality columns first (most selective first, from `pg_stats`), then at most one range column, then sort columns. The search is greedy and BOUNDED. Each accepted index stays in place while the rest are re-costed on top of it, so later picks account for what earlier ones already fixed. `max_candidates` caps how many candidates are considered and `max_explains` caps total EXPLAIN round trips; when a cap stops the search early, `budget_exhausted` is true and the result is a truncated search, not a converged one. PostgreSQL 18 note, and it reverses a rule you have probably internalized: PG18 added B-tree SKIP SCAN, so a multi-column index whose LEADING column the query never constrains CAN now be used. The classic 'leading column never filtered means the index is useless' heuristic is wrong on PG18+. This tool gates that prune on the server version -- on PG18+ such candidates are kept and costed (`skip_scan_available: true`, and an accepted one carries `requires_skip_scan`), below PG18 they are pruned as unusable and counted in `candidates_pruned_l

输入参数limitschemastatementsmax_explainsmax_candidatesmin_improvementmax_index_columnsmax_recommendations

pg_health

Database health snapshot

Quick health snapshot: server version, database size, connection counts measured against `max_connections`, active queries with their wait events, a pg_stat_database rollup, and table count. Useful as a connection sanity check and to spot runaway queries, connection-cap pressure, and lock/IO waits. - connections: `total` for the CURRENT database, broken down into `active` / `idle` / `idle_in_transaction` / `idle_in_transaction_aborted` / `other` (starting, fastpath function call, disabled) / `state_unavailable` -- those six sum to `total`. `idle_in_transaction_aborted` is called out separately because it holds locks and blocks vacuum while doing no work and will never commit. `state_unavailable` counts sessions whose `state` reads NULL because the role lacks pg_read_all_stats / pg_monitor membership; a non-zero value means every other bucket is under-counted by at least that much, so do NOT read `active: 0` next to it as an idle database. Plus `cluster_client_backends` (client backends across ALL databases -- those are what actually consume connection slots), `max_connections`, `superuser_reserved_connections`, and `used_fraction` (cluster_client_backends / max_connections). A raw connection count means nothing without the cap; read `used_fraction` first. - active_queries: `pid`, `state`, `query`, `application_name`, `backend_type`, `wait_event_type` / `wait_event` (both NULL when the backend is running rather than waiting -- the single most diagnostic pair in pg_stat_activity). Both are reported verbatim as the server spells them, and that spelling changes between majors: a backend waiting on a buffer pin reports `wait_event_type` 'BufferPin' through PostgreSQL 18 and 'Buffer' from 19 on, with the `wait_event` names beneath it changing to match. Read them against the reported `version` rather than hard-coding a literal. `duration_seconds` (since query_start) and `transaction_age_seconds` (since xact_start). A large transaction_age_seconds next to a small duration_s

输入参数activeQueryLimit

pg_top_queries

Top queries by execution time

Top N queries by total or mean execution time. Requires the `pg_stat_statements` extension to be installed and enabled (most managed Postgres providers have it on by default). Returns `{rows, stats_reset, stats_reset_age_seconds, dealloc}`: each row has normalized query text (constants replaced with `?`), call count, total/mean/min/max time in ms, rows returned, and cache hit ratio. Use this to find slow queries worth optimizing. `calls` and `total_time_ms` are cumulative since the last `pg_stat_statements_reset()`, so this ranking only describes the window that started at the top-level `stats_reset` (with `stats_reset_age_seconds` beside it). This is pg_stat_statements' OWN reset clock, read from `pg_stat_statements_info` -- it is independent of the `stats_reset` reported by `pg_seq_scan_tables` / `pg_unused_indexes`, which comes from pg_stat_database, so do not compare the two timestamps or assume one implies the other. `stats_reset: null` means the start of the window is unknown, not that it covers all time. READ `dealloc` BEFORE TRUSTING THE RANKING: it counts how many times entries for the LEAST-EXECUTED statements were evicted because more distinct statements were seen than `pg_stat_statements.max` allows. A non-zero `dealloc` means this ranking is drawn from an INCOMPLETE population -- queries may be missing from these results entirely, and an evicted query's counters restart from zero if it runs again, understating it. The larger `dealloc` is, the more churn, so 'not in the top N' stops being evidence that a query is cheap. Raise `pg_stat_statements.max` to get a complete picture. On pg_stat_statements < 1.9 (before Postgres 14) `pg_stat_statements_info` does not exist, so `stats_reset`, `stats_reset_age_seconds` and `dealloc` are omitted entirely rather than returned as nulls, and a `_warnings` entry says so. On pg_stat_statements >= 1.10 (Postgres 15+), also returns `io_read_time_ms` and `io_write_time_ms` to separate IO-bound from CPU-bound queries (null

输入参数limitorderBy

pg_seq_scan_tables

Find tables with heavy sequential scans

Tables with high sequential-scan counts relative to index scans - the first place to look for missing-index candidates. Returns `{rows, stats_reset, stats_reset_age_seconds}`: each row has seq_scans, idx_scans, live tuples, and the ratio. A high ratio on a large table usually means a query is reading the whole table where an index would suffice. Pair with `pg_top_queries` to find which query is doing it. These counters are cumulative since the last statistics reset, so every ratio here is only meaningful relative to the top-level `stats_reset` (and `stats_reset_age_seconds`). A ratio measured over a window that was reset minutes ago describes that window, not the workload; `stats_reset: null` means the start of the window is unknown. On PostgreSQL 16+ each row also carries `last_seq_scan` and `last_idx_scan` timestamps (null = no such scan since the reset), which separate 'scanned hard months ago' from 'being scanned right now' in a way the raw counts cannot.

输入参数limitschemaminSize

pg_unused_indexes

Find unused indexes

Indexes that have never been scanned or have very low usage, largest first. Each unused index costs write amplification (every INSERT/UPDATE maintains it) and disk space, so before adding a new index, check whether the fix is to drop a dead one. Returns `{rows, stats_reset, stats_reset_age_seconds}`. READ THIS BEFORE RECOMMENDING A DROP: `scans` is a counter, not a verdict. It only counts since the last statistics reset, which is why the top-level `stats_reset` and `stats_reset_age_seconds` are part of the answer. If the counters were reset an hour ago, EVERY index looks unused; if `stats_reset` is null, the start of the window is unknown and the counts prove nothing. This list is only trustworthy once the reset age comfortably exceeds the slowest cycle that could use the index - a monthly report, a quarterly close, a yearly job, a failover-only query path. PRIMARY KEY and UNIQUE indexes are already excluded from these results: they enforce a constraint and stay load-bearing at zero scans, so they never appear here and their absence is not evidence of anything. On PostgreSQL 16+ each row also carries `last_idx_scan`, the timestamp of the most recent scan (null = never scanned since the reset). 'Not scanned since 2026-02-14' is a far better basis for a decision than a bare count. On PostgreSQL 18+, do not fall back on the old 'the leading column is never filtered, so this index is dead weight' reasoning. Skip scan lets the planner use a multi-column btree whose leading column is unconstrained, so such an index can now be doing real work.

输入参数limitschemamaxScans

pg_io_stats

I/O statistics and in-flight async I/O

I/O observability: cumulative per-backend-type I/O from `pg_stat_io` (PostgreSQL 16+), plus in-flight asynchronous I/O handles from `pg_aios` (PostgreSQL 18+). This is the layer underneath `pg_top_queries` and `pg_health` -- it says WHICH subsystem is doing the I/O (client backends vs autovacuum vs checkpointer vs walwriter) and through which path, which a per-query or per-table view cannot. - io: one row per (`backend_type`, `io_object`, `io_context`) combination. Counters `reads` / `writes` / `extends` / `writebacks` / `hits` / `evictions` / `reuses` / `fsyncs` are bigints returned as decimal strings; `read_time_ms` / `write_time_ms` / `writeback_time_ms` / `extend_time_ms` / `fsync_time_ms` are float8 milliseconds. A timing of 0 next to a non-zero op count means `track_io_timing` is off, NOT that the I/O was free -- turn it on to get real numbers. A NULL counter means the operation is not possible for that combination, which is different from 0. - io[].read_bytes / write_bytes / extend_bytes: a normalized byte figure that means the same thing on every supported server. On PG16-17 it is computed as `op_bytes * <op count>`; on PG18 `op_bytes` was removed and the server reports bytes directly. The top-level `byte_accounting` field says which source produced the numbers. - io[].stats_reset: these are CUMULATIVE counters, so a row is only interpretable next to its reset point. Reported per row because that is how the view reports it; `pg_stat_reset_shared('io')` resets them together in practice, but this tool does not assert that. - Rows whose counters are all zero are omitted by default (`pg_stat_io` is mostly zeros on a quiet system, and the noise buries the handful of rows that matter). Pass `includeZeroRows: true` for the full matrix. - in_flight + io_method: PostgreSQL 18+ ONLY, and both keys are ABSENT on older servers rather than empty/null -- an empty `in_flight` array would read as 'nothing is stalled' when the truth is 'this server cannot tell you'. `in_flig

输入参数limitincludeZeroRows

pg_inspect_locks

Inspect blocking locks

Show current lock contention: which sessions are blocked and who is blocking them. Returns blocked PID, blocking PID, lock types, relation being contested, and the queries involved. Use this first when a tool call hangs or the app feels stuck - it's the fastest way to identify a long-held transaction holding a lock. Row shape: one row per (blocked_pid, blocking_pid) pair. A session waiting on multiple blockers appears on multiple rows -- group/deduplicate by `blocked_pid` if you want a per-blocked-session count. Scope: CLUSTER-WIDE, unlike `pg_health` active_queries, which is filtered to the database in DATABASE_URL. Lock waits cross databases (a shared catalog, a long transaction in a sibling database), so the blocker is not always somewhere this connection could see -- but it does mean a row here may name a pid in another database entirely. `relation` is only resolved for locks in the CURRENT database or on a cluster-shared catalog (pg_authid, pg_database, ...); it is NULL for a lock held in another database, because a pg_class OID is only meaningful within its own database. A NULL `relation` has TWO unrelated causes, and `lock_type` is what tells them apart. When the wait is ON a relation (`lock_type` relation / extend / page / tuple), NULL means the lock is held in another database and cannot be named from here. When the wait is on something else (`transactionid` / `virtualxid` / `advisory`), `relation` was only ever a best-effort hint -- an alphabetical guess among the blocker's held write-intent locks, not authoritative -- so NULL there means the guess found no candidate, and the blocker is very likely LOCAL. Do not read that second case as 'somewhere else': use `blocking_pid` and `blocking_query`, which are populated either way. Note also that pg_cancel_backend / pg_terminate_backend signal by PID across the whole cluster, so even a genuinely cross-database blocker is actionable via `pg_kill`. Use the blocked/blocking query text to disambiguate which table is

输入参数limit

pg_list_roles

List roles

List database roles (users and groups) with their login/superuser/createdb/createrole attributes and inherited role memberships. Use this to answer 'who has access to this database?' without needing to read `pg_authid` directly.

输入参数includeSystem

pg_table_privileges

Show table privileges

Show which roles have which privileges (SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER) on a table or on every table in a schema. If `table` is omitted, the result spans every table in `schema`, ordered by table then grantee. Use this to answer 'who can write to this table?' or to audit schema-wide access before a migration. Visibility caveat: backed by `information_schema.table_privileges`, which postgres filters by what the calling role can see. A least-privileged role may not see grants involving unrelated third-party roles. For a complete picture, run as a superuser or a member of `pg_read_all_data`.

输入参数tableschema

pg_kill

Cancel or terminate a backend

Cancel a running query (SIGINT-equivalent) or terminate a backend connection (SIGTERM-equivalent) by PID. Find the PID via `pg_health` active_queries or `pg_inspect_locks`. Requires ALLOW_WRITES=1 since this changes database session state. The role in DATABASE_URL must have permission - cancelling another user's query needs the `pg_signal_backend` role or superuser. Note: `pg_signal_backend` does NOT cover superuser-owned backends - only a superuser can signal another superuser's session. Cancel is graceful; terminate is forceful. `signaled=true` means postgres SENT the signal, not that the target stopped: a session that is idle (or idle in a transaction) ignores a cancel, so re-check with `pg_health` and use `terminate` for an idle session. `signaled=false` means postgres found no backend with that PID; the `note` field carries its own warning (e.g. 'PID 123 is not a PostgreSQL backend process'). A permission denial is NOT a `false`: postgres raises, so the call returns an error with SQLSTATE 42501 that says what the role lacks.

输入参数pid必填mode

pg_replication_status

Replication status

Replication overview: configured replication slots, connected replicas (from `pg_stat_replication`), and current WAL position. Use on primary to spot lagging or disconnected replicas, on replicas to see upstream status. Returns empty arrays on a standalone (non-replicated) database rather than erroring.

无需输入参数。

pg_advisor

Database advisor (DBA lints)

Rolled-up DBA lint pass. One call returns four categories of findings: - sequence_exhaustion: SERIAL / BIGSERIAL / IDENTITY sequences whose `last_value` is above `seqExhaustionThreshold` of `max_value`. The classic incident class. - wraparound_risk: transaction-ID AND multixact wraparound pressure, the classic pageable incident. `{autovacuum_freeze_max_age, autovacuum_multixact_freeze_max_age, databases[], tables[]}`. Those two cluster GUCs are the divisors both lists are measured against (null if unreadable). Multixact IDs are a SEPARATE 32-bit counter, consumed by row-level locking (`SELECT ... FOR SHARE/UPDATE`, FK checks), so a lock-heavy workload can exhaust them while relfrozenxid stays perfectly healthy -- both counters are checked here. `databases` rows: `{database, xid_age (age(datfrozenxid)), mxid_age (mxid_age(datminmxid)), pct_of_freeze_max_age, pct_of_multixact_freeze_max_age, triggered_by}` -- template databases included, since template0 ages like any other and the cluster horizon is the minimum across all of them. `tables` rows: `{schema, table, relkind, xid_age (age(relfrozenxid)), freeze_max_age, pct_of_freeze_max_age, mxid_age (mxid_age(relminmxid)), multixact_freeze_max_age, pct_of_multixact_freeze_max_age, triggered_by}`, where `freeze_max_age` / `multixact_freeze_max_age` are the EFFECTIVE limits -- a per-table `autovacuum_freeze_max_age` / `autovacuum_multixact_freeze_max_age` storage parameter wins over the GUC. A row is returned when EITHER ratio is at or above `wraparoundThreshold`, and `triggered_by` ('xid' | 'multixact' | 'both') says which one did it: 'xid' means chase freezing/autovacuum, 'multixact' means chase the lock-heavy workload burning members. `mxid_age` and `pct_of_multixact_freeze_max_age` are null on rows whose minmxid is InvalidMultiXactId (no multixact ever recorded); such rows can only be xid-triggered. At pct_of_freeze_max_age 1.0 autovacuum forces an anti-wraparound VACUUM, and near 2.1 billion xids (or 4.2 billion multi

输入参数limitrlsSchemaswraparoundThresholdseqExhaustionThreshold

pg_table_bloat

Estimate table bloat

Estimate table bloat (dead tuples + free space) for tables in a schema. Returns live tuples, dead tuples, dead-tuple ratio, last_vacuum / last_autovacuum timestamps, and total relation size. A high dead_ratio with a stale last_autovacuum is a sign a table needs VACUUM. On PostgreSQL 19+ every row also carries `stats_reset`: the last time THAT relation's counters were reset via `pg_stat_reset_single_table_counters()`. Read it before trusting anything else in the row -- a reset zeroes live_tuples and dead_tuples AND clears last_vacuum / last_autovacuum / last_analyze together, so a table reset a minute ago is indistinguishable from a pristine one without it. The key is ABSENT on older servers rather than null; null on PG19+ means this relation's counters have never been reset. Three methods are available via the `method` parameter: - `estimate` (default): reads pg_stat_user_tables -- fast, no extensions, ANALYZE-driven approximations. Use this first. - `approx`: uses pgstattuple_approx() -- fast sampling pass, more accurate than estimates, requires the pgstattuple extension. - `exact`: uses pgstattuple() -- full table scan, exact counts, slow on large tables, requires the pgstattuple extension. Always pass `schema` with method='exact' -- scanning all user tables in one statement will hit statement_timeout on non-trivial databases. Install pgstattuple with `CREATE EXTENSION pgstattuple` (requires superuser).

输入参数limitmethodschemaminDeadRatio

版本历史

7
  1. v0.13.10最新Oct 31, 2026
  2. v0.13.9Oct 11, 2026
  3. v0.13.8Oct 6, 2026
  4. v0.13.7Sep 30, 2026
  5. v0.13.6Sep 23, 2026
  6. v0.13.4Sep 19, 2026
  7. v0.13.2Sep 16, 2026

评分

已满足适用检查项的 90%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分
  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分