@yawlabs/postgres-mcp 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +361 -0
- package/README.md +43 -6
- package/dist/index.js +530 -151
- package/package.json +11 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.1] - 2026-05-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Multi-query handlers (`pg_describe_table`, `pg_health`, `pg_advisor`,
|
|
14
|
+
`pg_replication_status`) now share a single connection across their
|
|
15
|
+
internal catalog fan-out via a new `withSharedClient` helper, so one
|
|
16
|
+
tool call's 3-9 query fan-out can no longer saturate the pool (default
|
|
17
|
+
max 5) and starve concurrent calls. Previously, a single
|
|
18
|
+
`pg_describe_table` issued 9 parallel `Promise.all` queries against the
|
|
19
|
+
pool; under concurrent load this could block other tool calls until
|
|
20
|
+
the describe drained.
|
|
21
|
+
- `pg_top_queries` now returns `calls` and `rows` as text strings,
|
|
22
|
+
matching the bigint serialization of `pg_seq_scan_tables`,
|
|
23
|
+
`pg_unused_indexes`, and `pg_table_bloat`. Timing fields stay as JSON
|
|
24
|
+
numbers since they are inherently fractional milliseconds.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `runUserQueryBounded` now distinguishes "DECLARE failed" (re-running
|
|
28
|
+
on the direct-exec path is safe -- the user SQL never executed) from
|
|
29
|
+
"FETCH/CLOSE/RELEASE failed" (re-running could double-execute side
|
|
30
|
+
effects). Previously, a transient FETCH-time failure on a
|
|
31
|
+
RETURNING-DML statement would silently re-run the mutation.
|
|
32
|
+
- `pg_top_queries` `orderBy: "calls"` now sorts numerically rather than
|
|
33
|
+
lexically. The 0.4.1 change to return `calls` as `text` shadowed the
|
|
34
|
+
source bigint column with a text output alias in the ORDER BY, so
|
|
35
|
+
"9" beat "10". The fix qualifies the ORDER BY expression with the
|
|
36
|
+
source table. Caught in the post-implementation review before tagging.
|
|
37
|
+
- `pg_describe_table` "not found" error message now escapes user-supplied
|
|
38
|
+
schema/table names via `JSON.stringify`, so a name containing `"` no
|
|
39
|
+
longer renders as broken-looking nested quotes.
|
|
40
|
+
- `pg_explain` `hypothetical_indexes` now pre-flight rejects pre-quoted
|
|
41
|
+
identifiers (`"odd.name"`, `weird"col`) with a clear validation error
|
|
42
|
+
before opening a database connection, instead of producing a confusing
|
|
43
|
+
planner error after the fact.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- New CI release plumbing mirroring `@yawlabs/tailscale-mcp`:
|
|
47
|
+
`.github/workflows/ci.yml` (lint + build + test on push/PR across
|
|
48
|
+
Node 18/20/22), `.github/workflows/integration.yml` (PG17 + PG18
|
|
49
|
+
service-container matrix, scheduled nightly + on-demand), and
|
|
50
|
+
`.github/workflows/release.yml` (tag-pushes-trigger-publish gated
|
|
51
|
+
on integration). `npm publish` now runs in CI with `--provenance`
|
|
52
|
+
via the org-level `NPM_TOKEN`; `release.sh` retains a local-dev
|
|
53
|
+
path that runs the WSL integration matrix as a pre-flight when
|
|
54
|
+
cutting a release from a workstation.
|
|
55
|
+
- New regression tests:
|
|
56
|
+
- `compareVersions` unit tests covering pre-release tags,
|
|
57
|
+
missing/longer segments, and the actual `1.8` boundary used by
|
|
58
|
+
`pg_top_queries`.
|
|
59
|
+
- Integration guard that partition children inheriting a parent's
|
|
60
|
+
primary key are correctly excluded from `pg_advisor`'s
|
|
61
|
+
`tables_without_primary_key` list.
|
|
62
|
+
- Integration coverage for the cursor-fallback path on both DDL
|
|
63
|
+
(`CREATE TABLE`) and DML-without-RETURNING (`INSERT`).
|
|
64
|
+
|
|
65
|
+
## [0.4.0] - 2026-04-25
|
|
66
|
+
|
|
67
|
+
### Security
|
|
68
|
+
- User SQL fetches are now memory-bounded by Postgres, not by Node. The
|
|
69
|
+
prior flow had node-pg materialize the entire result set into memory and
|
|
70
|
+
then sliced down to `POSTGRES_MAX_ROWS` for output -- so a payload like
|
|
71
|
+
`SELECT * FROM big1 CROSS JOIN big2` could OOM the MCP process before
|
|
72
|
+
the 30 s `statement_timeout` fired. User SQL now runs through a
|
|
73
|
+
server-side `DECLARE ... NO SCROLL CURSOR FOR ...` + `FETCH MAX_ROWS+1`
|
|
74
|
+
pattern; only the response-sized batch is ever materialized in Node.
|
|
75
|
+
Non-cursorable statements (DDL, DML without RETURNING, utility commands)
|
|
76
|
+
fall back to a direct execute via `SAVEPOINT` so the outer transaction
|
|
77
|
+
stays alive -- those statements never produce a runaway result set
|
|
78
|
+
anyway.
|
|
79
|
+
- User SQL is now sent with `queryMode: 'extended'`, forcing pg to use the
|
|
80
|
+
extended query protocol regardless of whether `params` is empty. The
|
|
81
|
+
extended protocol restricts each request to a single statement, closing
|
|
82
|
+
the stacked-query injection pattern documented by
|
|
83
|
+
[Datadog Security Labs](https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/)
|
|
84
|
+
against the now-archived `@modelcontextprotocol/server-postgres`. Without
|
|
85
|
+
this fix, a payload like `SELECT 1; COMMIT; DROP SCHEMA x CASCADE;` passed
|
|
86
|
+
to `pg_query` would escape the `BEGIN READ ONLY` wrapper and run DDL in
|
|
87
|
+
autocommit. Added an integration regression test that asserts the
|
|
88
|
+
multi-statement request is rejected by Postgres.
|
|
89
|
+
- `pg` minimum bumped from `^8.13.0` to `^8.14.0`. The `queryMode: 'extended'`
|
|
90
|
+
option that backs the stacked-query guard above is silently ignored on pg
|
|
91
|
+
8.13.x -- a fresh `npm install` resolving to that range would have regressed
|
|
92
|
+
the security guard with no visible signal. Today's lockfile pins 8.20.0;
|
|
93
|
+
the range bump prevents future installs from sliding back.
|
|
94
|
+
- `@types/pg` bumped from `^8.11.10` to `^8.20.0` to track pg 8.20.0 runtime.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
- New `pg_advisor` tool: rolled-up DBA lints in one call. Returns three
|
|
98
|
+
categories of findings -- `sequence_exhaustion` (sequences whose
|
|
99
|
+
last_value is past `seqExhaustionThreshold` of max_value, default 50%;
|
|
100
|
+
the classic "BIGINT, eventually" incident class), `tables_without_primary_key`
|
|
101
|
+
(bloat candidates and a sign of design drift), and
|
|
102
|
+
`public_tables_without_rls` (default `public`; configurable via
|
|
103
|
+
`rlsSchemas`). Use as the "what should I be looking at?" starting
|
|
104
|
+
point and drill into `pg_unused_indexes` / `pg_table_bloat` /
|
|
105
|
+
`pg_seq_scan_tables` for the perf side.
|
|
106
|
+
- `pg_explain` accepts a `hypothetical_indexes` parameter -- list of
|
|
107
|
+
`{table, columns, using?}` -- which asks the planner "what would the
|
|
108
|
+
plan be if these indexes existed?". Requires the
|
|
109
|
+
[HypoPG](https://github.com/HypoPG/hypopg) extension
|
|
110
|
+
(`CREATE EXTENSION hypopg;`); the tool returns a friendly hint pointing
|
|
111
|
+
at that command if HypoPG isn't installed. Indexes are session-scoped
|
|
112
|
+
and torn down at the end of the call via `hypopg_reset()`, so they
|
|
113
|
+
never persist across MCP requests and never touch real disk. Closes
|
|
114
|
+
the biggest competitive gap vs. Crystal DBA's Postgres MCP Pro per
|
|
115
|
+
the comp-landscape audit. WSL setup script now installs the
|
|
116
|
+
`postgresql-${V}-hypopg` package opportunistically; the matrix test
|
|
117
|
+
runs against PG17 and PG18 and verifies the planner switches a Seq
|
|
118
|
+
Scan to an Index/Bitmap scan when a hypothetical index is supplied.
|
|
119
|
+
- `pg_describe_table` now returns four new fields:
|
|
120
|
+
- `referenced_by` -- incoming FKs (other tables whose foreign keys point at
|
|
121
|
+
this one). Answers "what depends on this table?" before a destructive
|
|
122
|
+
change. None of the surveyed competing Postgres MCP servers expose this;
|
|
123
|
+
in psql you'd run `\d+` on every candidate table and squint.
|
|
124
|
+
- `constraints` -- CHECK / UNIQUE non-PK / EXCLUDE constraints with each
|
|
125
|
+
constraint's full definition string from `pg_get_constraintdef()`. PK and
|
|
126
|
+
FK still live in their dedicated fields, so no double-listing.
|
|
127
|
+
- `partition_of` -- when the relation is a partition, the parent
|
|
128
|
+
schema / table.
|
|
129
|
+
- `partitions` -- when the relation is a `partitioned_table`, the list of
|
|
130
|
+
children with their `pg_get_expr()` partition bounds.
|
|
131
|
+
- `pg_query` result `fields` now include `dataTypeName` (e.g. `int4`, `text`,
|
|
132
|
+
`jsonb`) alongside `dataTypeID`. Previously LLMs saw only the OID and had
|
|
133
|
+
to map it themselves. Resolution is process-cached against `pg_type`, with
|
|
134
|
+
a single miss-fill query for any OID introduced by `CREATE TYPE` mid-session.
|
|
135
|
+
- npm tarball now ships `LICENSE`, `README.md`, and `CHANGELOG.md` alongside
|
|
136
|
+
the bundle. Previously the `files` allowlist was `["dist/index.js"]` only,
|
|
137
|
+
so `npm pack` produced a tarball with no docs and no license file -- bad
|
|
138
|
+
for downstream consumers and registry surfaces that read README from the
|
|
139
|
+
tarball rather than the repo.
|
|
140
|
+
- README workflow examples now show three multi-tool sequences (unstick a
|
|
141
|
+
hung app via `pg_inspect_locks` -> `pg_kill`, chase a slow page via
|
|
142
|
+
`pg_top_queries` -> `pg_explain` -> `pg_unused_indexes`, oncall triage via
|
|
143
|
+
`pg_health` -> `pg_inspect_locks` -> `pg_replication_status`). The previous
|
|
144
|
+
example list was single-tool only.
|
|
145
|
+
- New `pg_table_bloat` integration test asserts every returned `dead_ratio`
|
|
146
|
+
is a finite number in `[0, 1]`. Locks down the invariant that the recent
|
|
147
|
+
`dead / (live + dead)` formula change was meant to enforce.
|
|
148
|
+
- New `pg_query` integration tests for the `$1`-without-params error path
|
|
149
|
+
and the result-count-equals-`POSTGRES_MAX_ROWS` boundary (must NOT flag
|
|
150
|
+
`truncated: true`).
|
|
151
|
+
- New `pg_explain` integration tests for `analyze: true` on a SELECT in
|
|
152
|
+
read-only mode (works) and on an INSERT in read-only mode (errors with
|
|
153
|
+
the ALLOW_WRITES hint).
|
|
154
|
+
- npm `keywords` expanded with `agent`, `claude`, `claude-code`, `cursor`,
|
|
155
|
+
`llm` so registry / search engine queries for "postgres mcp claude code"
|
|
156
|
+
surface this package.
|
|
157
|
+
- `POSTGRES_CONNECTION_TIMEOUT_MS` env var (default `10000`). Without it, a
|
|
158
|
+
dead host hangs the first connection attempt until the OS times out
|
|
159
|
+
(~2 minutes on most platforms), and the agent waits the whole time before
|
|
160
|
+
surfacing an error.
|
|
161
|
+
- `pg_describe_table` now returns a `kind` field (`table` / `view` /
|
|
162
|
+
`materialized_view` / `partitioned_table` / `foreign_table`). Previously
|
|
163
|
+
the tool silently accepted views and matviews and returned columns with
|
|
164
|
+
empty `primary_key` / `foreign_keys` / `indexes` -- correct, but an LLM
|
|
165
|
+
couldn't tell whether the relation was writable.
|
|
166
|
+
|
|
167
|
+
### Changed
|
|
168
|
+
- README now states the supported Postgres versions: tested on 17 and 18,
|
|
169
|
+
expected to work on 13+.
|
|
170
|
+
|
|
171
|
+
### Changed
|
|
172
|
+
- `pg_top_queries` now returns `calls` and `rows` as JS numbers (cast to
|
|
173
|
+
`float8` in SQL) instead of strings. The previous `::text` cast forced
|
|
174
|
+
consumers to parse `"42"` to use it; the timing fields next to them were
|
|
175
|
+
already numbers, so the response shape was inconsistent. float8 is fine
|
|
176
|
+
-- 2^53 is well above any realistic per-query call/row count.
|
|
177
|
+
- `pg_health` partial-failure shape: failed sub-queries now contribute to a
|
|
178
|
+
top-level `_warnings: string[]` array, and the affected fields stay null
|
|
179
|
+
instead of becoming `{error: "..."}`. Previously a failure of the size
|
|
180
|
+
query made `data.database.size_bytes` resolve to `undefined` with no
|
|
181
|
+
signal -- LLMs couldn't tell "missing" from "errored". This matches the
|
|
182
|
+
`_warnings` convention `pg_describe_table` already uses.
|
|
183
|
+
- `shutdown()` now races `pool.end()` against a 5 s timer. `pool.end()`
|
|
184
|
+
waits for in-flight queries with no upper bound, so a wedged query
|
|
185
|
+
(frozen NFS, network hang) could leave the MCP server appearing stuck on
|
|
186
|
+
exit until the OS reaped the TCP sockets.
|
|
187
|
+
- `pg_table_privileges` description tightened to spell out that omitting
|
|
188
|
+
`table` returns privileges for every table in the schema.
|
|
189
|
+
- README now lists supported PG versions explicitly and groups workflow
|
|
190
|
+
examples by single-tool / multi-tool intent.
|
|
191
|
+
|
|
192
|
+
### Fixed
|
|
193
|
+
- `pg_table_bloat` now uses `dead / (live + dead)` for `dead_ratio` instead of
|
|
194
|
+
`dead / live`. The previous formula reported `0` for tables with `live = 0`
|
|
195
|
+
even when `dead > 0`, hiding the most-bloated tables (an empty-shell table
|
|
196
|
+
full of dead tuples is the textbook VACUUM target). The new formula is
|
|
197
|
+
bounded `[0, 1]`, behaves correctly at edges, and the `WHERE` filter now
|
|
198
|
+
excludes tables with both counters at 0 entirely. The `minDeadRatio`
|
|
199
|
+
parameter description was updated to match.
|
|
200
|
+
- `npm test` now serializes test files with `--test-concurrency=1`. Unit tests
|
|
201
|
+
in `api.test.ts` and `tools/admin.test.ts` both mutate `process.env`
|
|
202
|
+
(`ALLOW_WRITES`, `POSTGRES_MAX_ROWS`, etc.); under Node's default parallel
|
|
203
|
+
test-file scheduling these races could flap on CI.
|
|
204
|
+
- README integration-suite paragraph corrected: the schema is named
|
|
205
|
+
`test_fixture`, not `postgres_mcp_integration`.
|
|
206
|
+
- `scripts/wsl-test-matrix.sh` derives `REPO_SRC` from its own location instead
|
|
207
|
+
of a hardcoded `/mnt/c/Users/jeff/...` path. Anyone other than the original
|
|
208
|
+
author can now run the matrix from their own clone.
|
|
209
|
+
- `release.sh` creates annotated tags (`git tag -a`) and pushes with
|
|
210
|
+
`--follow-tags` instead of `--tags`. The previous `--tags` form pushed every
|
|
211
|
+
local tag, including any unrelated experimental ones lying around;
|
|
212
|
+
`--follow-tags` only pushes the tag(s) reachable from the commits being
|
|
213
|
+
pushed -- but it ignores lightweight tags, so the tag-creation step had to
|
|
214
|
+
switch to annotated to keep working.
|
|
215
|
+
|
|
216
|
+
## [0.3.2] - 2026-04-24
|
|
217
|
+
|
|
218
|
+
### Fixed
|
|
219
|
+
- `pg_explain` with `analyze: true` and `ALLOW_WRITES=1` no longer persists
|
|
220
|
+
writes executed by `EXPLAIN ANALYZE`. Previously the write ran inside a
|
|
221
|
+
`BEGIN; ... COMMIT` transaction, so `pg_explain { analyze: true, sql:
|
|
222
|
+
"INSERT ..." }` would actually insert the row. Now writes run inside a
|
|
223
|
+
`BEGIN; ... ROLLBACK` transaction — the plan (with real row counts and
|
|
224
|
+
timing) comes back but the mutation is rolled back. This matches the user
|
|
225
|
+
expectation when asking for a plan, and the tool description has been
|
|
226
|
+
updated to reflect it.
|
|
227
|
+
- `pg_health` `table_count` now excludes `pg_temp_%` schemas, matching the
|
|
228
|
+
filter in `pg_list_schemas`. The `relkind` filter already masked most
|
|
229
|
+
divergence, but the two queries are now consistent.
|
|
230
|
+
- `pg_seq_scan_tables` ratio column simplified. The previous CASE had a
|
|
231
|
+
branch that only fired when `idx_scan = 0 AND seq_scan = 0` (practically
|
|
232
|
+
unreachable given the table was ordered by `seq_scan DESC`), returning
|
|
233
|
+
`0` and implying a distinction that didn't exist. Now returns `NULL`
|
|
234
|
+
whenever `idx_scan = 0`, which is the meaningful ratio-undefined case.
|
|
235
|
+
|
|
236
|
+
### Added
|
|
237
|
+
- `process.stdin` `end` handler cleans up the pg pool when the MCP client
|
|
238
|
+
disconnects. Previously the server kept running for up to 60 seconds
|
|
239
|
+
(the pool's idle timeout) after the parent closed the pipe.
|
|
240
|
+
- Shared `src/tools/params.ts` for the `paramValue` zod schema, previously
|
|
241
|
+
duplicated verbatim in `query.ts` and `explain.ts`.
|
|
242
|
+
|
|
243
|
+
### Infrastructure
|
|
244
|
+
- Integration test suites now share one `before(setupFixtures)` /
|
|
245
|
+
`after(teardownFixtures)` per file via an outer `describe`, instead of
|
|
246
|
+
running DROP/CREATE per inner `describe`. Each file previously reset
|
|
247
|
+
the fixture schema 3-4 times; now it resets once.
|
|
248
|
+
|
|
249
|
+
## [0.3.1] - 2026-04-22
|
|
250
|
+
|
|
251
|
+
### Fixed
|
|
252
|
+
- `pg_list_roles` with `includeSystem: false` (the default) now actually
|
|
253
|
+
excludes built-in `pg_*` roles. The previous `LIKE 'pg\_%' ESCAPE '\\'`
|
|
254
|
+
filter ended up as SQL `ESCAPE '\\'` (two backslashes), which Postgres
|
|
255
|
+
rejects since `ESCAPE` requires a single character — so the whole filter
|
|
256
|
+
was silently being dropped. Replaced with `starts_with(rolname, 'pg_')`.
|
|
257
|
+
- `pg_describe_table` foreign-key `columns` and `foreign_columns` are now
|
|
258
|
+
proper JSON arrays. They were previously returned as the raw postgres
|
|
259
|
+
text form (e.g. `"{user_id}"`) because `array_agg(name)` returns `name[]`,
|
|
260
|
+
which node-pg doesn't auto-parse. Cast to `text[]` so the driver parses.
|
|
261
|
+
|
|
262
|
+
### Infrastructure (main branch CI hygiene; no user-facing changes)
|
|
263
|
+
- `.gitattributes` forces LF line endings in the working tree on every OS,
|
|
264
|
+
so biome's formatter doesn't reject every file on Windows runners after
|
|
265
|
+
git's auto-CRLF conversion.
|
|
266
|
+
- The integration CI job now starts postgres via `docker run` with
|
|
267
|
+
`-c shared_preload_libraries=pg_stat_statements` instead of the `services:`
|
|
268
|
+
block (which passes options to `docker create`, where `-c` means
|
|
269
|
+
--cpu-shares and collided with the postmaster flag).
|
|
270
|
+
- Cross-platform test discovery via `scripts/run-tests.mjs`. `node --test dist`
|
|
271
|
+
hangs on Windows; `dist/**/*.test.js` globs only expand in bash with
|
|
272
|
+
globstar. The wrapper uses `fs.readdirSync({ recursive: true })` (stdlib)
|
|
273
|
+
and passes explicit paths, plus `--test-concurrency=1` for the integration
|
|
274
|
+
suite so fixture-schema setup doesn't race.
|
|
275
|
+
|
|
276
|
+
## [0.3.0] - 2026-04-22
|
|
277
|
+
|
|
278
|
+
### Added
|
|
279
|
+
- `pg_list_views` — list views and materialized views with SQL definitions.
|
|
280
|
+
- `pg_list_functions` — list functions, procedures, and aggregates with signatures.
|
|
281
|
+
- `pg_list_extensions` — list installed extensions (pgvector, postgis, etc.) with versions.
|
|
282
|
+
- `pg_search_columns` — find columns by name pattern across all user schemas.
|
|
283
|
+
- `pg_top_queries` — top N queries by total/mean execution time from
|
|
284
|
+
`pg_stat_statements`. Detects extension version and picks the right column
|
|
285
|
+
names (v1.8+ uses `total_exec_time`, older uses `total_time`). Returns clear
|
|
286
|
+
setup instructions if the extension is not installed.
|
|
287
|
+
- `pg_list_tables` now accepts `limit` / `offset` for pagination on large schemas.
|
|
288
|
+
- `pg_health` now accepts `activeQueryLimit` (1–100) to override the default of 10.
|
|
289
|
+
- `pg_query` / `pg_explain` `params` now accept arrays and objects (for
|
|
290
|
+
postgres arrays, `ANY`, and json/jsonb columns) in addition to scalars.
|
|
291
|
+
- `POSTGRES_SSL_REJECT_UNAUTHORIZED` env var to disable TLS cert verification for
|
|
292
|
+
managed databases using private-CA certs (Supabase, Neon, RDS). Documented in
|
|
293
|
+
a new "Connecting to managed Postgres" README section.
|
|
294
|
+
- `pg_describe_table` now surfaces partial failures via a `_warnings` array
|
|
295
|
+
instead of silently collapsing FK/index fetch errors into empty lists.
|
|
296
|
+
- Troubleshooting section in README covering common failure modes (env vars,
|
|
297
|
+
auth, timeouts, write-blocked, pool exhaustion, cold-start latency).
|
|
298
|
+
- CHANGELOG.md.
|
|
299
|
+
- Dependabot config for npm + github-actions (weekly, grouped dev deps).
|
|
300
|
+
- Windows CI matrix (ubuntu + windows × Node 18/20/22).
|
|
301
|
+
- Integration test suite (`npm run test:integration`) that exercises every
|
|
302
|
+
tool against a real Postgres instance. Gated on `POSTGRES_MCP_INTEGRATION=1`
|
|
303
|
+
so local `npm test` stays fast with no DB required. CI runs it on Linux via
|
|
304
|
+
a `postgres:16` service container with `pg_stat_statements` preloaded.
|
|
305
|
+
- `pg_inspect_locks` — show current blocking locks (blocked PID, blocker PID,
|
|
306
|
+
relation, lock type, both queries). First tool to reach for when a session
|
|
307
|
+
hangs or the app feels stuck.
|
|
308
|
+
- `pg_list_roles` — database roles with login/superuser/createdb/createrole
|
|
309
|
+
flags and inherited group memberships.
|
|
310
|
+
- `pg_table_privileges` — who has SELECT/INSERT/UPDATE/DELETE/etc. on a table,
|
|
311
|
+
or on all tables in a schema. Useful for pre-migration audits.
|
|
312
|
+
- `pg_seq_scan_tables` — tables with heavy sequential scans relative to index
|
|
313
|
+
scans. Missing-index candidates.
|
|
314
|
+
- `pg_unused_indexes` — non-unique, non-primary indexes with low/zero scan
|
|
315
|
+
counts. Drop candidates (each unused index costs write amplification).
|
|
316
|
+
- `pg_kill` — cancel a running query or terminate a backend by PID. Requires
|
|
317
|
+
`ALLOW_WRITES=1` since it changes session state. Distinguishes `cancel`
|
|
318
|
+
(SIGINT-equivalent, graceful) from `terminate` (SIGTERM, forceful).
|
|
319
|
+
- `pg_table_bloat` — estimate dead tuples and vacuum-candidate tables from
|
|
320
|
+
`pg_stat_user_tables`. No extensions required.
|
|
321
|
+
- `pg_replication_status` — replication slots, connected replicas with lag,
|
|
322
|
+
and current WAL position. Returns empty arrays on a standalone DB rather
|
|
323
|
+
than erroring, so it's safe to call unconditionally.
|
|
324
|
+
- New "What can an agent do with this?" README section with concrete example
|
|
325
|
+
conversations mapped to tool calls.
|
|
326
|
+
|
|
327
|
+
### Changed
|
|
328
|
+
- Loosened identifier validation on `pg_list_tables` and `pg_describe_table`.
|
|
329
|
+
Quoted identifiers (e.g. `"My Table"`) now work. Length capped at 63 bytes
|
|
330
|
+
(the postgres limit) via zod schema; the previous regex-based whitelist
|
|
331
|
+
blocked legitimate identifiers.
|
|
332
|
+
- Pool `idleTimeoutMillis` widened 10s → 60s. MCP sessions routinely have
|
|
333
|
+
minute-long gaps; the short timeout was forcing a reconnect on every tool
|
|
334
|
+
call.
|
|
335
|
+
- `pg_query` / `pg_explain` `sql` inputs now hard-capped at 1 MB.
|
|
336
|
+
|
|
337
|
+
### Fixed
|
|
338
|
+
- `pg_explain` now rejects pre-wrapped SQL (e.g. `"EXPLAIN SELECT 1"`) with a
|
|
339
|
+
clear error instead of producing a `EXPLAIN (...) EXPLAIN SELECT 1` syntax
|
|
340
|
+
error. LLMs frequently make this mistake.
|
|
341
|
+
|
|
342
|
+
## [0.1.1] - 2026-04-21
|
|
343
|
+
|
|
344
|
+
### Changed
|
|
345
|
+
- Release workflow: widened npm registry propagation wait from 1 min to 10 min
|
|
346
|
+
to handle occasional stalls observed on initial publish.
|
|
347
|
+
|
|
348
|
+
## [0.1.0] - 2026-04-21
|
|
349
|
+
|
|
350
|
+
Initial release.
|
|
351
|
+
|
|
352
|
+
### Added
|
|
353
|
+
- `pg_query` — run SQL with read-only-by-default safety. Writes opt in via `ALLOW_WRITES=1`.
|
|
354
|
+
- `pg_list_schemas` — list non-system schemas.
|
|
355
|
+
- `pg_list_tables` — list tables (and optionally views) with estimated row counts.
|
|
356
|
+
- `pg_describe_table` — columns, PK, FKs, indexes.
|
|
357
|
+
- `pg_explain` — `EXPLAIN` / `EXPLAIN ANALYZE` with text or JSON output.
|
|
358
|
+
- `pg_health` — server version, db size, connections, active queries, table count.
|
|
359
|
+
- Single-file bundled distribution (zero runtime deps) for fast `npx` cold starts.
|
|
360
|
+
- Result row truncation at `POSTGRES_MAX_ROWS` (default 1000).
|
|
361
|
+
- Parameterized queries via `params` on `pg_query` and `pg_explain`.
|
package/README.md
CHANGED
|
@@ -92,29 +92,34 @@ Prefer scoping this to dev/test databases — for production, leave writes off a
|
|
|
92
92
|
|
|
93
93
|
## What can an agent do with this?
|
|
94
94
|
|
|
95
|
-
Once connected, the agent picks tools automatically based on what you ask. A few
|
|
95
|
+
Once connected, the agent picks tools automatically based on what you ask. A few single-tool examples:
|
|
96
96
|
|
|
97
|
-
- **"Describe the users table"** -> `pg_describe_table` -> returns columns, PK, FKs, indexes.
|
|
97
|
+
- **"Describe the users table"** -> `pg_describe_table` -> returns kind, columns, PK, FKs, indexes.
|
|
98
98
|
- **"Which tables have a `user_id` column?"** -> `pg_search_columns` with pattern `user_id` -> one call instead of iterating every table.
|
|
99
99
|
- **"This query is slow, why?"** -> `pg_explain` with `analyze: true` -> returns the plan with actual row counts and timing.
|
|
100
100
|
- **"What's the slowest query we run?"** -> `pg_top_queries` -> returns the top N from `pg_stat_statements` with mean/total/min/max times.
|
|
101
|
-
- **"Why is my app hanging?"** -> `pg_inspect_locks` -> returns blocked PIDs and the queries holding their locks; follow up with `pg_kill` (with `ALLOW_WRITES=1`) to cancel the blocker.
|
|
102
101
|
- **"Do we have any unused indexes?"** -> `pg_unused_indexes` -> returns non-unique, non-primary indexes with zero or low scan counts + their size.
|
|
103
102
|
- **"Is `pgvector` installed?"** -> `pg_list_extensions` -> yes/no with version.
|
|
104
103
|
|
|
104
|
+
The bigger leverage is multi-tool reasoning. A few real workflows:
|
|
105
|
+
|
|
106
|
+
- **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."
|
|
107
|
+
- **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."
|
|
108
|
+
- **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.
|
|
109
|
+
|
|
105
110
|
## Tools
|
|
106
111
|
|
|
107
112
|
| Tool | Description |
|
|
108
113
|
|------|-------------|
|
|
109
|
-
| `pg_query` | Run a SQL query. Read-only by default; writes require `ALLOW_WRITES=1`. Supports parameterized queries via `params`. |
|
|
114
|
+
| `pg_query` | Run a SQL query. Read-only by default; writes require `ALLOW_WRITES=1`. Supports parameterized queries via `params`. Result fields include `dataTypeName` (e.g. `int4`, `jsonb`) alongside `dataTypeID`. |
|
|
110
115
|
| `pg_list_schemas` | List non-system schemas. |
|
|
111
116
|
| `pg_list_tables` | List tables (and optionally views) in a schema with estimated row counts. Paginated via `limit`/`offset`. |
|
|
112
|
-
| `pg_describe_table` |
|
|
117
|
+
| `pg_describe_table` | Kind, columns, PK, outgoing FKs, incoming FKs (`referenced_by`), CHECK / UNIQUE / EXCLUDE constraints, indexes, and partition parent/children for a relation. |
|
|
113
118
|
| `pg_list_views` | List views and materialized views in a schema, including their SQL definitions. |
|
|
114
119
|
| `pg_list_functions` | List functions, procedures, and aggregates in a schema with signatures and return types. |
|
|
115
120
|
| `pg_list_extensions` | List installed extensions (pgvector, postgis, pg_stat_statements, etc.) with versions. |
|
|
116
121
|
| `pg_search_columns` | Find columns by name pattern across all user schemas. Case-insensitive, supports SQL LIKE wildcards. |
|
|
117
|
-
| `pg_explain` | `EXPLAIN` or `EXPLAIN ANALYZE` for a SQL statement. Text or JSON output. |
|
|
122
|
+
| `pg_explain` | `EXPLAIN` or `EXPLAIN ANALYZE` for a SQL statement. Text or JSON output. Optional `hypothetical_indexes` (requires the [HypoPG](https://github.com/HypoPG/hypopg) extension) lets you ask "what would the plan be with these indexes?" without creating them on disk. |
|
|
118
123
|
| `pg_health` | Server version, database size, connection count, active queries, table count. |
|
|
119
124
|
| `pg_top_queries` | Top N queries by total/mean execution time. Requires the `pg_stat_statements` extension. |
|
|
120
125
|
| `pg_seq_scan_tables` | Tables with heavy sequential scans — missing-index candidates. |
|
|
@@ -124,6 +129,7 @@ Once connected, the agent picks tools automatically based on what you ask. A few
|
|
|
124
129
|
| `pg_table_privileges` | Who has SELECT/INSERT/UPDATE/DELETE/etc. on a table or whole schema. |
|
|
125
130
|
| `pg_table_bloat` | Tables with high dead-tuple ratios — VACUUM candidates. |
|
|
126
131
|
| `pg_replication_status` | Replication slots, connected replicas, and current WAL position. |
|
|
132
|
+
| `pg_advisor` | Rolled-up DBA lints in one call: sequence-exhaustion candidates, tables without a primary key, and (configurable) public tables with RLS disabled. The "what should I be looking at?" starting point. |
|
|
127
133
|
| `pg_kill` | Cancel a running query or terminate a backend connection. Requires `ALLOW_WRITES=1`. |
|
|
128
134
|
|
|
129
135
|
## Configuration
|
|
@@ -135,10 +141,15 @@ All env vars are read from the MCP server's environment:
|
|
|
135
141
|
| `DATABASE_URL` | (required) | PostgreSQL connection string. |
|
|
136
142
|
| `ALLOW_WRITES` | unset | Set to `1` or `true` to allow DML/DDL via `pg_query` and `pg_explain` ANALYZE of writes. |
|
|
137
143
|
| `POSTGRES_STATEMENT_TIMEOUT_MS` | `30000` | Per-statement timeout. |
|
|
144
|
+
| `POSTGRES_CONNECTION_TIMEOUT_MS` | `10000` | TCP connect timeout. Without this, a dead host hangs until the OS gives up (~2 minutes). |
|
|
138
145
|
| `POSTGRES_MAX_ROWS` | `1000` | Cap on rows returned by `pg_query`. |
|
|
139
146
|
| `POSTGRES_POOL_MAX` | `5` | Max pool connections. Set to `1` for single-threaded backends (pglite-socket, PgBouncer transaction mode). |
|
|
140
147
|
| `POSTGRES_SSL_REJECT_UNAUTHORIZED` | unset | Set to `false` to skip TLS cert verification (for managed DBs using private-CA certs). Connection is still encrypted. |
|
|
141
148
|
|
|
149
|
+
### Supported Postgres versions
|
|
150
|
+
|
|
151
|
+
Tested on **PostgreSQL 17 and 18** in CI. Should work on PG13+ -- a few tools (`pg_replication_status` reading `wal_status`, `pg_top_queries` reading `*_exec_time`) rely on columns that landed in PG13. PG12 and below are out of upstream support and not exercised here.
|
|
152
|
+
|
|
142
153
|
### Connecting to managed Postgres (Supabase, Neon, RDS, etc.)
|
|
143
154
|
|
|
144
155
|
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:
|
|
@@ -174,6 +185,32 @@ This disables certificate chain verification only -- the TCP connection is still
|
|
|
174
185
|
|
|
175
186
|
**First query is slow, subsequent queries are fast** — Expected. The pg driver lazily establishes the first connection; subsequent queries reuse the pool.
|
|
176
187
|
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
Run the full suite (unit + integration) against a real Postgres:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
DATABASE_URL='postgres://user:pass@host:5432/db' POSTGRES_MCP_INTEGRATION=1 npm run test:integration
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The integration suite assumes a disposable database -- it creates and drops a `test_fixture` schema. Don't point it at anything you care about.
|
|
197
|
+
|
|
198
|
+
### Windows: integration tests via WSL2
|
|
199
|
+
|
|
200
|
+
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):
|
|
201
|
+
|
|
202
|
+
```powershell
|
|
203
|
+
wsl --install -d Ubuntu --no-launch
|
|
204
|
+
# reboot, then:
|
|
205
|
+
wsl -d Ubuntu -u root bash -c "apt-get update && apt-get install -y nodejs npm rsync"
|
|
206
|
+
wsl -d Ubuntu -u root bash /mnt/c/path/to/postgres-mcp/scripts/wsl-pg-setup.sh
|
|
207
|
+
wsl -d Ubuntu -u root bash /mnt/c/path/to/postgres-mcp/scripts/wsl-test-matrix.sh
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`wsl-pg-setup.sh` installs PG17 and PG18 from the PGDG apt repo on ports 5432 and 5433, 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`.
|
|
211
|
+
|
|
212
|
+
Tear down when finished: `wsl --unregister Ubuntu`.
|
|
213
|
+
|
|
177
214
|
## License
|
|
178
215
|
|
|
179
216
|
MIT © 2026 YawLabs
|