@yawlabs/postgres-mcp 0.3.0 → 0.3.2

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.
Files changed (2) hide show
  1. package/dist/index.js +45 -17
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -35361,6 +35361,30 @@ async function runReadWrite(sql, params = []) {
35361
35361
  client.release();
35362
35362
  }
35363
35363
  }
35364
+ async function runReadWriteRollback(sql, params = []) {
35365
+ if (!isWritesAllowed()) {
35366
+ return {
35367
+ ok: false,
35368
+ error: "Write blocked: ALLOW_WRITES is not set. Set ALLOW_WRITES=1 in the MCP server env to enable DML/DDL."
35369
+ };
35370
+ }
35371
+ const client = await getPool().connect();
35372
+ const maxRows = getMaxRows();
35373
+ try {
35374
+ await client.query("BEGIN");
35375
+ const result = await client.query(sql, params);
35376
+ await client.query("ROLLBACK");
35377
+ return { ok: true, data: toQueryResult(result, maxRows) };
35378
+ } catch (err) {
35379
+ try {
35380
+ await client.query("ROLLBACK");
35381
+ } catch {
35382
+ }
35383
+ return { ok: false, error: formatPgError(err) };
35384
+ } finally {
35385
+ client.release();
35386
+ }
35387
+ }
35364
35388
  async function runInternal(sql, params = []) {
35365
35389
  try {
35366
35390
  const result = await getPool().query(sql, params);
@@ -35439,8 +35463,10 @@ var adminTools = [
35439
35463
  }),
35440
35464
  handler: async (input) => {
35441
35465
  const { includeSystem } = input;
35442
- const filter = includeSystem ? "" : "WHERE r.rolname NOT LIKE 'pg\\_%' ESCAPE '\\\\'";
35466
+ const filter = includeSystem ? "" : "WHERE NOT starts_with(r.rolname, 'pg_')";
35443
35467
  return runInternal(
35468
+ // Cast member_of to text[] so node-pg parses it into a JS array.
35469
+ // Without the cast, it comes back as the postgres text form `{a,b}`.
35444
35470
  `SELECT
35445
35471
  r.rolname AS name,
35446
35472
  r.rolcanlogin AS can_login,
@@ -35450,11 +35476,11 @@ var adminTools = [
35450
35476
  r.rolreplication AS replication,
35451
35477
  r.rolbypassrls AS bypass_rls,
35452
35478
  COALESCE(
35453
- (SELECT array_agg(g.rolname ORDER BY g.rolname)
35479
+ (SELECT array_agg(g.rolname::text ORDER BY g.rolname)
35454
35480
  FROM pg_catalog.pg_auth_members m
35455
35481
  JOIN pg_catalog.pg_roles g ON g.oid = m.roleid
35456
35482
  WHERE m.member = r.oid),
35457
- ARRAY[]::name[]
35483
+ ARRAY[]::text[]
35458
35484
  ) AS member_of
35459
35485
  FROM pg_catalog.pg_roles r
35460
35486
  ${filter}
@@ -35635,14 +35661,16 @@ var adminTools = [
35635
35661
  }
35636
35662
  ];
35637
35663
 
35638
- // src/tools/explain.ts
35664
+ // src/tools/params.ts
35639
35665
  var paramValue = external_exports3.lazy(
35640
35666
  () => external_exports3.union([external_exports3.string(), external_exports3.number(), external_exports3.boolean(), external_exports3.null(), external_exports3.array(paramValue), external_exports3.record(external_exports3.string(), paramValue)])
35641
35667
  );
35668
+
35669
+ // src/tools/explain.ts
35642
35670
  var explainTools = [
35643
35671
  {
35644
35672
  name: "pg_explain",
35645
- description: "Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE \u2014 for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement).",
35673
+ description: "Get the query plan for a SQL statement. By default, this uses plain EXPLAIN (no execution). Set `analyze: true` to run the query with EXPLAIN ANALYZE \u2014 for non-SELECT statements, ALLOW_WRITES=1 is required (since ANALYZE actually executes the statement). Writes executed during EXPLAIN ANALYZE are always rolled back, so you can inspect a plan for an INSERT/UPDATE/DELETE without persisting the mutation. Format is `text` (default) or `json`. Pass the raw SQL (not an EXPLAIN-prefixed statement).",
35646
35674
  annotations: {
35647
35675
  title: "Explain query plan",
35648
35676
  readOnlyHint: false,
@@ -35668,7 +35696,7 @@ var explainTools = [
35668
35696
  if (analyze) flags.push("ANALYZE");
35669
35697
  if (format === "json") flags.push("FORMAT JSON");
35670
35698
  const explainSql = flags.length > 0 ? `EXPLAIN (${flags.join(", ")}) ${sql}` : `EXPLAIN ${sql}`;
35671
- const result = analyze && isWritesAllowed() ? await runReadWrite(explainSql, params ?? []) : await runReadOnly(explainSql, params ?? []);
35699
+ const result = analyze && isWritesAllowed() ? await runReadWriteRollback(explainSql, params ?? []) : await runReadOnly(explainSql, params ?? []);
35672
35700
  if (!result.ok) return result;
35673
35701
  const data = result.data;
35674
35702
  const rows = data?.rows ?? [];
@@ -35738,7 +35766,8 @@ var healthTools = [
35738
35766
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
35739
35767
  WHERE c.relkind IN ('r', 'p')
35740
35768
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
35741
- AND n.nspname NOT LIKE 'pg_toast%'`
35769
+ AND n.nspname NOT LIKE 'pg_toast%'
35770
+ AND n.nspname NOT LIKE 'pg_temp_%'`
35742
35771
  )
35743
35772
  ]);
35744
35773
  if (!versionRes.ok) return versionRes;
@@ -35758,9 +35787,6 @@ var healthTools = [
35758
35787
  ];
35759
35788
 
35760
35789
  // src/tools/query.ts
35761
- var paramValue2 = external_exports3.lazy(
35762
- () => external_exports3.union([external_exports3.string(), external_exports3.number(), external_exports3.boolean(), external_exports3.null(), external_exports3.array(paramValue2), external_exports3.record(external_exports3.string(), paramValue2)])
35763
- );
35764
35790
  var queryTools = [
35765
35791
  {
35766
35792
  name: "pg_query",
@@ -35775,7 +35801,7 @@ var queryTools = [
35775
35801
  },
35776
35802
  inputSchema: external_exports3.object({
35777
35803
  sql: external_exports3.string().min(1).max(1e6).describe("The SQL statement to execute. Hard cap of 1 MB."),
35778
- params: external_exports3.array(paramValue2).optional().describe("Positional parameters referenced as $1, $2, ... in the SQL.")
35804
+ params: external_exports3.array(paramValue).optional().describe("Positional parameters referenced as $1, $2, ... in the SQL.")
35779
35805
  }),
35780
35806
  handler: async (input) => {
35781
35807
  const { sql, params } = input;
@@ -35902,10 +35928,10 @@ var schemaTools = [
35902
35928
  const foreignKeysQuery = `
35903
35929
  SELECT
35904
35930
  con.conname AS constraint_name,
35905
- array_agg(att.attname ORDER BY u.attposition) AS columns,
35931
+ array_agg(att.attname::text ORDER BY u.attposition) AS columns,
35906
35932
  cl.relname AS foreign_table,
35907
35933
  fn.nspname AS foreign_schema,
35908
- array_agg(fatt.attname ORDER BY u.attposition) AS foreign_columns
35934
+ array_agg(fatt.attname::text ORDER BY u.attposition) AS foreign_columns
35909
35935
  FROM pg_catalog.pg_constraint con
35910
35936
  JOIN pg_catalog.pg_class c ON c.oid = con.conrelid
35911
35937
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
@@ -36190,9 +36216,8 @@ var statsTools = [
36190
36216
  n_live_tup::text AS live_tuples,
36191
36217
  seq_tup_read::text AS seq_tup_read,
36192
36218
  CASE
36193
- WHEN COALESCE(idx_scan, 0) = 0 AND seq_scan > 0 THEN NULL
36194
- WHEN COALESCE(idx_scan, 0) = 0 THEN 0
36195
- ELSE (seq_scan::numeric / NULLIF(idx_scan, 0))::numeric(10, 2)::float8
36219
+ WHEN COALESCE(idx_scan, 0) = 0 THEN NULL
36220
+ ELSE (seq_scan::numeric / idx_scan)::numeric(10, 2)::float8
36196
36221
  END AS ratio
36197
36222
  FROM pg_catalog.pg_stat_user_tables
36198
36223
  WHERE n_live_tup >= $1
@@ -36258,7 +36283,7 @@ function compareVersions(a, b) {
36258
36283
  }
36259
36284
 
36260
36285
  // src/index.ts
36261
- var version2 = true ? "0.3.0" : (await null).createRequire(import.meta.url)("../package.json").version;
36286
+ var version2 = true ? "0.3.2" : (await null).createRequire(import.meta.url)("../package.json").version;
36262
36287
  var subcommand = process.argv[2];
36263
36288
  if (subcommand === "version" || subcommand === "--version") {
36264
36289
  console.log(version2);
@@ -36320,4 +36345,7 @@ process.on("SIGINT", () => {
36320
36345
  process.on("SIGTERM", () => {
36321
36346
  void cleanup().finally(() => process.exit(0));
36322
36347
  });
36348
+ process.stdin.on("end", () => {
36349
+ void cleanup().finally(() => process.exit(0));
36350
+ });
36323
36351
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/postgres-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "PostgreSQL MCP server — query, schema introspection, explain, and health checks for AI assistants",
5
5
  "license": "MIT",
6
6
  "author": "YawLabs <contact@yaw.sh>",
@@ -31,9 +31,9 @@
31
31
  "build": "tsc && node build.mjs",
32
32
  "dev": "tsc --watch",
33
33
  "start": "node dist/index.js",
34
- "test": "npm run build && node --test dist/**/*.test.js",
34
+ "test": "npm run build && node scripts/run-tests.mjs dist",
35
35
  "test:ci": "npm run test",
36
- "test:integration": "npm run build && node --test dist/**/*.integration.test.js",
36
+ "test:integration": "npm run build && node scripts/run-tests.mjs dist --integration",
37
37
  "lint": "biome check src/",
38
38
  "lint:fix": "biome check --write src/",
39
39
  "prepublishOnly": "npm run build"