@yawlabs/postgres-mcp 0.8.0 → 0.9.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,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - The `postgres-mcp` command is now a runtime launcher (`bin/postgres-mcp.mjs`)
13
+ that prefers the [oam](https://oamjs.org) runtime and falls back to Node.
14
+ Selection is via `POSTGRES_MCP_RUNTIME` (`auto` | `oam` | `node`, default
15
+ `auto`) and `OAM_BIN`.
16
+
17
+ The fallback costs nothing: npm already started Node to run the launcher, so
18
+ falling back is an `import()` into that same process -- no second spawn, no
19
+ extra startup, behavior identical to running `dist/index.js` directly. Users
20
+ without oam see no change and no stderr noise.
21
+
22
+ Equivalence on the oam path is verified end to end, not assumed: all 21 tools
23
+ register, a live query returns identical rows and `dataTypeName` values, and
24
+ the error paths match. oam provides every `node:` builtin the pg driver needs
25
+ (`net`, `tls`, `crypto`, `dns`), so SCRAM auth and the extended query protocol
26
+ both work.
27
+
28
+ **Latency, stated plainly:** taking the oam path means Node has booted first,
29
+ so both startups are paid. On windows-arm64 against the 1.4 MB bundle, Node
30
+ alone is ~650-900ms, oam alone ~980-1290ms, and launcher-to-oam ~1.8s. That is
31
+ a one-time cost per MCP session rather than per tool call, but it is a real
32
+ regression against plain Node -- `POSTGRES_MCP_RUNTIME=node` opts out.
33
+
10
34
  > **Version note:** the "Changed (breaking)" entries below alter the shape of
11
35
  > tool output and the CLI's exit behavior. Under SemVer-for-0.x that makes the
12
36
  > next release a MINOR bump -- `0.8.0`, not `0.7.1`. `release.sh` performs the
package/README.md CHANGED
@@ -198,10 +198,37 @@ All env vars are read from the MCP server's environment:
198
198
  | `POSTGRES_MAX_ROWS` | `1000` | Cap on rows returned by `pg_query`. |
199
199
  | `POSTGRES_POOL_MAX` | `5` | Max pool connections. Set to `1` for single-threaded backends (pglite-socket, PgBouncer transaction mode). |
200
200
  | `POSTGRES_SSL_REJECT_UNAUTHORIZED` | unset | Set to `false` to skip TLS cert verification (for managed DBs using private-CA certs). Connection is still encrypted. |
201
+ | `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
+ | `OAM_BIN` | unset | Explicit path to an `oam` binary, checked before PATH and the default install locations. |
201
203
 
202
204
  ### Supported Postgres versions
203
205
 
204
- 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.
206
+ Tested on **PostgreSQL 15, 17 and 18** in the integration matrix. 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.
207
+
208
+ ### Runtime
209
+
210
+ The published `postgres-mcp` command is a small launcher that prefers the [oam](https://oamjs.org) runtime and falls back to Node.
211
+
212
+ **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
+
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
+
216
+ **Cost, stated plainly.** Taking the oam path means Node has already booted, so you pay both startups. Measured on windows-arm64 against the 1.4 MB bundle: Node alone ~650-900ms, oam alone ~980-1290ms, launcher-to-oam ~1.8s. This is a **one-time cost per MCP session**, not per tool call -- hosts spawn the server once and hold it open -- but if you care about launch latency, set `POSTGRES_MCP_RUNTIME=node`.
217
+
218
+ ```jsonc
219
+ {
220
+ "mcpServers": {
221
+ "postgres": {
222
+ "command": "npx",
223
+ "args": ["-y", "@yawlabs/postgres-mcp"],
224
+ "env": {
225
+ "DATABASE_URL": "postgres://...",
226
+ "POSTGRES_MCP_RUNTIME": "node" // opt out of oam
227
+ }
228
+ }
229
+ }
230
+ }
231
+ ```
205
232
 
206
233
  ### Connecting to managed Postgres (Supabase, Neon, RDS, etc.)
207
234
 
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runtime launcher for @yawlabs/postgres-mcp.
4
+ *
5
+ * Prefers the oam runtime (https://oamjs.org) and falls back to the Node
6
+ * process already running this file. The server itself (`dist/index.js`) is
7
+ * runtime-agnostic -- it is a pre-bundled ESM file using only `node:` builtins
8
+ * that oam implements -- so neither path changes behavior.
9
+ *
10
+ * WHY THE FALLBACK COSTS NOTHING
11
+ * The fallback does NOT re-exec node. npm already started a node process to
12
+ * run this launcher, so falling back is a plain `import()` of the server into
13
+ * THIS process: zero extra spawn, zero extra startup, byte-identical behavior
14
+ * to invoking `dist/index.js` directly. Users without oam pay only the cost of
15
+ * resolving a few paths (a handful of `existsSync` calls, no subprocess).
16
+ *
17
+ * WHAT THE OAM PATH COSTS
18
+ * Taking the oam path means node has already booted, so the total is node's
19
+ * startup plus oam's. Measured on windows-arm64 against the 1.4 MB bundle:
20
+ * node alone ~650-900ms, oam alone ~980-1290ms, so the oam path lands near
21
+ * ~1.8s. This is a ONE-TIME cost per MCP session, not per tool call -- hosts
22
+ * spawn the server once and keep it -- but it is a real regression against
23
+ * plain node and the reason `POSTGRES_MCP_RUNTIME=node` exists.
24
+ *
25
+ * SELECTION
26
+ * POSTGRES_MCP_RUNTIME=oam require oam; fail loudly if it is missing
27
+ * POSTGRES_MCP_RUNTIME=node never use oam
28
+ * POSTGRES_MCP_RUNTIME=auto prefer oam, silently fall back (default)
29
+ * OAM_BIN=/path/to/oam explicit binary, checked before any discovery
30
+ */
31
+
32
+ import { spawn } from "node:child_process";
33
+ import { existsSync } from "node:fs";
34
+ import { constants, homedir } from "node:os";
35
+ import { delimiter, join } from "node:path";
36
+ import { fileURLToPath } from "node:url";
37
+
38
+ // Two forms, deliberately. `import()` on Windows REJECTS a bare `C:\...` path
39
+ // with ERR_UNSUPPORTED_ESM_URL_SCHEME (it reads `c:` as a protocol), so the
40
+ // in-process fallback must use the file:// URL. spawn(), conversely, needs a
41
+ // real filesystem path. Keeping both avoids converting at each call site and
42
+ // getting it backwards on one of them.
43
+ const SERVER_URL = new URL("../dist/index.js", import.meta.url);
44
+ const SERVER_ENTRY = fileURLToPath(SERVER_URL);
45
+ const isWin = process.platform === "win32";
46
+ const exe = isWin ? "oam.exe" : "oam";
47
+
48
+ /**
49
+ * Locate an oam binary, or null. Ordered cheapest-and-most-explicit first;
50
+ * every branch is a stat, never a subprocess, so the miss case (the common one
51
+ * for users who have never heard of oam) stays sub-millisecond.
52
+ */
53
+ function findOam() {
54
+ // 1. Explicit override wins and is never second-guessed.
55
+ const override = process.env.OAM_BIN;
56
+ if (override) return existsSync(override) ? override : null;
57
+
58
+ // 2. PATH. Resolved manually rather than by spawning `which`/`where`, which
59
+ // would cost a subprocess on every launch just to decide whether to spawn.
60
+ const pathExt = isWin ? (process.env.PATHEXT ?? ".EXE").split(";").filter(Boolean) : [""];
61
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
62
+ if (!dir) continue;
63
+ for (const ext of isWin ? pathExt : [""]) {
64
+ const candidate = join(dir, isWin ? `oam${ext.toLowerCase()}` : "oam");
65
+ if (existsSync(candidate)) return candidate;
66
+ }
67
+ }
68
+
69
+ // 3. The per-user locations oamjs.org's installers write to. Checked because
70
+ // an MCP host launched from a GUI often has a PATH that does not include
71
+ // them, so PATH-only discovery would miss an oam the user really has.
72
+ const installed = isWin
73
+ ? [join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "oam", "bin", exe)]
74
+ : [join(homedir(), ".oam", "bin", exe)];
75
+ for (const candidate of installed) {
76
+ if (existsSync(candidate)) return candidate;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /** Run the server in THIS process. The zero-overhead fallback. */
83
+ async function runInProcess() {
84
+ await import(SERVER_URL.href);
85
+ }
86
+
87
+ const mode = (process.env.POSTGRES_MCP_RUNTIME ?? "auto").toLowerCase();
88
+
89
+ if (mode === "node") {
90
+ await runInProcess();
91
+ } else {
92
+ const oam = findOam();
93
+
94
+ if (!oam) {
95
+ if (mode === "oam") {
96
+ // Explicitly demanded, so this is a real misconfiguration -- do not
97
+ // silently do something else. writeSync because stderr is async for
98
+ // TTYs/pipes on Windows and process.exit truncates pending writes.
99
+ const { writeSync } = await import("node:fs");
100
+ writeSync(
101
+ 2,
102
+ "postgres-mcp: POSTGRES_MCP_RUNTIME=oam but no oam binary was found.\n" +
103
+ "Install from https://oamjs.org, set OAM_BIN=/path/to/oam, or use POSTGRES_MCP_RUNTIME=node.\n",
104
+ );
105
+ process.exit(1);
106
+ }
107
+ await runInProcess();
108
+ } else {
109
+ // `--` separates oam's own flags from the script's argv. Everything after
110
+ // it lands in process.argv for the server, so `postgres-mcp version` and
111
+ // any host-supplied flags survive the hop unchanged.
112
+ const child = spawn(oam, ["run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
113
+ // inherit keeps the SAME fds, so MCP's newline-delimited JSON framing on
114
+ // stdin/stdout is untouched and the host's stdin-close still reaches the
115
+ // server's shutdown path.
116
+ stdio: "inherit",
117
+ env: process.env,
118
+ windowsHide: true,
119
+ });
120
+
121
+ // If oam cannot be executed at all (deleted between the stat and the
122
+ // spawn, wrong arch, permission), fall back rather than failing the whole
123
+ // server. `spawned` guards against falling back AFTER the child has begun
124
+ // running, which would double-start the server.
125
+ let spawned = false;
126
+ child.on("spawn", () => {
127
+ spawned = true;
128
+ });
129
+ child.on("error", (err) => {
130
+ if (spawned) return;
131
+ if (mode === "oam") {
132
+ process.stderr.write(`postgres-mcp: failed to launch oam (${err.message})\n`);
133
+ process.exit(1);
134
+ }
135
+ void runInProcess();
136
+ });
137
+
138
+ // Forward termination so the server's own SIGINT/SIGTERM cleanup (pool
139
+ // drain) runs in the child instead of the child being orphaned. Signals
140
+ // are a no-op on Windows but harmless to register.
141
+ for (const sig of ["SIGINT", "SIGTERM"]) {
142
+ process.on(sig, () => {
143
+ if (!child.killed) child.kill(sig);
144
+ });
145
+ }
146
+
147
+ child.on("exit", (code, signal) => {
148
+ // Mirror the child's fate: a signal death becomes 128+n so callers see a
149
+ // conventional shell exit status rather than a bare 0.
150
+ if (signal) {
151
+ process.exit(128 + (constants.signals[signal] ?? 15));
152
+ }
153
+ process.exit(code ?? 0);
154
+ });
155
+ }
156
+ }
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.8.0" : await readPackageVersion();
37799
+ var version2 = true ? "0.9.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/postgres-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "mcpName": "io.github.YawLabs/postgres-mcp",
5
5
  "description": "PostgreSQL MCP server - query, schema introspection, explain, and health checks for AI assistants",
6
6
  "license": "MIT",
@@ -28,9 +28,10 @@
28
28
  "type": "module",
29
29
  "main": "dist/index.js",
30
30
  "bin": {
31
- "postgres-mcp": "dist/index.js"
31
+ "postgres-mcp": "bin/postgres-mcp.mjs"
32
32
  },
33
33
  "files": [
34
+ "bin/postgres-mcp.mjs",
34
35
  "dist/index.js",
35
36
  "LICENSE",
36
37
  "README.md",