@yawlabs/postgres-mcp 0.9.0 → 0.10.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 +108 -5
- package/README.md +15 -1
- package/bin/postgres-mcp.mjs +133 -8
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-08-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **An opt-in `--permission` sandbox under oam**, via `POSTGRES_MCP_SANDBOX=1`.
|
|
15
|
+
The network grant is derived from `DATABASE_URL` at launch rather than
|
|
16
|
+
hardcoded, so the one endpoint the server may reach is the one it was
|
|
17
|
+
configured to reach. Host and port are both pinned, because oam matches grants
|
|
18
|
+
by prefix and a bare host would also admit every other port on it. Filesystem
|
|
19
|
+
and child-process are denied outright.
|
|
20
|
+
|
|
21
|
+
Opt-in rather than default because a wrong grant does not fail loudly: oam
|
|
22
|
+
denies a non-granted environment variable by making it **absent** from
|
|
23
|
+
`process.env` rather than throwing, so an under-granted `DATABASE_URL` would
|
|
24
|
+
read as "not configured" instead of "denied". The environment allow-list is
|
|
25
|
+
derived from what the shipped bundle actually reads, which is why it includes
|
|
26
|
+
the pg driver's own lookups (`PGSSLMODE`, `PGCONNECT_TIMEOUT` and friends) that
|
|
27
|
+
a hand-written list would have missed.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **oam 0.9.0 is now the minimum**, enforced in `bin/postgres-mcp.mjs`. Older
|
|
32
|
+
releases ran `child_process.execFile` arguments through a shell, accepted
|
|
33
|
+
`exec`'s `timeout` and ignored it, truncated `spawnSync` at `maxBuffer` while
|
|
34
|
+
reporting success, and treated `stdio: 'inherit'` as `'pipe'`. This server
|
|
35
|
+
spawns nothing, so the floor is enforced for consistency across
|
|
36
|
+
`@yawlabs/*-mcp` rather than because this launcher was exposed. An older oam is
|
|
37
|
+
not an error: the launcher falls back to Node and says so on stderr, and
|
|
38
|
+
`POSTGRES_MCP_RUNTIME=oam` turns that into a hard error.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`release.sh` aborted instead of releasing when `[Unreleased]` was empty.**
|
|
43
|
+
The body extraction pipes through `grep -v` to drop blank lines, and `grep`
|
|
44
|
+
exits non-zero when it matches nothing — so under `set -e` an empty section
|
|
45
|
+
killed the script at that line, and the `warn` branch written to handle
|
|
46
|
+
exactly that case could never run.
|
|
47
|
+
|
|
48
|
+
## [0.9.1] - 2026-08-07
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **Corrected the runtime startup figures published in 0.9.0.** They were wrong
|
|
53
|
+
in both magnitude and direction, and the README used them to advise opting
|
|
54
|
+
out of the faster runtime.
|
|
55
|
+
|
|
56
|
+
The 0.9.0 numbers (Node ~650-900ms, oam ~980-1290ms, launcher-to-oam ~1.8s)
|
|
57
|
+
were measured against cold, freshly-built binaries. On Windows a binary that
|
|
58
|
+
is not in the on-access scanner's cache is rescanned on every exec, while
|
|
59
|
+
`node` resolved from PATH was cached long ago -- so the comparison measured
|
|
60
|
+
the scanner and put the entire penalty on the binary under test.
|
|
61
|
+
|
|
62
|
+
Re-measured on the same hardware with every binary warmed first, mean of 12
|
|
63
|
+
runs, `postgres-mcp version` (full module init):
|
|
64
|
+
|
|
65
|
+
| path | startup |
|
|
66
|
+
|---|---|
|
|
67
|
+
| standalone binary (`oam compile`) | 298ms |
|
|
68
|
+
| `oam run dist/index.js` | 306ms |
|
|
69
|
+
| `node dist/index.js` | 358ms |
|
|
70
|
+
| launcher -> Node (in-process) | 370ms |
|
|
71
|
+
| launcher -> oam (spawn) | 409ms |
|
|
72
|
+
|
|
73
|
+
oam starts faster than Node. What the launcher costs is the spawn: reaching
|
|
74
|
+
oam means Node has already booted, and that ~100ms hop outweighs oam's ~52ms
|
|
75
|
+
advantage, so the two land within ~40ms of each other through the npm `bin`.
|
|
76
|
+
`POSTGRES_MCP_RUNTIME=node` remains available but is now a marginal
|
|
77
|
+
difference, not the meaningful one 0.9.0 described.
|
|
78
|
+
|
|
79
|
+
No behavior changed -- `auto` (prefer oam) was and remains the default, and
|
|
80
|
+
it was the right default for the wrong stated reason. README, CHANGELOG, and
|
|
81
|
+
the launcher's header comment are corrected; the launcher comment also
|
|
82
|
+
records the measurement trap so the mistake is not repeated.
|
|
83
|
+
|
|
84
|
+
## [0.9.0] - 2026-08-07
|
|
85
|
+
|
|
10
86
|
### Added
|
|
11
87
|
|
|
12
88
|
- The `postgres-mcp` command is now a runtime launcher (`bin/postgres-mcp.mjs`)
|
|
@@ -31,11 +107,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
31
107
|
a one-time cost per MCP session rather than per tool call, but it is a real
|
|
32
108
|
regression against plain Node -- `POSTGRES_MCP_RUNTIME=node` opts out.
|
|
33
109
|
|
|
34
|
-
> **
|
|
35
|
-
>
|
|
36
|
-
>
|
|
37
|
-
>
|
|
38
|
-
|
|
110
|
+
> **These figures are wrong. See 0.9.1.** They were measured against cold,
|
|
111
|
+
> freshly-built binaries and reflect the Windows on-access virus scanner, not
|
|
112
|
+
> either runtime. oam is in fact faster than Node here. Left in place rather
|
|
113
|
+
> than rewritten so the correction has something to point at.
|
|
114
|
+
|
|
115
|
+
## [0.8.0] - 2026-08-07
|
|
39
116
|
|
|
40
117
|
### Changed (breaking)
|
|
41
118
|
|
|
@@ -130,6 +207,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
130
207
|
(PG17), and with only 17/18 in the matrix the pre-1.11 branch was never
|
|
131
208
|
selected.
|
|
132
209
|
|
|
210
|
+
## [0.7.0] - 2026-07-21
|
|
211
|
+
|
|
212
|
+
Backfilled from `git log v0.6.20..v0.7.0` -- this release shipped without a
|
|
213
|
+
CHANGELOG entry. Summarized from commit subjects rather than re-derived from
|
|
214
|
+
the diff, so it is less detailed than the entries around it.
|
|
215
|
+
|
|
216
|
+
### Added
|
|
217
|
+
|
|
218
|
+
- `pg_table_bloat`: `approx` and `exact` methods via `pgstattuple` (#19).
|
|
219
|
+
- `pg_top_queries`: `io_read_time_ms` / `io_write_time_ms` on
|
|
220
|
+
`pg_stat_statements` >= 1.10 (#16).
|
|
221
|
+
- Cross-platform single-binary release pipeline (Scoop + Homebrew), released
|
|
222
|
+
as 0.6.21.
|
|
223
|
+
|
|
224
|
+
### Fixed
|
|
225
|
+
|
|
226
|
+
- `pg_list_tables`: cast `reltuples` to `float8` so `estimated_rows` is a
|
|
227
|
+
number rather than a string (#17).
|
|
228
|
+
- 10 confirmed findings from a review of `f7b7cb3..a736200` (#21).
|
|
229
|
+
|
|
230
|
+
### Removed
|
|
231
|
+
|
|
232
|
+
- GitHub Actions workflows and the dependabot config (#24). This is why the
|
|
233
|
+
repo has no `.github/` directory and why `release.sh` is the only release
|
|
234
|
+
path; several stale comments referring to CI survived until 0.8.0.
|
|
235
|
+
|
|
133
236
|
## [0.6.20] - 2026-06-04
|
|
134
237
|
|
|
135
238
|
### Fixed
|
package/README.md
CHANGED
|
@@ -213,7 +213,21 @@ The published `postgres-mcp` command is a small launcher that prefers the [oam](
|
|
|
213
213
|
|
|
214
214
|
**If you do have oam,** the server runs under it. Verified equivalent on both runtimes: all 21 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
215
|
|
|
216
|
-
**
|
|
216
|
+
**Startup cost, measured.** windows-arm64, 1.4 MB bundle, `postgres-mcp version` (full module init), every binary warmed first, mean of 12 runs:
|
|
217
|
+
|
|
218
|
+
| path | startup |
|
|
219
|
+
|---|---|
|
|
220
|
+
| standalone binary (`oam compile`) | 298ms |
|
|
221
|
+
| `oam run dist/index.js` | 306ms |
|
|
222
|
+
| `node dist/index.js` | 358ms |
|
|
223
|
+
| launcher -> Node (in-process) | 370ms |
|
|
224
|
+
| launcher -> oam (spawn) | 409ms |
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
> 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.
|
|
217
231
|
|
|
218
232
|
```jsonc
|
|
219
233
|
{
|
package/bin/postgres-mcp.mjs
CHANGED
|
@@ -15,26 +15,66 @@
|
|
|
15
15
|
* resolving a few paths (a handful of `existsSync` calls, no subprocess).
|
|
16
16
|
*
|
|
17
17
|
* WHAT THE OAM PATH COSTS
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ~
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* The spawn, not the runtime. windows-arm64, 1.4 MB bundle, warmed binaries,
|
|
19
|
+
* mean of 12 runs: oam run 306ms, node 358ms -- oam is the FASTER of the two.
|
|
20
|
+
* But reaching oam from here means node has already booted, and that hop
|
|
21
|
+
* (~100ms) outweighs oam's ~52ms advantage: launcher -> node 370ms,
|
|
22
|
+
* launcher -> oam 409ms. So the two land within ~40ms through the npm bin.
|
|
23
|
+
* A one-time cost per MCP session either way, not per tool call.
|
|
24
|
+
*
|
|
25
|
+
* The `oam compile` standalone binary sidesteps this entirely (298ms, no
|
|
26
|
+
* launcher, no spawn) and is the right answer if startup actually matters.
|
|
27
|
+
*
|
|
28
|
+
* DO NOT re-measure this by timing a freshly built binary. On Windows a
|
|
29
|
+
* binary that is not in the on-access scanner's cache gets rescanned on every
|
|
30
|
+
* exec, while `node` from PATH was cached long ago -- the comparison then
|
|
31
|
+
* measures the scanner and dumps the whole penalty on the new binary. That
|
|
32
|
+
* mistake produced the numbers published in 0.9.0 (node ~650-900ms, oam
|
|
33
|
+
* ~980-1290ms), which were wrong in both magnitude and direction. Warm every
|
|
34
|
+
* candidate first, or stage it out of the build directory.
|
|
35
|
+
*
|
|
36
|
+
* THE `--permission` SANDBOX (oam 0.9.0+, opt-in)
|
|
37
|
+
* `POSTGRES_MCP_SANDBOX=1` runs the server under oam's permission model.
|
|
38
|
+
*
|
|
39
|
+
* The database host is not knowable ahead of time, so the net grant is DERIVED
|
|
40
|
+
* from DATABASE_URL at launch: the one endpoint this server may reach is the one
|
|
41
|
+
* it was configured to reach. Both host and port are pinned, because grants are
|
|
42
|
+
* prefix-matched and a bare host would also admit every other port on it.
|
|
43
|
+
* Filesystem and child-process stay denied.
|
|
44
|
+
*
|
|
45
|
+
* Opt-in, not default. A denied environment variable is ABSENT from process.env
|
|
46
|
+
* rather than throwing, so an under-granted DATABASE_URL would look like "not
|
|
47
|
+
* configured" instead of "denied". The env list is derived from the shipped
|
|
48
|
+
* bundle and includes the pg driver's own reads (PGSSLMODE, PGCONNECT_TIMEOUT
|
|
49
|
+
* and friends) -- a hand-written list misses those.
|
|
50
|
+
*
|
|
51
|
+
* MINIMUM OAM VERSION
|
|
52
|
+
* 0.9.0. Below it `child_process.execFile` ran its arguments through a SHELL,
|
|
53
|
+
* `exec` accepted `timeout` and ignored it, `spawnSync` truncated at
|
|
54
|
+
* `maxBuffer` while reporting success, and `stdio: 'inherit'`/`'ignore'` both
|
|
55
|
+
* behaved as `'pipe'`. This server spawns nothing, so the floor is
|
|
56
|
+
* enforced for consistency across @yawlabs/*-mcp rather than because this
|
|
57
|
+
* launcher was exposed.
|
|
58
|
+
* An older oam is not an error: the launcher falls back to Node and says so on
|
|
59
|
+
* stderr. Pinning the floor here is what makes that fallback automatic.
|
|
24
60
|
*
|
|
25
61
|
* SELECTION
|
|
26
62
|
* POSTGRES_MCP_RUNTIME=oam require oam; fail loudly if it is missing
|
|
27
63
|
* POSTGRES_MCP_RUNTIME=node never use oam
|
|
28
64
|
* POSTGRES_MCP_RUNTIME=auto prefer oam, silently fall back (default)
|
|
65
|
+
* POSTGRES_MCP_SANDBOX=1 run oam under --permission (oam 0.9.0+)
|
|
29
66
|
* OAM_BIN=/path/to/oam explicit binary, checked before any discovery
|
|
30
67
|
*/
|
|
31
68
|
|
|
32
|
-
import { spawn } from "node:child_process";
|
|
69
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
33
70
|
import { existsSync } from "node:fs";
|
|
34
71
|
import { constants, homedir } from "node:os";
|
|
35
72
|
import { delimiter, join } from "node:path";
|
|
36
73
|
import { fileURLToPath } from "node:url";
|
|
37
74
|
|
|
75
|
+
/** Oldest oam whose `child_process` matches Node. See MINIMUM OAM VERSION above. */
|
|
76
|
+
const OAM_MIN = [0, 9, 0];
|
|
77
|
+
|
|
38
78
|
// Two forms, deliberately. `import()` on Windows REJECTS a bare `C:\...` path
|
|
39
79
|
// with ERR_UNSUPPORTED_ESM_URL_SCHEME (it reads `c:` as a protocol), so the
|
|
40
80
|
// in-process fallback must use the file:// URL. spawn(), conversely, needs a
|
|
@@ -79,6 +119,72 @@ function findOam() {
|
|
|
79
119
|
return null;
|
|
80
120
|
}
|
|
81
121
|
|
|
122
|
+
/**
|
|
123
|
+
* `oam --version` -> [major, minor, patch], or null when it cannot be read.
|
|
124
|
+
* A pre-release suffix (0.9.0-rc.1) truncates to its base version.
|
|
125
|
+
*/
|
|
126
|
+
function oamVersion(cmd) {
|
|
127
|
+
try {
|
|
128
|
+
const out = execFileSync(cmd, ["--version"], {
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
131
|
+
});
|
|
132
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(out);
|
|
133
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
134
|
+
} catch {
|
|
135
|
+
// Not executable, wrong arch, or deleted since the stat. Caller degrades.
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** True when `v` is at least `min`, comparing major/minor/patch in order. */
|
|
141
|
+
function atLeast(v, min) {
|
|
142
|
+
if (!v) return false;
|
|
143
|
+
for (let i = 0; i < min.length; i++) {
|
|
144
|
+
if (v[i] > min[i]) return true;
|
|
145
|
+
if (v[i] < min[i]) return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The `--permission` grant list, or [] when the sandbox is not requested.
|
|
152
|
+
*
|
|
153
|
+
* These are oam's PROCESS-level flags: they belong before the `run` subcommand,
|
|
154
|
+
* not after it. `oam run --permission file.js` is rejected outright, which is a
|
|
155
|
+
* good failure but only because it is loud -- ordering here is load-bearing.
|
|
156
|
+
*
|
|
157
|
+
* Net grants prefix-match `host` for fetch and `host:port` for sockets.
|
|
158
|
+
* A denied environment variable is ABSENT from process.env rather than throwing,
|
|
159
|
+
* so the env list below is derived from what the bundle actually reads; trimming
|
|
160
|
+
* it produces silent misbehaviour, not a clear denial.
|
|
161
|
+
*/
|
|
162
|
+
function sandboxFlags() {
|
|
163
|
+
if (process.env.POSTGRES_MCP_SANDBOX !== "1") return [];
|
|
164
|
+
|
|
165
|
+
// Derived, not hardcoded: the only endpoint this server may reach is the one
|
|
166
|
+
// it was configured to reach. Grants are prefix-matched against "host:port"
|
|
167
|
+
// for sockets, so host alone would also admit any other port on that host --
|
|
168
|
+
// pin both. A DSN we cannot parse falls back to a bare grant rather than a
|
|
169
|
+
// broken one, because a wrong narrow grant fails at connect time.
|
|
170
|
+
const dsn = process.env.DATABASE_URL ?? null;
|
|
171
|
+
let netFlag = "--allow-net";
|
|
172
|
+
if (dsn) {
|
|
173
|
+
try {
|
|
174
|
+
const u = new URL(dsn);
|
|
175
|
+
if (u.hostname) netFlag = `--allow-net=${u.hostname}:${u.port || 5432}`;
|
|
176
|
+
} catch {
|
|
177
|
+
// Unparseable DATABASE_URL: leave the grant open. The server will fail on
|
|
178
|
+
// its own connection error, which names the real problem.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const env = ["ALLOW_WRITES","DATABASE_URL","NODE_PG_FORCE_NATIVE","PGCONNECT_TIMEOUT","PGSSLMODE","POSTGRES_CONNECTION_TIMEOUT_MS","POSTGRES_MAX_ROWS","POSTGRES_POOL_MAX","POSTGRES_SSL_REJECT_UNAUTHORIZED","POSTGRES_STATEMENT_TIMEOUT_MS","USER","USERNAME"];
|
|
183
|
+
|
|
184
|
+
const flags = ["--permission", netFlag, `--allow-env=${env.join(",")}`];
|
|
185
|
+
return flags;
|
|
186
|
+
}
|
|
187
|
+
|
|
82
188
|
/** Run the server in THIS process. The zero-overhead fallback. */
|
|
83
189
|
async function runInProcess() {
|
|
84
190
|
await import(SERVER_URL.href);
|
|
@@ -105,11 +211,30 @@ if (mode === "node") {
|
|
|
105
211
|
process.exit(1);
|
|
106
212
|
}
|
|
107
213
|
await runInProcess();
|
|
214
|
+
} else if (!atLeast(oamVersion(oam), OAM_MIN)) {
|
|
215
|
+
// Discovery itself stays stat-only; this is the first subprocess, and it
|
|
216
|
+
// runs only once we have already decided to spawn oam anyway. Measured 26ms
|
|
217
|
+
// median (n=12, windows-arm64), paid once per MCP session.
|
|
218
|
+
const min = OAM_MIN.join(".");
|
|
219
|
+
if (mode === "oam") {
|
|
220
|
+
const { writeSync } = await import("node:fs");
|
|
221
|
+
writeSync(
|
|
222
|
+
2,
|
|
223
|
+
`postgres-mcp: POSTGRES_MCP_RUNTIME=oam but ${oam} is older than oam ${min}.\n` +
|
|
224
|
+
`Run \`oam self-update\`, or use POSTGRES_MCP_RUNTIME=node.\n`,
|
|
225
|
+
);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
// auto: an old oam is a reason to prefer Node, not to fail. Say so, because
|
|
229
|
+
// a silent downgrade is how someone keeps running an oam they meant to
|
|
230
|
+
// update. stderr is safe -- MCP frames travel on stdout.
|
|
231
|
+
process.stderr.write(`postgres-mcp: oam at ${oam} is older than ${min}; using Node instead.\n`);
|
|
232
|
+
await runInProcess();
|
|
108
233
|
} else {
|
|
109
234
|
// `--` separates oam's own flags from the script's argv. Everything after
|
|
110
235
|
// it lands in process.argv for the server, so `postgres-mcp version` and
|
|
111
236
|
// any host-supplied flags survive the hop unchanged.
|
|
112
|
-
const child = spawn(oam, ["run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
|
|
237
|
+
const child = spawn(oam, [...sandboxFlags(), "run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
|
|
113
238
|
// inherit keeps the SAME fds, so MCP's newline-delimited JSON framing on
|
|
114
239
|
// stdin/stdout is untouched and the host's stdin-close still reaches the
|
|
115
240
|
// server's shutdown path.
|
package/dist/index.js
CHANGED
|
@@ -37796,7 +37796,7 @@ function compareVersions(a, b) {
|
|
|
37796
37796
|
}
|
|
37797
37797
|
|
|
37798
37798
|
// src/index.ts
|
|
37799
|
-
var version2 = true ? "0.
|
|
37799
|
+
var version2 = true ? "0.10.0" : await readPackageVersion();
|
|
37800
37800
|
var subcommand = process.argv[2];
|
|
37801
37801
|
if (subcommand === "version" || subcommand === "--version") {
|
|
37802
37802
|
console.log(version2);
|
package/package.json
CHANGED