@yawlabs/postgres-mcp 0.11.2 → 0.12.0
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 +79 -0
- package/README.md +1 -0
- package/bin/postgres-mcp.mjs +8 -1
- package/dist/index.js +20813 -21401
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-08-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`pg_index_advisor`, an index RECOMMENDATION tool.** The server already had
|
|
15
|
+
every input -- HypoPG for hypothetical indexes, `pg_stat_statements` via
|
|
16
|
+
`pg_top_queries`, and `pg_seq_scan_tables` -- and lacked only the search layer.
|
|
17
|
+
Candidates are costed by creating hypothetical indexes, re-running EXPLAIN, and
|
|
18
|
+
keeping what actually lowers cost, under a caller-set bound on candidates and
|
|
19
|
+
EXPLAIN round trips. Hypothetical indexes are session-scoped, so `hypopg_reset()`
|
|
20
|
+
runs in a teardown that fires even on error; leaking them would poison every
|
|
21
|
+
later plan in the session.
|
|
22
|
+
|
|
23
|
+
Encoded correctness note: PostgreSQL 18's skip scan means a multi-column btree
|
|
24
|
+
with an unconstrained LEADING column can now be used, so the classic "leading
|
|
25
|
+
column never filtered = dead index" heuristic is wrong on PG18+. That reasoning
|
|
26
|
+
is version-gated.
|
|
27
|
+
|
|
28
|
+
- **Structured tool output.** Every tool now declares an `outputSchema` and
|
|
29
|
+
returns `structuredContent` alongside the existing serialized text block.
|
|
30
|
+
Tools that return a bare array wrap it as `{ rows: [...] }`, because
|
|
31
|
+
`structuredContent` must be a JSON object; the `content` block is unchanged, so
|
|
32
|
+
nothing that reads it today breaks. Version-gated fields are modelled as
|
|
33
|
+
OPTIONAL rather than nullable -- absence is a deliberate signal in this codebase
|
|
34
|
+
and a nullable schema would erase the distinction.
|
|
35
|
+
|
|
36
|
+
- **Opt-in audit logging of agent SQL** via `POSTGRES_AUDIT_LOG` /
|
|
37
|
+
`POSTGRES_AUDIT_LOG_FILE` / `POSTGRES_AUDIT_REDACT`, off by default. One JSON
|
|
38
|
+
line per statement: timestamp, tool, SQL, parameter COUNT, duration, rows, and
|
|
39
|
+
ok/SQLSTATE. Parameter VALUES are never logged -- they routinely carry PII and
|
|
40
|
+
credentials. A redacting mode logs only the leading keyword plus a hash for
|
|
41
|
+
operators who want the trail without the content. An unopenable sink fails
|
|
42
|
+
LOUDLY at startup rather than degrading to no trail, since an audit control that
|
|
43
|
+
quietly disables itself is worse than none.
|
|
44
|
+
|
|
45
|
+
- **PostgreSQL 19 forward-compatibility.** PG19 renames
|
|
46
|
+
`pg_stat_subscription_stats.sync_error_count` to `sync_table_error_count` and
|
|
47
|
+
wait event type `BUFFERPIN` to `BUFFER`. Wait-event values are asserted to pass
|
|
48
|
+
through untouched, so a future filter on the old literal -- which would silently
|
|
49
|
+
match nothing on PG19 -- cannot be added without failing a test.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Dual-era MCP protocol support.** The server was legacy-only: it pinned SDK
|
|
54
|
+
v1, which tops out at protocol revision 2025-11-25, and the current revision's
|
|
55
|
+
compatibility matrix states that a modern client talking to a legacy server
|
|
56
|
+
FAILS. It now serves both eras via SDK v2's `serveStdio`. The existing
|
|
57
|
+
process-level tests still drive a real `initialize` handshake against the
|
|
58
|
+
emitted bundle, which is what proves the legacy path still works.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`release.sh` could report a failed release as a success.** The status lived
|
|
63
|
+
in a value a caller could easily discard, and a pipeline returns its LAST
|
|
64
|
+
command's status -- so a release that died at the push step read as exit 0. The
|
|
65
|
+
failure banner is now the final thing written, on both streams, naming the step
|
|
66
|
+
and its remedy. Re-entry is also guarded: a local tag that does not match the
|
|
67
|
+
commit being released now fails loudly instead of building a GitHub release
|
|
68
|
+
from an orphaned tag.
|
|
69
|
+
|
|
70
|
+
- **`wsl-test-matrix.sh` tested against stale dependencies.** It ran `npm ci`
|
|
71
|
+
only when `node_modules` was ABSENT, so a branch that changed a dependency was
|
|
72
|
+
silently tested against the previous branch's tree. It reinstalls on a
|
|
73
|
+
lockfile-hash change, and a failing install aborts instead of proceeding. This
|
|
74
|
+
produced a red matrix on all three majors during this very change set.
|
|
75
|
+
|
|
76
|
+
- **The oam sandbox allowlist dropped the new audit variables.** oam removes an
|
|
77
|
+
undeclared variable rather than denying it, so `POSTGRES_MCP_SANDBOX=1` would
|
|
78
|
+
have silently disabled the audit trail. Caught by the bundle-scanning test
|
|
79
|
+
added in 0.11.0. Also documented that the sandbox denies the filesystem, so a
|
|
80
|
+
FILE audit sink cannot open under it -- use the stderr sink there.
|
|
81
|
+
|
|
82
|
+
- **Integration tests called handlers with arguments no MCP client could send.**
|
|
83
|
+
Several call sites passed a `limit` above the declared schema maximum; direct
|
|
84
|
+
handler calls bypass Zod, so they silently worked and a cap regression would
|
|
85
|
+
not have been caught. Bounds are now respected, and a schema-level test pins
|
|
86
|
+
the cap.
|
|
87
|
+
|
|
88
|
+
|
|
10
89
|
## [0.11.2] - 2026-08-23
|
|
11
90
|
|
|
12
91
|
### Fixed
|
package/README.md
CHANGED
|
@@ -190,6 +190,7 @@ The bigger leverage is multi-tool reasoning. A few real workflows:
|
|
|
190
190
|
| `pg_list_extensions` | List installed extensions (pgvector, postgis, pg_stat_statements, etc.) with versions. |
|
|
191
191
|
| `pg_search_columns` | Find columns by name pattern across all user schemas. Case-insensitive, supports SQL LIKE wildcards. |
|
|
192
192
|
| `pg_explain` | `EXPLAIN` 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](https://github.com/HypoPG/hypopg) extension) lets you ask "what would the plan be with these indexes?" without creating them on disk. |
|
|
193
|
+
| `pg_index_advisor` | Recommend 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](https://github.com/HypoPG/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. |
|
|
193
194
|
| `pg_health` | Server 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. |
|
|
194
195
|
| `pg_top_queries` | Top 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. |
|
|
195
196
|
| `pg_seq_scan_tables` | Tables 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+. |
|
package/bin/postgres-mcp.mjs
CHANGED
|
@@ -190,7 +190,14 @@ function sandboxFlags() {
|
|
|
190
190
|
// getApplicationName() in src/api.ts; PGAPPNAME is pg's own env fallback for
|
|
191
191
|
// the same setting (connection-parameters.js: val('application_name', config,
|
|
192
192
|
// 'PGAPPNAME')), so omitting it would drop a name set the driver's way.
|
|
193
|
-
|
|
193
|
+
// POSTGRES_AUDIT_LOG_FILE is granted as a VARIABLE here, but the sandbox
|
|
194
|
+
// still denies the filesystem, so the file sink cannot actually open its
|
|
195
|
+
// target under POSTGRES_MCP_SANDBOX=1. The audit module fails loudly on an
|
|
196
|
+
// unopenable sink rather than silently dropping the trail, so the combination
|
|
197
|
+
// refuses to start -- which is the correct outcome (an audit control that
|
|
198
|
+
// quietly disables itself is worse than none), but it is a surprising one to
|
|
199
|
+
// hit at runtime. Use the stderr sink under the sandbox.
|
|
200
|
+
const env = ["ALLOW_WRITES","DATABASE_URL","NODE_PG_FORCE_NATIVE","PGAPPNAME","PGCONNECT_TIMEOUT","PGSSLMODE","POSTGRES_APPLICATION_NAME","POSTGRES_AUDIT_LOG","POSTGRES_AUDIT_LOG_FILE","POSTGRES_AUDIT_REDACT","POSTGRES_CONNECTION_TIMEOUT_MS","POSTGRES_MAX_ROWS","POSTGRES_POOL_MAX","POSTGRES_SSL_REJECT_UNAUTHORIZED","POSTGRES_STATEMENT_TIMEOUT_MS","USER","USERNAME"];
|
|
194
201
|
|
|
195
202
|
const flags = ["--permission", netFlag, `--allow-env=${env.join(",")}`];
|
|
196
203
|
return flags;
|