@yawlabs/postgres-mcp 0.6.12 → 0.6.14
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 +31 -0
- package/dist/index.js +48 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.14] - 2026-05-16
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- `paramValue` (used by `pg_query`, `pg_readonly`, `pg_explain` for positional
|
|
14
|
+
parameters) was unbounded recursive via `z.lazy`. A pathologically nested
|
|
15
|
+
request body could surface a `Maximum call stack size exceeded` RangeError
|
|
16
|
+
out of the request path instead of a clear validation error. The new
|
|
17
|
+
`paramsArray` schema wraps the top-level params array with an iterative
|
|
18
|
+
depth check capped at 32 levels (arrays and objects each count as one
|
|
19
|
+
level). Closes #7.
|
|
20
|
+
|
|
21
|
+
## [0.6.13] - 2026-05-16
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- `pg_kill` now captures postgres's NOTICE channel during
|
|
25
|
+
`pg_cancel_backend` / `pg_terminate_backend` and surfaces the message in
|
|
26
|
+
the `note` field when `signaled=false`. Postgres distinguishes "PID N
|
|
27
|
+
is not a PostgreSQL backend process" from "must be a member of the role
|
|
28
|
+
whose query is being canceled or member of pg_signal_backend" via
|
|
29
|
+
NOTICE, but the boolean return collapses both to `false`. Pre-0.6.13
|
|
30
|
+
the handler returned a generic three-way list and the agent had to
|
|
31
|
+
guess; now the cause is in the response. Closes #6.
|
|
32
|
+
|
|
33
|
+
### Docs
|
|
34
|
+
- `pg_kill` description now documents the NOTICE-derived `note` field.
|
|
35
|
+
|
|
36
|
+
### Internal
|
|
37
|
+
- `formatPgError` is now exported from `api.ts` so handlers that bypass
|
|
38
|
+
`runInternal` / `withSharedClient` (currently only `pg_kill`) can format
|
|
39
|
+
errors consistently with the rest of the codebase.
|
|
40
|
+
|
|
10
41
|
## [0.6.12] - 2026-05-16
|
|
11
42
|
|
|
12
43
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -36353,6 +36353,25 @@ async function shutdown() {
|
|
|
36353
36353
|
var paramValue = external_exports.lazy(
|
|
36354
36354
|
() => external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null(), external_exports.array(paramValue), external_exports.record(external_exports.string(), paramValue)])
|
|
36355
36355
|
);
|
|
36356
|
+
var MAX_PARAM_DEPTH = 32;
|
|
36357
|
+
function isWithinDepth(value, maxDepth) {
|
|
36358
|
+
const stack = [[value, 0]];
|
|
36359
|
+
while (stack.length > 0) {
|
|
36360
|
+
const next = stack.pop();
|
|
36361
|
+
if (!next) break;
|
|
36362
|
+
const [v, d] = next;
|
|
36363
|
+
if (d > maxDepth) return false;
|
|
36364
|
+
if (Array.isArray(v)) {
|
|
36365
|
+
for (const item of v) stack.push([item, d + 1]);
|
|
36366
|
+
} else if (v !== null && typeof v === "object") {
|
|
36367
|
+
for (const item of Object.values(v)) stack.push([item, d + 1]);
|
|
36368
|
+
}
|
|
36369
|
+
}
|
|
36370
|
+
return true;
|
|
36371
|
+
}
|
|
36372
|
+
var paramsArray = external_exports.array(paramValue).refine((arr) => arr.every((v) => isWithinDepth(v, MAX_PARAM_DEPTH)), {
|
|
36373
|
+
message: `Parameter value exceeds maximum nesting depth of ${MAX_PARAM_DEPTH}`
|
|
36374
|
+
});
|
|
36356
36375
|
var identSchema = external_exports.string().min(1).max(63).refine((v) => Buffer.byteLength(v, "utf8") <= 63, {
|
|
36357
36376
|
message: "Identifier exceeds PostgreSQL's 63-byte NAMEDATALEN limit (multi-byte characters count as multiple bytes)."
|
|
36358
36377
|
});
|
|
@@ -36508,7 +36527,7 @@ var adminTools = [
|
|
|
36508
36527
|
},
|
|
36509
36528
|
{
|
|
36510
36529
|
name: "pg_kill",
|
|
36511
|
-
description: "Cancel a running query (SIGINT-equivalent) or terminate a backend connection (SIGTERM-equivalent) by PID. Find the PID via `pg_health` active_queries or `pg_inspect_locks`. Requires ALLOW_WRITES=1 since this changes database session state. The role in DATABASE_URL must have permission - cancelling another user's query needs the `pg_signal_backend` role or superuser. Note: `pg_signal_backend` does NOT cover superuser-owned backends - only a superuser can signal another superuser's session. Cancel is graceful; terminate is forceful.",
|
|
36530
|
+
description: "Cancel a running query (SIGINT-equivalent) or terminate a backend connection (SIGTERM-equivalent) by PID. Find the PID via `pg_health` active_queries or `pg_inspect_locks`. Requires ALLOW_WRITES=1 since this changes database session state. The role in DATABASE_URL must have permission - cancelling another user's query needs the `pg_signal_backend` role or superuser. Note: `pg_signal_backend` does NOT cover superuser-owned backends - only a superuser can signal another superuser's session. Cancel is graceful; terminate is forceful. When `signaled=false`, the `note` field surfaces postgres's NOTICE explaining why (e.g. 'not a PostgreSQL backend process' for a non-pg PID, 'must be a member of...' for permission denial) so an agent can act on the specific cause rather than guess from a three-way list.",
|
|
36512
36531
|
annotations: {
|
|
36513
36532
|
title: "Cancel or terminate a backend",
|
|
36514
36533
|
readOnlyHint: false,
|
|
@@ -36529,18 +36548,31 @@ var adminTools = [
|
|
|
36529
36548
|
};
|
|
36530
36549
|
}
|
|
36531
36550
|
const fn = mode === "terminate" ? "pg_terminate_backend" : "pg_cancel_backend";
|
|
36532
|
-
const
|
|
36533
|
-
|
|
36534
|
-
const
|
|
36535
|
-
|
|
36536
|
-
ok: true,
|
|
36537
|
-
data: {
|
|
36538
|
-
pid,
|
|
36539
|
-
mode,
|
|
36540
|
-
signaled,
|
|
36541
|
-
note: signaled ? `Sent ${mode === "terminate" ? "SIGTERM" : "SIGINT"} to backend ${pid}.` : `Signal returned false - PID ${pid} may not exist, may already be gone, or the current role lacks permission.`
|
|
36542
|
-
}
|
|
36551
|
+
const client = await getPool().connect();
|
|
36552
|
+
const notices = [];
|
|
36553
|
+
const onNotice = (n) => {
|
|
36554
|
+
if (n.message) notices.push(n.message);
|
|
36543
36555
|
};
|
|
36556
|
+
client.on("notice", onNotice);
|
|
36557
|
+
try {
|
|
36558
|
+
const result = await client.query(`SELECT ${fn}($1) AS signaled`, [pid]);
|
|
36559
|
+
const signaled = result.rows[0]?.signaled === true;
|
|
36560
|
+
const noticeText = notices.join(" ").trim();
|
|
36561
|
+
return {
|
|
36562
|
+
ok: true,
|
|
36563
|
+
data: {
|
|
36564
|
+
pid,
|
|
36565
|
+
mode,
|
|
36566
|
+
signaled,
|
|
36567
|
+
note: signaled ? `Sent ${mode === "terminate" ? "SIGTERM" : "SIGINT"} to backend ${pid}.` : noticeText ? `${noticeText} (Signal returned false for PID ${pid}.)` : `Signal returned false - PID ${pid} may not exist, may already be gone, or the current role lacks permission.`
|
|
36568
|
+
}
|
|
36569
|
+
};
|
|
36570
|
+
} catch (err) {
|
|
36571
|
+
return { ok: false, error: formatPgError(err) };
|
|
36572
|
+
} finally {
|
|
36573
|
+
client.off("notice", onNotice);
|
|
36574
|
+
client.release();
|
|
36575
|
+
}
|
|
36544
36576
|
}
|
|
36545
36577
|
},
|
|
36546
36578
|
{
|
|
@@ -36820,7 +36852,7 @@ var explainTools = [
|
|
|
36820
36852
|
sql: external_exports.string().min(1).max(1e6).describe("The SQL statement to explain. Do NOT prefix with EXPLAIN."),
|
|
36821
36853
|
analyze: external_exports.boolean().default(false).describe("Run EXPLAIN ANALYZE (actually executes the query)."),
|
|
36822
36854
|
format: external_exports.enum(["text", "json"]).default("text").describe("Output format."),
|
|
36823
|
-
params:
|
|
36855
|
+
params: paramsArray.optional().describe("Positional parameters referenced as $1, $2, ... in the SQL."),
|
|
36824
36856
|
hypothetical_indexes: external_exports.array(hypotheticalIndex).optional().describe(
|
|
36825
36857
|
"List of indexes the planner should pretend exist for this EXPLAIN. Requires the HypoPG extension. Indexes are session-scoped and reset at the end of the call."
|
|
36826
36858
|
)
|
|
@@ -36972,7 +37004,7 @@ var queryTools = [
|
|
|
36972
37004
|
},
|
|
36973
37005
|
inputSchema: external_exports.object({
|
|
36974
37006
|
sql: external_exports.string().min(1).max(1e6).describe("The SQL statement to execute. Hard cap of 1 MB."),
|
|
36975
|
-
params:
|
|
37007
|
+
params: paramsArray.optional().describe("Positional parameters referenced as $1, $2, ... in the SQL.")
|
|
36976
37008
|
}),
|
|
36977
37009
|
handler: async (input) => {
|
|
36978
37010
|
const { sql, params } = input;
|
|
@@ -36992,7 +37024,7 @@ var queryTools = [
|
|
|
36992
37024
|
},
|
|
36993
37025
|
inputSchema: external_exports.object({
|
|
36994
37026
|
sql: external_exports.string().min(1).max(1e6).describe("The SQL statement to execute. Hard cap of 1 MB."),
|
|
36995
|
-
params:
|
|
37027
|
+
params: paramsArray.optional().describe("Positional parameters referenced as $1, $2, ... in the SQL.")
|
|
36996
37028
|
}),
|
|
36997
37029
|
handler: async (input) => {
|
|
36998
37030
|
const { sql, params } = input;
|
|
@@ -37578,7 +37610,7 @@ function compareVersions(a, b) {
|
|
|
37578
37610
|
}
|
|
37579
37611
|
|
|
37580
37612
|
// src/index.ts
|
|
37581
|
-
var version2 = true ? "0.6.
|
|
37613
|
+
var version2 = true ? "0.6.14" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
37582
37614
|
var subcommand = process.argv[2];
|
|
37583
37615
|
if (subcommand === "version" || subcommand === "--version") {
|
|
37584
37616
|
console.log(version2);
|
package/package.json
CHANGED