@yawlabs/postgres-mcp 0.6.20 → 0.7.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 +51 -0
- package/README.md +6 -0
- package/dist/index.js +76 -15
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -88,6 +88,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
88
88
|
a local-only pre-tag gate; this is just an opt-in fail-fast.
|
|
89
89
|
- `package.json` `package-lock.json` and `server.json` all bumped to 0.6.20.
|
|
90
90
|
|
|
91
|
+
## [0.6.19] - 2026-06-02
|
|
92
|
+
|
|
93
|
+
Release-flow hardening; no library behavior changes shipped in this version.
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
- `release.sh` refuses to push when origin already has `v<version>` pointing
|
|
97
|
+
at a different commit (rewound tag elsewhere, parallel release race) --
|
|
98
|
+
previously `git push --follow-tags` silently skipped the stale tag and the
|
|
99
|
+
GitHub release linked the wrong commit. Compares tag-object SHAs so resume
|
|
100
|
+
runs don't false-abort.
|
|
101
|
+
- README "Add to Yaw MCP" badge points at the https forwarder so it renders
|
|
102
|
+
as a link on github.com (raw `yaw://` hrefs are stripped).
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
- `SKIP_LINT=1` escape hatch in `release.sh` for hosts where the npm
|
|
106
|
+
run-script wrapper segfaults on exit-cleanup (MINGW64-ARM64).
|
|
107
|
+
- `wrapToolHandler` extracted from `index.ts` for testability, with unit
|
|
108
|
+
coverage of the MCP result wrapper and the connect-failure path (expanded
|
|
109
|
+
further in 0.6.20).
|
|
110
|
+
|
|
111
|
+
## [0.6.18] - 2026-05-28
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
- Release publishing consolidated into `release.sh`: the MCP Registry publish
|
|
115
|
+
moved into the script and `release.yml` (plus the non-release CI workflows)
|
|
116
|
+
was dropped. The script hands off to CI when a CI publish path exists and
|
|
117
|
+
publishes from the workstation otherwise.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
- `release.sh` syncs `server.json` unconditionally, not only inside the bump
|
|
121
|
+
branch, so a resume run no longer asks mcp-publisher to re-publish the
|
|
122
|
+
previous version (400 duplicate-version).
|
|
123
|
+
- `release.sh` falls back to the gh CLI session token when
|
|
124
|
+
`MCP_REGISTRY_TOKEN` is unset.
|
|
125
|
+
- The release confirmation prompt is tty-gated so non-interactive runs don't
|
|
126
|
+
hang on `read`.
|
|
127
|
+
|
|
128
|
+
### Docs
|
|
129
|
+
- README install badge swapped to the "Add to Yaw MCP" deep link; `npx`
|
|
130
|
+
spawn examples pinned to `@latest` for auto-update.
|
|
131
|
+
|
|
132
|
+
## [0.6.17] - 2026-05-19
|
|
133
|
+
|
|
134
|
+
### Added
|
|
135
|
+
- `release.sh` accepts an optional pre-release commit message as a second
|
|
136
|
+
argument: runs the pre-commit checklist, commits tracked changes, then
|
|
137
|
+
proceeds with the release.
|
|
138
|
+
- Post-publish smoke script (`scripts/post-publish-smoke.sh`) wired into the
|
|
139
|
+
release flow -- exercises the published tarball via a real `npx` install
|
|
140
|
+
instead of trusting `npm view` registry metadata.
|
|
141
|
+
|
|
91
142
|
## [0.6.16] - 2026-05-18
|
|
92
143
|
|
|
93
144
|
### Tests
|
package/README.md
CHANGED
|
@@ -246,6 +246,12 @@ DATABASE_URL='postgres://user:pass@host:5432/db' POSTGRES_MCP_INTEGRATION=1 npm
|
|
|
246
246
|
|
|
247
247
|
The integration suite assumes a disposable database -- it creates and drops a `test_fixture` schema. Don't point it at anything you care about.
|
|
248
248
|
|
|
249
|
+
To also run the destructive tests (REVOKE / restricted-role path), add `POSTGRES_MCP_DESTRUCTIVE_TESTS=1`. Only safe on a disposable cluster:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
DATABASE_URL='postgres://user:pass@host:5432/db' POSTGRES_MCP_INTEGRATION=1 POSTGRES_MCP_DESTRUCTIVE_TESTS=1 npm run test:integration
|
|
253
|
+
```
|
|
254
|
+
|
|
249
255
|
### Windows: integration tests via WSL2
|
|
250
256
|
|
|
251
257
|
Native Postgres on Windows ARM64 is fragile (UCRT runtime gaps, missing ARM64 builds). The reliable path is a disposable Ubuntu under WSL2 with the integration suite running inside WSL (WSL2's NAT blocks the Windows host from reaching :5432, so don't try to run the tests from PowerShell):
|
package/dist/index.js
CHANGED
|
@@ -36691,7 +36691,7 @@ var adminTools = [
|
|
|
36691
36691
|
},
|
|
36692
36692
|
{
|
|
36693
36693
|
name: "pg_advisor",
|
|
36694
|
-
description: "Rolled-up DBA lint pass. One call returns three categories of findings:\n- sequence_exhaustion: SERIAL / BIGSERIAL / IDENTITY sequences whose `last_value` is above `seqExhaustionThreshold` of `max_value`. The classic incident class.\n- tables_without_primary_key: user tables with no PK. Bloat candidates and a sign of design drift; some replication setups also need PKs.\n- public_tables_without_rls: tables in `public` (or any schema in `rlsSchemas`) with row-level security disabled. Useful as a security baseline check.\nUse this as the 'what should I be looking at?' starting point, then drill into `pg_unused_indexes`, `pg_table_bloat`, `pg_seq_scan_tables` for the perf side.",
|
|
36694
|
+
description: "Rolled-up DBA lint pass. One call returns three categories of findings:\n- sequence_exhaustion: SERIAL / BIGSERIAL / IDENTITY sequences whose `last_value` is above `seqExhaustionThreshold` of `max_value`. The classic incident class.\n- tables_without_primary_key: user tables (plain and partitioned) with no PK defined. Bloat candidates and a sign of design drift; some replication setups also need PKs. Foreign tables are excluded -- PostgreSQL forbids declaring PKs on foreign tables.\n- public_tables_without_rls: tables in `public` (or any schema in `rlsSchemas`) with row-level security disabled. Useful as a security baseline check.\nUse this as the 'what should I be looking at?' starting point, then drill into `pg_unused_indexes`, `pg_table_bloat`, `pg_seq_scan_tables` for the perf side.",
|
|
36695
36695
|
annotations: {
|
|
36696
36696
|
title: "Database advisor (DBA lints)",
|
|
36697
36697
|
readOnlyHint: true,
|
|
@@ -36744,6 +36744,10 @@ var adminTools = [
|
|
|
36744
36744
|
// Partition children (relkind='r') inherit the parent's PK as an
|
|
36745
36745
|
// indisprimary index on the child, so the NOT EXISTS clause keeps
|
|
36746
36746
|
// already filtering them out.
|
|
36747
|
+
//
|
|
36748
|
+
// Foreign tables (relkind='f') are excluded: PostgreSQL forbids
|
|
36749
|
+
// PRIMARY KEY (and UNIQUE) constraints on foreign tables entirely,
|
|
36750
|
+
// so they would always appear here with no possible remediation.
|
|
36747
36751
|
`SELECT
|
|
36748
36752
|
n.nspname AS schema,
|
|
36749
36753
|
c.relname AS "table"
|
|
@@ -36792,7 +36796,7 @@ var adminTools = [
|
|
|
36792
36796
|
},
|
|
36793
36797
|
{
|
|
36794
36798
|
name: "pg_table_bloat",
|
|
36795
|
-
description: "Estimate table bloat (dead tuples + free space) for tables in a schema. Returns live tuples, dead tuples, dead-tuple ratio, last_vacuum / last_autovacuum timestamps, and total relation size. A high dead_ratio with a stale last_autovacuum is a sign a table needs VACUUM
|
|
36799
|
+
description: "Estimate table bloat (dead tuples + free space) for tables in a schema. Returns live tuples, dead tuples, dead-tuple ratio, last_vacuum / last_autovacuum timestamps, and total relation size. A high dead_ratio with a stale last_autovacuum is a sign a table needs VACUUM.\n\nThree methods are available via the `method` parameter:\n- `estimate` (default): reads pg_stat_user_tables -- fast, no extensions, ANALYZE-driven approximations. Use this first.\n- `approx`: uses pgstattuple_approx() -- fast sampling pass, more accurate than estimates, requires the pgstattuple extension.\n- `exact`: uses pgstattuple() -- full table scan, exact counts, slow on large tables, requires the pgstattuple extension. Always pass `schema` with method='exact' -- scanning all user tables in one statement will hit statement_timeout on non-trivial databases.\nInstall pgstattuple with `CREATE EXTENSION pgstattuple` (requires superuser).",
|
|
36796
36800
|
annotations: {
|
|
36797
36801
|
title: "Estimate table bloat",
|
|
36798
36802
|
readOnlyHint: true,
|
|
@@ -36803,13 +36807,60 @@ var adminTools = [
|
|
|
36803
36807
|
inputSchema: external_exports.object({
|
|
36804
36808
|
schema: identSchema.optional().describe("Limit to one schema. If omitted, all user schemas are included."),
|
|
36805
36809
|
minDeadRatio: external_exports.number().min(0).max(1).default(0.1).describe("Minimum dead-tuple fraction to include - dead / (live + dead). Default 0.1 = 10%."),
|
|
36806
|
-
limit: external_exports.number().int().min(1).max(200).default(50).describe("Max rows to return (default 50).")
|
|
36810
|
+
limit: external_exports.number().int().min(1).max(200).default(50).describe("Max rows to return (default 50)."),
|
|
36811
|
+
method: external_exports.enum(["estimate", "approx", "exact"]).default("estimate").describe(
|
|
36812
|
+
"Bloat measurement method. 'estimate' (default) uses pg_stat_user_tables (fast, no extensions). 'approx' uses pgstattuple_approx() (fast sampling, more accurate). 'exact' uses pgstattuple() (full scan, exact but slow). Both 'approx' and 'exact' require the pgstattuple extension."
|
|
36813
|
+
)
|
|
36807
36814
|
}),
|
|
36808
36815
|
handler: async (input) => {
|
|
36809
|
-
const {
|
|
36816
|
+
const {
|
|
36817
|
+
schema,
|
|
36818
|
+
minDeadRatio,
|
|
36819
|
+
limit,
|
|
36820
|
+
method: rawMethod
|
|
36821
|
+
} = input;
|
|
36822
|
+
const method = rawMethod ?? "estimate";
|
|
36810
36823
|
const schemaFilter = schema ? "AND schemaname = $3" : "AND schemaname NOT IN ('pg_catalog', 'information_schema') AND schemaname NOT LIKE 'pg_%'";
|
|
36811
36824
|
const params = [minDeadRatio, limit];
|
|
36812
36825
|
if (schema) params.push(schema);
|
|
36826
|
+
if (method !== "estimate") {
|
|
36827
|
+
const check2 = await runInternal(
|
|
36828
|
+
`SELECT EXISTS (
|
|
36829
|
+
SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'pgstattuple'
|
|
36830
|
+
) AS installed`
|
|
36831
|
+
);
|
|
36832
|
+
if (!check2.ok) return check2;
|
|
36833
|
+
if (!check2.data?.[0]?.installed) {
|
|
36834
|
+
return {
|
|
36835
|
+
ok: false,
|
|
36836
|
+
error: `pgstattuple extension is not installed. Install with \`CREATE EXTENSION pgstattuple;\` (requires superuser), then retry with method='${method}'.`
|
|
36837
|
+
};
|
|
36838
|
+
}
|
|
36839
|
+
const fn = method === "approx" ? "pgstattuple_approx" : "pgstattuple";
|
|
36840
|
+
const liveTuplesCol = method === "approx" ? "approx_tuple_count" : "tuple_count";
|
|
36841
|
+
return runInternal(
|
|
36842
|
+
`SELECT
|
|
36843
|
+
s.schemaname AS schema,
|
|
36844
|
+
s.relname AS "table",
|
|
36845
|
+
(p.${liveTuplesCol})::text AS live_tuples,
|
|
36846
|
+
p.dead_tuple_count::text AS dead_tuples,
|
|
36847
|
+
(p.dead_tuple_count::float8 / NULLIF(p.${liveTuplesCol} + p.dead_tuple_count, 0))::numeric(6, 3)::float8 AS dead_ratio,
|
|
36848
|
+
pg_size_pretty(pg_total_relation_size(s.relid)) AS size_pretty,
|
|
36849
|
+
pg_total_relation_size(s.relid)::text AS size_bytes,
|
|
36850
|
+
s.last_vacuum::text AS last_vacuum,
|
|
36851
|
+
s.last_autovacuum::text AS last_autovacuum,
|
|
36852
|
+
s.last_analyze::text AS last_analyze
|
|
36853
|
+
FROM pg_catalog.pg_stat_user_tables s
|
|
36854
|
+
JOIN pg_catalog.pg_class c ON c.oid = s.relid AND c.relkind IN ('r', 'm')
|
|
36855
|
+
CROSS JOIN LATERAL ${fn}(s.relid::regclass) p
|
|
36856
|
+
WHERE (p.${liveTuplesCol} + p.dead_tuple_count) > 0
|
|
36857
|
+
AND (p.dead_tuple_count::float8 / NULLIF(p.${liveTuplesCol} + p.dead_tuple_count, 0)) >= $1
|
|
36858
|
+
${schemaFilter}
|
|
36859
|
+
ORDER BY p.dead_tuple_count DESC
|
|
36860
|
+
LIMIT $2`,
|
|
36861
|
+
params
|
|
36862
|
+
);
|
|
36863
|
+
}
|
|
36813
36864
|
return runInternal(
|
|
36814
36865
|
// dead_ratio = dead / (live + dead): bounded [0, 1]. A 100%-dead table
|
|
36815
36866
|
// (live=0, dead>0) correctly reports 1.0 instead of 0. Tables with both
|
|
@@ -36855,14 +36906,14 @@ function quoteQualifiedTable(name) {
|
|
|
36855
36906
|
return name.split(".").map((p) => quoteIdent(p)).join(".");
|
|
36856
36907
|
}
|
|
36857
36908
|
function validateHypoIndex(idx) {
|
|
36909
|
+
if (idx.table.includes('"')) {
|
|
36910
|
+
return `Hypothetical index table ${JSON.stringify(idx.table)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
36911
|
+
}
|
|
36858
36912
|
const pieces = idx.table.split(".");
|
|
36859
36913
|
if (pieces.length > 2) {
|
|
36860
36914
|
return `Hypothetical index table ${JSON.stringify(idx.table)} is over-qualified; use only \`schema.table\` or \`table\`.`;
|
|
36861
36915
|
}
|
|
36862
36916
|
for (const piece of pieces) {
|
|
36863
|
-
if (piece.includes('"')) {
|
|
36864
|
-
return `Hypothetical index table ${JSON.stringify(idx.table)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
36865
|
-
}
|
|
36866
36917
|
if (Buffer.byteLength(piece, "utf8") > 63) {
|
|
36867
36918
|
return `Hypothetical index table piece ${JSON.stringify(piece)} exceeds PostgreSQL's 63-byte NAMEDATALEN limit (multi-byte characters count as multiple bytes).`;
|
|
36868
36919
|
}
|
|
@@ -37140,7 +37191,7 @@ var schemaTools = [
|
|
|
37140
37191
|
},
|
|
37141
37192
|
{
|
|
37142
37193
|
name: "pg_list_tables",
|
|
37143
|
-
description: "List tables (and optionally views) in a schema. Returns name, type (table/view/materialized view/foreign), and estimated row count (from `reltuples`;
|
|
37194
|
+
description: "List tables (and optionally views) in a schema. Returns name, type (table/view/materialized view/foreign), and estimated row count (from `reltuples`; null = no ANALYZE yet on PG 14+; 0 may mean empty or unanalyzed on PG <= 13). Paginate via `limit`/`offset` on very large schemas.",
|
|
37144
37195
|
annotations: {
|
|
37145
37196
|
title: "List tables in a schema",
|
|
37146
37197
|
readOnlyHint: true,
|
|
@@ -37168,7 +37219,7 @@ var schemaTools = [
|
|
|
37168
37219
|
WHEN 'p' THEN 'partitioned_table'
|
|
37169
37220
|
ELSE c.relkind::text
|
|
37170
37221
|
END AS type,
|
|
37171
|
-
c.reltuples::
|
|
37222
|
+
NULLIF(round(c.reltuples), -1)::float8 AS estimated_rows
|
|
37172
37223
|
FROM pg_catalog.pg_class c
|
|
37173
37224
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
37174
37225
|
WHERE n.nspname = $1
|
|
@@ -37540,7 +37591,7 @@ var schemaTools = [
|
|
|
37540
37591
|
var statsTools = [
|
|
37541
37592
|
{
|
|
37542
37593
|
name: "pg_top_queries",
|
|
37543
|
-
description: "Top N queries by total or mean execution time. Requires the `pg_stat_statements` extension to be installed and enabled (most managed Postgres providers have it on by default). Returns normalized query text (constants replaced with `?`), call count, total/mean/min/max time in ms, rows returned, and cache hit ratio. Use this to find slow queries worth optimizing.",
|
|
37594
|
+
description: "Top N queries by total or mean execution time. Requires the `pg_stat_statements` extension to be installed and enabled (most managed Postgres providers have it on by default). Returns normalized query text (constants replaced with `?`), call count, total/mean/min/max time in ms, rows returned, and cache hit ratio. Use this to find slow queries worth optimizing. On pg_stat_statements >= 1.10 (Postgres 15+), also returns `io_read_time_ms` and `io_write_time_ms` to separate IO-bound from CPU-bound queries (null when track_io_timing = off or the query did no measurable IO -- enable track_io_timing in postgresql.conf to get non-null values).",
|
|
37544
37595
|
annotations: {
|
|
37545
37596
|
title: "Top queries by execution time",
|
|
37546
37597
|
readOnlyHint: true,
|
|
@@ -37570,6 +37621,11 @@ var statsTools = [
|
|
|
37570
37621
|
const meanCol = useExecSuffix ? "mean_exec_time" : "mean_time";
|
|
37571
37622
|
const minCol = useExecSuffix ? "min_exec_time" : "min_time";
|
|
37572
37623
|
const maxCol = useExecSuffix ? "max_exec_time" : "max_time";
|
|
37624
|
+
const hasIoTiming = compareVersions(extVersion, "1.10") >= 0;
|
|
37625
|
+
const hasSharedBlkCols = compareVersions(extVersion, "1.11") >= 0;
|
|
37626
|
+
const ioTimingCols = hasIoTiming ? `,
|
|
37627
|
+
NULLIF(${hasSharedBlkCols ? "shared_blk_read_time" : "blk_read_time"}, 0)::numeric(18, 2)::float8 AS io_read_time_ms,
|
|
37628
|
+
NULLIF(${hasSharedBlkCols ? "shared_blk_write_time" : "blk_write_time"}, 0)::numeric(18, 2)::float8 AS io_write_time_ms` : "";
|
|
37573
37629
|
const orderCol = orderBy === "total_time" ? totalCol : orderBy === "mean_time" ? meanCol : "pg_stat_statements.calls";
|
|
37574
37630
|
return runInternal(
|
|
37575
37631
|
// bigint counters (calls, rows) come back as `.text` for lossless
|
|
@@ -37588,7 +37644,7 @@ var statsTools = [
|
|
|
37588
37644
|
WHEN (shared_blks_hit + shared_blks_read) > 0
|
|
37589
37645
|
THEN (shared_blks_hit::float8 / (shared_blks_hit + shared_blks_read) * 100)::numeric(5, 2)::float8
|
|
37590
37646
|
ELSE NULL
|
|
37591
|
-
END AS hit_percent
|
|
37647
|
+
END AS hit_percent${ioTimingCols}
|
|
37592
37648
|
FROM pg_stat_statements
|
|
37593
37649
|
ORDER BY ${orderCol} DESC NULLS LAST
|
|
37594
37650
|
LIMIT $1`,
|
|
@@ -37695,7 +37751,7 @@ function compareVersions(a, b) {
|
|
|
37695
37751
|
}
|
|
37696
37752
|
|
|
37697
37753
|
// src/index.ts
|
|
37698
|
-
var version2 = true ? "0.
|
|
37754
|
+
var version2 = true ? "0.7.0" : await readPackageVersion();
|
|
37699
37755
|
var subcommand = process.argv[2];
|
|
37700
37756
|
if (subcommand === "version" || subcommand === "--version") {
|
|
37701
37757
|
console.log(version2);
|
|
@@ -37716,9 +37772,14 @@ for (const tool of allTools) {
|
|
|
37716
37772
|
);
|
|
37717
37773
|
}
|
|
37718
37774
|
var transport = new StdioServerTransport();
|
|
37719
|
-
|
|
37720
|
-
|
|
37721
|
-
console.error(`@yawlabs/postgres-mcp v${version2} ready (${allTools.length} tools, ${writesNote})`);
|
|
37775
|
+
server.connect(transport).then(() => {
|
|
37776
|
+
const writesNote = isWritesAllowed() ? "writes ENABLED" : "read-only";
|
|
37777
|
+
console.error(`@yawlabs/postgres-mcp v${version2} ready (${allTools.length} tools, ${writesNote})`);
|
|
37778
|
+
}).catch((err) => {
|
|
37779
|
+
process.stderr.write(`postgres-mcp: ${err instanceof Error ? err.message : String(err)}
|
|
37780
|
+
`);
|
|
37781
|
+
process.exit(1);
|
|
37782
|
+
});
|
|
37722
37783
|
var exiting = false;
|
|
37723
37784
|
var cleanup = async () => {
|
|
37724
37785
|
if (exiting) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/postgres-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@types/pg": "^8.20.0",
|
|
55
55
|
"esbuild": "^0.28.0",
|
|
56
56
|
"pg": "^8.14.0",
|
|
57
|
+
"postject": "^1.0.0-alpha.6",
|
|
57
58
|
"typescript": "^6.0.3",
|
|
58
59
|
"zod": "^4.3.6"
|
|
59
60
|
},
|