Sign In

@yawlabs/postgres-mcp

Package Overview
Dependencies
Maintainers
1
Versions
40
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@yawlabs/postgres-mcp - npm Package Compare versions

Comparing version
0.8.0
to
0.9.0
+156
bin/postgres-mcp.mjs
#!/usr/bin/env node
/**
* Runtime launcher for @yawlabs/postgres-mcp.
*
* Prefers the oam runtime (https://oamjs.org) and falls back to the Node
* process already running this file. The server itself (`dist/index.js`) is
* runtime-agnostic -- it is a pre-bundled ESM file using only `node:` builtins
* that oam implements -- so neither path changes behavior.
*
* WHY THE FALLBACK COSTS NOTHING
* The fallback does NOT re-exec node. npm already started a node process to
* run this launcher, so falling back is a plain `import()` of the server into
* THIS process: zero extra spawn, zero extra startup, byte-identical behavior
* to invoking `dist/index.js` directly. Users without oam pay only the cost of
* resolving a few paths (a handful of `existsSync` calls, no subprocess).
*
* WHAT THE OAM PATH COSTS
* Taking the oam path means node has already booted, so the total is node's
* startup plus oam's. Measured on windows-arm64 against the 1.4 MB bundle:
* node alone ~650-900ms, oam alone ~980-1290ms, so the oam path lands near
* ~1.8s. This is a ONE-TIME cost per MCP session, not per tool call -- hosts
* spawn the server once and keep it -- but it is a real regression against
* plain node and the reason `POSTGRES_MCP_RUNTIME=node` exists.
*
* SELECTION
* POSTGRES_MCP_RUNTIME=oam require oam; fail loudly if it is missing
* POSTGRES_MCP_RUNTIME=node never use oam
* POSTGRES_MCP_RUNTIME=auto prefer oam, silently fall back (default)
* OAM_BIN=/path/to/oam explicit binary, checked before any discovery
*/
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { constants, homedir } from "node:os";
import { delimiter, join } from "node:path";
import { fileURLToPath } from "node:url";
// Two forms, deliberately. `import()` on Windows REJECTS a bare `C:\...` path
// with ERR_UNSUPPORTED_ESM_URL_SCHEME (it reads `c:` as a protocol), so the
// in-process fallback must use the file:// URL. spawn(), conversely, needs a
// real filesystem path. Keeping both avoids converting at each call site and
// getting it backwards on one of them.
const SERVER_URL = new URL("../dist/index.js", import.meta.url);
const SERVER_ENTRY = fileURLToPath(SERVER_URL);
const isWin = process.platform === "win32";
const exe = isWin ? "oam.exe" : "oam";
/**
* Locate an oam binary, or null. Ordered cheapest-and-most-explicit first;
* every branch is a stat, never a subprocess, so the miss case (the common one
* for users who have never heard of oam) stays sub-millisecond.
*/
function findOam() {
// 1. Explicit override wins and is never second-guessed.
const override = process.env.OAM_BIN;
if (override) return existsSync(override) ? override : null;
// 2. PATH. Resolved manually rather than by spawning `which`/`where`, which
// would cost a subprocess on every launch just to decide whether to spawn.
const pathExt = isWin ? (process.env.PATHEXT ?? ".EXE").split(";").filter(Boolean) : [""];
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
if (!dir) continue;
for (const ext of isWin ? pathExt : [""]) {
const candidate = join(dir, isWin ? `oam${ext.toLowerCase()}` : "oam");
if (existsSync(candidate)) return candidate;
}
}
// 3. The per-user locations oamjs.org's installers write to. Checked because
// an MCP host launched from a GUI often has a PATH that does not include
// them, so PATH-only discovery would miss an oam the user really has.
const installed = isWin
? [join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "oam", "bin", exe)]
: [join(homedir(), ".oam", "bin", exe)];
for (const candidate of installed) {
if (existsSync(candidate)) return candidate;
}
return null;
}
/** Run the server in THIS process. The zero-overhead fallback. */
async function runInProcess() {
await import(SERVER_URL.href);
}
const mode = (process.env.POSTGRES_MCP_RUNTIME ?? "auto").toLowerCase();
if (mode === "node") {
await runInProcess();
} else {
const oam = findOam();
if (!oam) {
if (mode === "oam") {
// Explicitly demanded, so this is a real misconfiguration -- do not
// silently do something else. writeSync because stderr is async for
// TTYs/pipes on Windows and process.exit truncates pending writes.
const { writeSync } = await import("node:fs");
writeSync(
2,
"postgres-mcp: POSTGRES_MCP_RUNTIME=oam but no oam binary was found.\n" +
"Install from https://oamjs.org, set OAM_BIN=/path/to/oam, or use POSTGRES_MCP_RUNTIME=node.\n",
);
process.exit(1);
}
await runInProcess();
} else {
// `--` separates oam's own flags from the script's argv. Everything after
// it lands in process.argv for the server, so `postgres-mcp version` and
// any host-supplied flags survive the hop unchanged.
const child = spawn(oam, ["run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
// inherit keeps the SAME fds, so MCP's newline-delimited JSON framing on
// stdin/stdout is untouched and the host's stdin-close still reaches the
// server's shutdown path.
stdio: "inherit",
env: process.env,
windowsHide: true,
});
// If oam cannot be executed at all (deleted between the stat and the
// spawn, wrong arch, permission), fall back rather than failing the whole
// server. `spawned` guards against falling back AFTER the child has begun
// running, which would double-start the server.
let spawned = false;
child.on("spawn", () => {
spawned = true;
});
child.on("error", (err) => {
if (spawned) return;
if (mode === "oam") {
process.stderr.write(`postgres-mcp: failed to launch oam (${err.message})\n`);
process.exit(1);
}
void runInProcess();
});
// Forward termination so the server's own SIGINT/SIGTERM cleanup (pool
// drain) runs in the child instead of the child being orphaned. Signals
// are a no-op on Windows but harmless to register.
for (const sig of ["SIGINT", "SIGTERM"]) {
process.on(sig, () => {
if (!child.killed) child.kill(sig);
});
}
child.on("exit", (code, signal) => {
// Mirror the child's fate: a signal death becomes 128+n so callers see a
// conventional shell exit status rather than a bare 0.
if (signal) {
process.exit(128 + (constants.signals[signal] ?? 15));
}
process.exit(code ?? 0);
});
}
}
+24
-0

@@ -10,2 +10,26 @@ # Changelog

### Added
- The `postgres-mcp` command is now a runtime launcher (`bin/postgres-mcp.mjs`)
that prefers the [oam](https://oamjs.org) runtime and falls back to Node.
Selection is via `POSTGRES_MCP_RUNTIME` (`auto` | `oam` | `node`, default
`auto`) and `OAM_BIN`.
The fallback costs nothing: npm already started Node to run the launcher, so
falling back is an `import()` into that same process -- no second spawn, no
extra startup, behavior identical to running `dist/index.js` directly. Users
without oam see no change and no stderr noise.
Equivalence on the oam path is verified end to end, not assumed: all 21 tools
register, a live query returns identical rows and `dataTypeName` values, and
the error paths match. oam provides every `node:` builtin the pg driver needs
(`net`, `tls`, `crypto`, `dns`), so SCRAM auth and the extended query protocol
both work.
**Latency, stated plainly:** taking the oam path means Node has booted first,
so both startups are paid. On windows-arm64 against the 1.4 MB bundle, Node
alone is ~650-900ms, oam alone ~980-1290ms, and launcher-to-oam ~1.8s. That is
a one-time cost per MCP session rather than per tool call, but it is a real
regression against plain Node -- `POSTGRES_MCP_RUNTIME=node` opts out.
> **Version note:** the "Changed (breaking)" entries below alter the shape of

@@ -12,0 +36,0 @@ > tool output and the CLI's exit behavior. Under SemVer-for-0.x that makes the

+3
-2
{
"name": "@yawlabs/postgres-mcp",
"version": "0.8.0",
"version": "0.9.0",
"mcpName": "io.github.YawLabs/postgres-mcp",

@@ -31,5 +31,6 @@ "description": "PostgreSQL MCP server - query, schema introspection, explain, and health checks for AI assistants",

"bin": {
"postgres-mcp": "dist/index.js"
"postgres-mcp": "bin/postgres-mcp.mjs"
},
"files": [
"bin/postgres-mcp.mjs",
"dist/index.js",

@@ -36,0 +37,0 @@ "LICENSE",

+28
-1

@@ -201,7 +201,34 @@ # @yawlabs/postgres-mcp

| `POSTGRES_SSL_REJECT_UNAUTHORIZED` | unset | Set to `false` to skip TLS cert verification (for managed DBs using private-CA certs). Connection is still encrypted. |
| `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). |
| `OAM_BIN` | unset | Explicit path to an `oam` binary, checked before PATH and the default install locations. |
### Supported Postgres versions
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.
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.
### Runtime
The published `postgres-mcp` command is a small launcher that prefers the [oam](https://oamjs.org) runtime and falls back to Node.
**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.
**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).
**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`.
```jsonc
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@yawlabs/postgres-mcp"],
"env": {
"DATABASE_URL": "postgres://...",
"POSTGRES_MCP_RUNTIME": "node" // opt out of oam
}
}
}
}
```
### Connecting to managed Postgres (Supabase, Neon, RDS, etc.)

@@ -208,0 +235,0 @@

Sorry, the diff of this file is too big to display