@yawlabs/postgres-mcp 0.10.0 → 0.11.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 +173 -0
- package/README.md +34 -10
- package/bin/postgres-mcp.mjs +9 -1
- package/dist/index.js +1600 -363
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,179 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.0] - 2026-08-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`pg_io_stats`, a new tool for I/O observability.** `pg_stat_io` read /
|
|
15
|
+
write / extend / fsync counts, bytes and times per backend type and context
|
|
16
|
+
(PG16+), plus in-flight async I/O handles from `pg_aios` and the active
|
|
17
|
+
`io_method` (PG18+). Byte accounting differs by server: PG16-17 expose
|
|
18
|
+
`op_bytes` to multiply against operation counts, and PG18 removed it in favour
|
|
19
|
+
of direct `read_bytes` / `write_bytes` / `extend_bytes`. Both branches are
|
|
20
|
+
normalized to one figure, and neither ever names the other's column.
|
|
21
|
+
|
|
22
|
+
- **Server-version gating.** `getServerVersionNum()` in `api.ts` caches
|
|
23
|
+
`server_version_num` per process and every version-dependent column now routes
|
|
24
|
+
through it. A failed probe returns 0, the "assume oldest" sentinel, so an
|
|
25
|
+
unknown server falls through to the conservative query rather than emitting
|
|
26
|
+
SQL referencing a column it may not have. Probe failures are deliberately not
|
|
27
|
+
cached: one transient blip during the first tool call would otherwise pin the
|
|
28
|
+
process to degraded output for its whole lifetime.
|
|
29
|
+
|
|
30
|
+
- **`pg_describe_table` flags generated and identity columns.** `pg_attrdef`
|
|
31
|
+
stores generation expressions alongside plain defaults, so a generated
|
|
32
|
+
column's expression was surfacing as `default_value` with nothing marking it
|
|
33
|
+
-- an agent read that as "optional column, has a default" and wrote an INSERT
|
|
34
|
+
postgres rejects. Identity columns failed the other way: no `pg_attrdef` row
|
|
35
|
+
at all, so they reported `default_value: null, nullable: false` and the agent
|
|
36
|
+
supplied a value into `GENERATED ALWAYS AS IDENTITY`. Columns now carry
|
|
37
|
+
`generated`, `identity`, and `generation_expression`.
|
|
38
|
+
|
|
39
|
+
- **PG18 constraint metadata in `pg_describe_table`.** Constraints carry
|
|
40
|
+
`validated` on every version, plus `enforced` and `has_period` on PG18+. A
|
|
41
|
+
`NOT ENFORCED` foreign key looks present but validates nothing, and a temporal
|
|
42
|
+
`WITHOUT OVERLAPS` primary key previously rendered as an ordinary one. PG18
|
|
43
|
+
also made NOT NULL a real `pg_constraint` row that can be `NOT VALID` -- its
|
|
44
|
+
`attnotnull` docs now read "possibly invalid" -- so `not_null_validated` is
|
|
45
|
+
reported on PG18+, where the question can arise.
|
|
46
|
+
|
|
47
|
+
- **`pg_explain` gained the planner options that actually diagnose a slow
|
|
48
|
+
query**: `buffers`, `settings`, `verbose`, `wal`, `costs`, `timing`, plus
|
|
49
|
+
`generic_plan` (PG16+) for planning a parameterized query with no values, and
|
|
50
|
+
`memory` / `serialize` (PG17+). Options below the server's version are
|
|
51
|
+
rejected up front by name and required major, rather than as a parse error
|
|
52
|
+
from the server.
|
|
53
|
+
|
|
54
|
+
- **`pg_health` reports what a health check needs.** `max_connections` and a
|
|
55
|
+
used-fraction (so a connection count is readable), `wait_event_type` /
|
|
56
|
+
`wait_event` / `backend_type` / transaction age on active queries, and a
|
|
57
|
+
`pg_stat_database` rollup: deadlocks, temp files and bytes, conflicts, cache
|
|
58
|
+
hit ratio, `stats_reset`.
|
|
59
|
+
|
|
60
|
+
- **`pg_advisor` checks wraparound risk**, the classic pageable incident:
|
|
61
|
+
per-database and per-table `age(relfrozenxid)` measured against
|
|
62
|
+
`autovacuum_freeze_max_age`, with a `wraparoundThreshold` parameter mirroring
|
|
63
|
+
the existing sequence-exhaustion one. Freeze coverage via `relallfrozen` on
|
|
64
|
+
PG18+.
|
|
65
|
+
|
|
66
|
+
Multixact wraparound is checked alongside it, on the same rows and with no
|
|
67
|
+
extra round trip: `mxid_age(relminmxid)` against
|
|
68
|
+
`autovacuum_multixact_freeze_max_age`. Multixacts are consumed by row-level
|
|
69
|
+
locking (`SELECT ... FOR SHARE/UPDATE`, foreign-key checks), so a lock-heavy
|
|
70
|
+
workload can exhaust them while `relfrozenxid` still looks perfectly healthy
|
|
71
|
+
-- checking only xids reports such a cluster as clean. A row is flagged when
|
|
72
|
+
EITHER counter crosses the threshold, and `triggered_by` (`xid` /
|
|
73
|
+
`multixact` / `both`) says which, because the remediation differs.
|
|
74
|
+
|
|
75
|
+
- **`pg_health` connection buckets now reconcile against `total`.** Two
|
|
76
|
+
separate ways the old breakdown lied. First, `pg_stat_activity` does not hide
|
|
77
|
+
other users' sessions from an unprivileged role -- it returns the rows with
|
|
78
|
+
`state` NULL -- so `total` was complete while `active` / `idle` counted only
|
|
79
|
+
the caller's own sessions, and an operator could read `active: 0` on a busy
|
|
80
|
+
database. A `state_unavailable` counter now makes that shortfall visible.
|
|
81
|
+
Second, `idle in transaction (aborted)`, `starting`, `fastpath function
|
|
82
|
+
call`, and `disabled` matched no filter and vanished from the breakdown; the
|
|
83
|
+
aborted state is the one that holds locks and blocks vacuum, so it gets its
|
|
84
|
+
own field, and an `other` catch-all absorbs the rest (a `NOT IN` list, so a
|
|
85
|
+
future major's new state cannot silently disappear again).
|
|
86
|
+
|
|
87
|
+
- **`POSTGRES_APPLICATION_NAME`** (default `postgres-mcp`), so agent traffic is
|
|
88
|
+
identifiable in `pg_stat_activity` instead of anonymous -- while `pg_health`
|
|
89
|
+
itself reports `application_name` for every other session. An
|
|
90
|
+
`application_name` in `DATABASE_URL` still wins.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- **BREAKING: `pg_seq_scan_tables` and `pg_unused_indexes` return an envelope,
|
|
95
|
+
not a bare row array.** `data` is now
|
|
96
|
+
`{rows, stats_reset, stats_reset_age_seconds}`; callers read `data.rows`.
|
|
97
|
+
The reason is `pg_unused_indexes` could tell an agent to drop a load-bearing
|
|
98
|
+
index: a scan count is meaningless without knowing when the counters were
|
|
99
|
+
reset, and if that happened an hour ago every index looks unused. Both tools
|
|
100
|
+
also report `last_idx_scan` / `last_seq_scan` on PG16+, where "not scanned
|
|
101
|
+
since March" beats a bare counter.
|
|
102
|
+
|
|
103
|
+
- **BREAKING: `pg_top_queries` returns the same envelope**, for the same
|
|
104
|
+
reason -- it ranks cumulative `total_exec_time` / `calls`. Its clock is NOT
|
|
105
|
+
`pg_stat_database.stats_reset` but `pg_stat_statements_info.stats_reset`, a
|
|
106
|
+
genuinely independent reset point; using the wrong one would have been worse
|
|
107
|
+
than omitting it. The same view supplies `dealloc`, which is the subtler
|
|
108
|
+
trap: it counts how often entries for the least-executed statements were
|
|
109
|
+
evicted for exceeding `pg_stat_statements.max`, so a non-zero value means
|
|
110
|
+
the "top queries" ranking is drawn from an incomplete population. Both
|
|
111
|
+
require extension 1.9 (PostgreSQL 14) and are omitted below it rather than
|
|
112
|
+
returned as nulls that would read as "never reset".
|
|
113
|
+
|
|
114
|
+
- **BREAKING: `pg_explain` with `analyze: true` now emits `BUFFERS`.** PG18
|
|
115
|
+
turns it on by default server-side; on PG15-17 it had to be requested and was
|
|
116
|
+
absent. Plans gain buffer lines, so text plans roughly double in length and
|
|
117
|
+
`POSTGRES_MAX_ROWS` truncation can fire where it previously did not. Pass
|
|
118
|
+
`buffers: false` for the old output.
|
|
119
|
+
|
|
120
|
+
- **BREAKING: the Node floor is now 22.** Node 20 reached end of life; the
|
|
121
|
+
supported lines are 22, 24 and 26. The esbuild target deliberately stays at
|
|
122
|
+
`node20` -- it only controls syntax downleveling, so a lower floor keeps the
|
|
123
|
+
bundle runnable under alternate runtimes.
|
|
124
|
+
|
|
125
|
+
- **Tools register through `registerTool` instead of `server.tool`.** All six
|
|
126
|
+
`server.tool` overloads are deprecated as of SDK 1.30 and are gone in the v2
|
|
127
|
+
packages. `registerTool` is also the only form that can carry `outputSchema`,
|
|
128
|
+
so this is what makes structured tool output reachable later. Tools now
|
|
129
|
+
advertise a top-level `title` as well as `annotations.title`; both are emitted,
|
|
130
|
+
since dropping either regresses hosts that read only one.
|
|
131
|
+
|
|
132
|
+
- **`@modelcontextprotocol/sdk` 1.29.0 -> 1.30.0** (the final v1.x release) and
|
|
133
|
+
**`pg` ^8.14.0 -> ^8.23.0**. The pg bump makes `sslnegotiation=direct`
|
|
134
|
+
available in `DATABASE_URL`, which skips a round trip against PG17+ servers;
|
|
135
|
+
it stays opt-in because a PG16-or-older server rejects the connection.
|
|
136
|
+
|
|
137
|
+
- **Documented where PostgreSQL support actually sits.** PG13 reached end of
|
|
138
|
+
life on 2025-11-13 and PG14 does so on 2026-11-12. The integration matrix
|
|
139
|
+
remains 15 / 17 / 18.
|
|
140
|
+
|
|
141
|
+
### Fixed
|
|
142
|
+
|
|
143
|
+
- **`POSTGRES_APPLICATION_NAME` was missing from the oam sandbox allowlist.**
|
|
144
|
+
Under `POSTGRES_MCP_SANDBOX=1`, oam removes an undeclared variable from
|
|
145
|
+
`process.env` rather than denying access, so the operator's configured name
|
|
146
|
+
would have silently vanished and the server would have reported the default
|
|
147
|
+
-- precisely the silent-misbehaviour failure the launcher's own comment warns
|
|
148
|
+
about. `PGAPPNAME` is now granted too, since the pg driver reads it as its
|
|
149
|
+
own fallback for the same setting. A new test scans the shipped bundle for
|
|
150
|
+
literal `process.env` reads and fails if any config variable is absent from
|
|
151
|
+
the allowlist, so the next one cannot ship silently.
|
|
152
|
+
|
|
153
|
+
- **`pg_io_stats` had no test coverage at all.** `tools.test.ts` builds its own
|
|
154
|
+
`allTools` array separate from `index.ts`, and the new tool was added to one
|
|
155
|
+
and not the other -- exempting it from every structural check, including the
|
|
156
|
+
duplicate-name guard. Both arrays now agree, and the tool has a unit suite
|
|
157
|
+
covering its version branches.
|
|
158
|
+
|
|
159
|
+
- **A version probe suspended across `shutdown()` could republish a stale
|
|
160
|
+
server version.** `shutdown()` clears the cache, but a probe already awaiting
|
|
161
|
+
its query would resolve afterwards and write the OLD server's version into
|
|
162
|
+
the cache the NEW pool uses -- gating catalog queries against the wrong
|
|
163
|
+
server, the exact failure the reset exists to prevent. A generation counter
|
|
164
|
+
now invalidates in-flight probes, matching the guard `resolveTypeNames`
|
|
165
|
+
already had.
|
|
166
|
+
|
|
167
|
+
- **Four `pg_explain` tests asserted the wrong thing whenever `DATABASE_URL`
|
|
168
|
+
was set.** Their helper cleared the env var to force the version probe's
|
|
169
|
+
"unknown" sentinel, but the pool and the version cache are module-scoped and
|
|
170
|
+
survive that, so on any machine with `DATABASE_URL` exported the probe never
|
|
171
|
+
re-ran. The suite passed or failed depending on the developer's environment.
|
|
172
|
+
The helper now calls `shutdown()` on both edges.
|
|
173
|
+
|
|
174
|
+
### Security
|
|
175
|
+
|
|
176
|
+
- **`fast-uri` bumped past the host-confusion advisory (GHSA, high).** It
|
|
177
|
+
reaches the published artifact rather than staying a build-time concern: the
|
|
178
|
+
MCP SDK depends on `ajv`, which depends on `fast-uri`, and esbuild bundles the
|
|
179
|
+
whole graph into `dist/index.js`. "It is only a devDependency" is not the
|
|
180
|
+
right test for this package -- the bundle is what ships, and the dependency
|
|
181
|
+
tree is flattened into it. `npm audit` now reports zero vulnerabilities.
|
|
182
|
+
|
|
10
183
|
## [0.10.0] - 2026-08-08
|
|
11
184
|
|
|
12
185
|
### Added
|
package/README.md
CHANGED
|
@@ -167,22 +167,23 @@ The bigger leverage is multi-tool reasoning. A few real workflows:
|
|
|
167
167
|
| `pg_query` | Run 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`. |
|
|
168
168
|
| `pg_list_schemas` | List non-system schemas. |
|
|
169
169
|
| `pg_list_tables` | List tables (and optionally views) in a schema with estimated row counts. Paginated via `limit`/`offset`. |
|
|
170
|
-
| `pg_describe_table` | Kind, columns, PK, outgoing FKs, incoming FKs (`referenced_by`), CHECK / UNIQUE / EXCLUDE constraints, indexes, and partition parent/children for a relation. |
|
|
170
|
+
| `pg_describe_table` | Kind, 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+. |
|
|
171
171
|
| `pg_list_views` | List views and materialized views in a schema, including their SQL definitions. |
|
|
172
172
|
| `pg_list_functions` | List functions, procedures, and aggregates in a schema with signatures and return types. |
|
|
173
173
|
| `pg_list_extensions` | List installed extensions (pgvector, postgis, pg_stat_statements, etc.) with versions. |
|
|
174
174
|
| `pg_search_columns` | Find columns by name pattern across all user schemas. Case-insensitive, supports SQL LIKE wildcards. |
|
|
175
|
-
| `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. |
|
|
176
|
-
| `pg_health` | Server version, database size,
|
|
177
|
-
| `pg_top_queries` | Top N queries by total/mean execution time. Requires the `pg_stat_statements` extension. |
|
|
178
|
-
| `pg_seq_scan_tables` | Tables with heavy sequential scans - missing-index candidates. |
|
|
179
|
-
| `pg_unused_indexes` | Non-unique, non-primary indexes with low scan counts - drop candidates. |
|
|
175
|
+
| `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. |
|
|
176
|
+
| `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. |
|
|
177
|
+
| `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. |
|
|
178
|
+
| `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+. |
|
|
179
|
+
| `pg_unused_indexes` | Non-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+. |
|
|
180
|
+
| `pg_io_stats` | I/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+). |
|
|
180
181
|
| `pg_inspect_locks` | Who is blocking whom right now (blocked PID, blocker PID, lock type, queries). |
|
|
181
182
|
| `pg_list_roles` | Database roles with login/superuser/createdb flags and group memberships. |
|
|
182
183
|
| `pg_table_privileges` | Who has SELECT/INSERT/UPDATE/DELETE/etc. on a table or whole schema. |
|
|
183
184
|
| `pg_table_bloat` | Tables with high dead-tuple ratios - VACUUM candidates. |
|
|
184
185
|
| `pg_replication_status` | Replication slots, connected replicas, and current WAL position. |
|
|
185
|
-
| `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. |
|
|
186
|
+
| `pg_advisor` | Rolled-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. |
|
|
186
187
|
| `pg_kill` | Cancel a running query or terminate a backend connection. Requires `ALLOW_WRITES=1`. |
|
|
187
188
|
|
|
188
189
|
## Configuration
|
|
@@ -198,12 +199,25 @@ All env vars are read from the MCP server's environment:
|
|
|
198
199
|
| `POSTGRES_MAX_ROWS` | `1000` | Cap on rows returned by `pg_query`. |
|
|
199
200
|
| `POSTGRES_POOL_MAX` | `5` | Max pool connections. Set to `1` for single-threaded backends (pglite-socket, PgBouncer transaction mode). |
|
|
200
201
|
| `POSTGRES_SSL_REJECT_UNAUTHORIZED` | unset | Set to `false` to skip TLS cert verification (for managed DBs using private-CA certs). Connection is still encrypted. |
|
|
202
|
+
| `POSTGRES_APPLICATION_NAME` | `postgres-mcp` | Value 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. |
|
|
201
203
|
| `POSTGRES_MCP_RUNTIME` | `auto` | Which JS runtime executes the server: `auto` (prefer [oam](https://oamjs.org), fall back to Node), `oam` (require oam, fail if absent), `node` (never use oam). See [Runtime](#runtime). |
|
|
202
204
|
| `OAM_BIN` | unset | Explicit path to an `oam` binary, checked before PATH and the default install locations. |
|
|
203
205
|
|
|
204
206
|
### Supported Postgres versions
|
|
205
207
|
|
|
206
|
-
Tested on **PostgreSQL 15, 17 and 18** in the integration matrix.
|
|
208
|
+
Tested on **PostgreSQL 15, 17 and 18** in the integration matrix.
|
|
209
|
+
|
|
210
|
+
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`).
|
|
211
|
+
|
|
212
|
+
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:
|
|
213
|
+
|
|
214
|
+
| Server | What it adds |
|
|
215
|
+
|--------|--------------|
|
|
216
|
+
| PG16+ | `last_idx_scan` / `last_seq_scan` in the stats tools (index/table staleness rather than a bare counter), `pg_explain` `generic_plan` |
|
|
217
|
+
| PG17+ | `pg_explain` `memory` and `serialize` |
|
|
218
|
+
| 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 |
|
|
219
|
+
|
|
220
|
+
If the version probe fails, the server assumes the oldest supported shape rather than emitting SQL a server might reject.
|
|
207
221
|
|
|
208
222
|
### Runtime
|
|
209
223
|
|
|
@@ -211,7 +225,7 @@ The published `postgres-mcp` command is a small launcher that prefers the [oam](
|
|
|
211
225
|
|
|
212
226
|
**If you do not have oam, nothing changes.** 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.
|
|
213
227
|
|
|
214
|
-
**If you do have oam,** the server runs under it. Verified equivalent on both runtimes: all
|
|
228
|
+
**If you do have oam,** the server runs under it. Verified equivalent on both runtimes: all 22 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).
|
|
215
229
|
|
|
216
230
|
**Startup cost, measured.** windows-arm64, 1.4 MB bundle, `postgres-mcp version` (full module init), every binary warmed first, mean of 12 runs:
|
|
217
231
|
|
|
@@ -263,6 +277,14 @@ To allow the connection while keeping traffic encrypted, add `POSTGRES_SSL_REJEC
|
|
|
263
277
|
|
|
264
278
|
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.
|
|
265
279
|
|
|
280
|
+
**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`:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
postgres://user:pass@host:5432/db?sslmode=require&sslnegotiation=direct
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
266
288
|
## Troubleshooting
|
|
267
289
|
|
|
268
290
|
**`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`.
|
|
@@ -307,7 +329,9 @@ wsl -d Ubuntu -u root bash /mnt/c/path/to/postgres-mcp/scripts/wsl-pg-setup.sh
|
|
|
307
329
|
wsl -d Ubuntu -u root bash /mnt/c/path/to/postgres-mcp/scripts/wsl-test-matrix.sh
|
|
308
330
|
```
|
|
309
331
|
|
|
310
|
-
`wsl-pg-setup.sh` installs PG17 and PG18 from the PGDG apt repo on
|
|
332
|
+
`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`.
|
|
333
|
+
|
|
334
|
+
**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.
|
|
311
335
|
|
|
312
336
|
Tear down when finished: `wsl --unregister Ubuntu`.
|
|
313
337
|
|
package/bin/postgres-mcp.mjs
CHANGED
|
@@ -179,7 +179,15 @@ function sandboxFlags() {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
// Every variable the SHIPPED BUNDLE reads, including the pg driver's own
|
|
183
|
+
// lookups. Adding a config env var to src/ without adding it here is a silent
|
|
184
|
+
// regression under the sandbox, not a loud one: oam removes an undeclared var
|
|
185
|
+
// from process.env rather than denying access, so the server reads undefined
|
|
186
|
+
// and quietly takes its default. POSTGRES_APPLICATION_NAME is read by
|
|
187
|
+
// getApplicationName() in src/api.ts; PGAPPNAME is pg's own env fallback for
|
|
188
|
+
// the same setting (connection-parameters.js: val('application_name', config,
|
|
189
|
+
// 'PGAPPNAME')), so omitting it would drop a name set the driver's way.
|
|
190
|
+
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"];
|
|
183
191
|
|
|
184
192
|
const flags = ["--permission", netFlag, `--allow-env=${env.join(",")}`];
|
|
185
193
|
return flags;
|