@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 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+. |
@@ -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
- const env = ["ALLOW_WRITES","DATABASE_URL","NODE_PG_FORCE_NATIVE","PGAPPNAME","PGCONNECT_TIMEOUT","PGSSLMODE","POSTGRES_APPLICATION_NAME","POSTGRES_CONNECTION_TIMEOUT_MS","POSTGRES_MAX_ROWS","POSTGRES_POOL_MAX","POSTGRES_SSL_REJECT_UNAUTHORIZED","POSTGRES_STATEMENT_TIMEOUT_MS","USER","USERNAME"];
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;