@yawlabs/postgres-mcp 0.4.0 → 0.5.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 +64 -0
- package/dist/index.js +272 -216
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-05-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **BREAKING:** `engines.node` raised from `>=18` to `>=20`. The CI matrix
|
|
14
|
+
has been on `[20, 22]` since 0.4.1 and the integration + release jobs
|
|
15
|
+
ran exclusively on Node 20+, so Node 18 was effectively unsupported in
|
|
16
|
+
practice; this release makes the declared support match. esbuild
|
|
17
|
+
`target` and `release.sh` prerequisite comment updated to match.
|
|
18
|
+
|
|
19
|
+
## [0.4.1] - 2026-05-04
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Multi-query handlers (`pg_describe_table`, `pg_health`, `pg_advisor`,
|
|
23
|
+
`pg_replication_status`) now share a single connection across their
|
|
24
|
+
internal catalog fan-out via a new `withSharedClient` helper, so one
|
|
25
|
+
tool call's 3-9 query fan-out can no longer saturate the pool (default
|
|
26
|
+
max 5) and starve concurrent calls. Previously, a single
|
|
27
|
+
`pg_describe_table` issued 9 parallel `Promise.all` queries against the
|
|
28
|
+
pool; under concurrent load this could block other tool calls until
|
|
29
|
+
the describe drained.
|
|
30
|
+
- `pg_top_queries` now returns `calls` and `rows` as text strings,
|
|
31
|
+
matching the bigint serialization of `pg_seq_scan_tables`,
|
|
32
|
+
`pg_unused_indexes`, and `pg_table_bloat`. Timing fields stay as JSON
|
|
33
|
+
numbers since they are inherently fractional milliseconds.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- `runUserQueryBounded` now distinguishes "DECLARE failed" (re-running
|
|
37
|
+
on the direct-exec path is safe -- the user SQL never executed) from
|
|
38
|
+
"FETCH/CLOSE/RELEASE failed" (re-running could double-execute side
|
|
39
|
+
effects). Previously, a transient FETCH-time failure on a
|
|
40
|
+
RETURNING-DML statement would silently re-run the mutation.
|
|
41
|
+
- `pg_top_queries` `orderBy: "calls"` now sorts numerically rather than
|
|
42
|
+
lexically. The 0.4.1 change to return `calls` as `text` shadowed the
|
|
43
|
+
source bigint column with a text output alias in the ORDER BY, so
|
|
44
|
+
"9" beat "10". The fix qualifies the ORDER BY expression with the
|
|
45
|
+
source table. Caught in the post-implementation review before tagging.
|
|
46
|
+
- `pg_describe_table` "not found" error message now escapes user-supplied
|
|
47
|
+
schema/table names via `JSON.stringify`, so a name containing `"` no
|
|
48
|
+
longer renders as broken-looking nested quotes.
|
|
49
|
+
- `pg_explain` `hypothetical_indexes` now pre-flight rejects pre-quoted
|
|
50
|
+
identifiers (`"odd.name"`, `weird"col`) with a clear validation error
|
|
51
|
+
before opening a database connection, instead of producing a confusing
|
|
52
|
+
planner error after the fact.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- New CI release plumbing mirroring `@yawlabs/tailscale-mcp`:
|
|
56
|
+
`.github/workflows/ci.yml` (lint + build + test on push/PR across
|
|
57
|
+
Node 18/20/22), `.github/workflows/integration.yml` (PG17 + PG18
|
|
58
|
+
service-container matrix, scheduled nightly + on-demand), and
|
|
59
|
+
`.github/workflows/release.yml` (tag-pushes-trigger-publish gated
|
|
60
|
+
on integration). `npm publish` now runs in CI with `--provenance`
|
|
61
|
+
via the org-level `NPM_TOKEN`; `release.sh` retains a local-dev
|
|
62
|
+
path that runs the WSL integration matrix as a pre-flight when
|
|
63
|
+
cutting a release from a workstation.
|
|
64
|
+
- New regression tests:
|
|
65
|
+
- `compareVersions` unit tests covering pre-release tags,
|
|
66
|
+
missing/longer segments, and the actual `1.8` boundary used by
|
|
67
|
+
`pg_top_queries`.
|
|
68
|
+
- Integration guard that partition children inheriting a parent's
|
|
69
|
+
primary key are correctly excluded from `pg_advisor`'s
|
|
70
|
+
`tables_without_primary_key` list.
|
|
71
|
+
- Integration coverage for the cursor-fallback path on both DDL
|
|
72
|
+
(`CREATE TABLE`) and DML-without-RETURNING (`INSERT`).
|
|
73
|
+
|
|
10
74
|
## [0.4.0] - 2026-04-25
|
|
11
75
|
|
|
12
76
|
### Security
|
package/dist/index.js
CHANGED
|
@@ -35340,12 +35340,14 @@ function formatPgError(err) {
|
|
|
35340
35340
|
}
|
|
35341
35341
|
async function runUserQueryBounded(client, sql, params, maxRows) {
|
|
35342
35342
|
await client.query("SAVEPOINT __pgmcp_sp");
|
|
35343
|
+
let declareSucceeded = false;
|
|
35343
35344
|
try {
|
|
35344
35345
|
await client.query({
|
|
35345
35346
|
text: `DECLARE __pgmcp_cur NO SCROLL CURSOR FOR ${sql}`,
|
|
35346
35347
|
values: params,
|
|
35347
35348
|
queryMode: "extended"
|
|
35348
35349
|
});
|
|
35350
|
+
declareSucceeded = true;
|
|
35349
35351
|
const fetched = await client.query(`FETCH ${maxRows + 1} FROM __pgmcp_cur`);
|
|
35350
35352
|
try {
|
|
35351
35353
|
await client.query("CLOSE __pgmcp_cur");
|
|
@@ -35353,7 +35355,10 @@ async function runUserQueryBounded(client, sql, params, maxRows) {
|
|
|
35353
35355
|
}
|
|
35354
35356
|
await client.query("RELEASE SAVEPOINT __pgmcp_sp");
|
|
35355
35357
|
return fetched;
|
|
35356
|
-
} catch {
|
|
35358
|
+
} catch (err) {
|
|
35359
|
+
if (declareSucceeded) {
|
|
35360
|
+
throw err;
|
|
35361
|
+
}
|
|
35357
35362
|
await client.query("ROLLBACK TO SAVEPOINT __pgmcp_sp");
|
|
35358
35363
|
await client.query("RELEASE SAVEPOINT __pgmcp_sp");
|
|
35359
35364
|
return client.query({
|
|
@@ -35468,6 +35473,22 @@ async function runInternal(sql, params = []) {
|
|
|
35468
35473
|
return { ok: false, error: formatPgError(err) };
|
|
35469
35474
|
}
|
|
35470
35475
|
}
|
|
35476
|
+
async function withSharedClient(fn) {
|
|
35477
|
+
const client = await getPool().connect();
|
|
35478
|
+
try {
|
|
35479
|
+
const runOnClient = async (sql, params = []) => {
|
|
35480
|
+
try {
|
|
35481
|
+
const result = await client.query(sql, params);
|
|
35482
|
+
return { ok: true, data: result.rows };
|
|
35483
|
+
} catch (err) {
|
|
35484
|
+
return { ok: false, error: formatPgError(err) };
|
|
35485
|
+
}
|
|
35486
|
+
};
|
|
35487
|
+
return await fn(runOnClient);
|
|
35488
|
+
} finally {
|
|
35489
|
+
client.release();
|
|
35490
|
+
}
|
|
35491
|
+
}
|
|
35471
35492
|
async function shutdown() {
|
|
35472
35493
|
typeNameCache = null;
|
|
35473
35494
|
if (!pool) return;
|
|
@@ -35652,49 +35673,51 @@ var adminTools = [
|
|
|
35652
35673
|
},
|
|
35653
35674
|
inputSchema: external_exports3.object({}),
|
|
35654
35675
|
handler: async () => {
|
|
35655
|
-
|
|
35656
|
-
|
|
35657
|
-
|
|
35658
|
-
|
|
35659
|
-
|
|
35660
|
-
|
|
35661
|
-
|
|
35662
|
-
|
|
35663
|
-
|
|
35664
|
-
|
|
35665
|
-
|
|
35666
|
-
|
|
35667
|
-
|
|
35668
|
-
|
|
35669
|
-
|
|
35670
|
-
|
|
35671
|
-
|
|
35672
|
-
|
|
35673
|
-
|
|
35674
|
-
|
|
35675
|
-
|
|
35676
|
-
|
|
35677
|
-
|
|
35678
|
-
|
|
35679
|
-
|
|
35680
|
-
|
|
35681
|
-
|
|
35682
|
-
|
|
35683
|
-
|
|
35684
|
-
|
|
35685
|
-
|
|
35686
|
-
|
|
35687
|
-
|
|
35688
|
-
|
|
35689
|
-
|
|
35690
|
-
|
|
35691
|
-
|
|
35692
|
-
|
|
35693
|
-
|
|
35694
|
-
|
|
35695
|
-
|
|
35696
|
-
|
|
35697
|
-
|
|
35676
|
+
return withSharedClient(async (run) => {
|
|
35677
|
+
const [slotsRes, replicasRes, walRes] = await Promise.all([
|
|
35678
|
+
run(
|
|
35679
|
+
`SELECT
|
|
35680
|
+
slot_name, slot_type, active,
|
|
35681
|
+
restart_lsn::text AS restart_lsn,
|
|
35682
|
+
confirmed_flush_lsn::text AS confirmed_flush_lsn,
|
|
35683
|
+
wal_status, database, plugin
|
|
35684
|
+
FROM pg_catalog.pg_replication_slots
|
|
35685
|
+
ORDER BY slot_name`
|
|
35686
|
+
),
|
|
35687
|
+
run(
|
|
35688
|
+
`SELECT
|
|
35689
|
+
application_name,
|
|
35690
|
+
client_addr::text AS client_addr,
|
|
35691
|
+
state,
|
|
35692
|
+
sync_state,
|
|
35693
|
+
EXTRACT(EPOCH FROM write_lag)::numeric(10, 2)::float8 AS write_lag_seconds,
|
|
35694
|
+
EXTRACT(EPOCH FROM flush_lag)::numeric(10, 2)::float8 AS flush_lag_seconds,
|
|
35695
|
+
EXTRACT(EPOCH FROM replay_lag)::numeric(10, 2)::float8 AS replay_lag_seconds
|
|
35696
|
+
FROM pg_catalog.pg_stat_replication
|
|
35697
|
+
ORDER BY application_name`
|
|
35698
|
+
),
|
|
35699
|
+
run(
|
|
35700
|
+
`SELECT
|
|
35701
|
+
pg_is_in_recovery() AS is_in_recovery,
|
|
35702
|
+
CASE
|
|
35703
|
+
WHEN pg_is_in_recovery() THEN pg_last_wal_receive_lsn()::text
|
|
35704
|
+
ELSE pg_current_wal_lsn()::text
|
|
35705
|
+
END AS wal_position`
|
|
35706
|
+
)
|
|
35707
|
+
]);
|
|
35708
|
+
if (!slotsRes.ok) return slotsRes;
|
|
35709
|
+
if (!replicasRes.ok) return replicasRes;
|
|
35710
|
+
if (!walRes.ok) return walRes;
|
|
35711
|
+
return {
|
|
35712
|
+
ok: true,
|
|
35713
|
+
data: {
|
|
35714
|
+
is_replica: walRes.data?.[0]?.is_in_recovery ?? false,
|
|
35715
|
+
wal_position: walRes.data?.[0]?.wal_position ?? null,
|
|
35716
|
+
slots: slotsRes.data ?? [],
|
|
35717
|
+
replicas: replicasRes.data ?? []
|
|
35718
|
+
}
|
|
35719
|
+
};
|
|
35720
|
+
});
|
|
35698
35721
|
}
|
|
35699
35722
|
},
|
|
35700
35723
|
{
|
|
@@ -35714,68 +35737,73 @@ var adminTools = [
|
|
|
35714
35737
|
}),
|
|
35715
35738
|
handler: async (input) => {
|
|
35716
35739
|
const { seqExhaustionThreshold, rlsSchemas, limit } = input;
|
|
35717
|
-
|
|
35718
|
-
|
|
35719
|
-
|
|
35720
|
-
|
|
35721
|
-
|
|
35722
|
-
|
|
35723
|
-
|
|
35724
|
-
|
|
35725
|
-
|
|
35726
|
-
|
|
35727
|
-
|
|
35728
|
-
|
|
35729
|
-
|
|
35730
|
-
|
|
35731
|
-
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
35742
|
-
|
|
35743
|
-
|
|
35744
|
-
|
|
35745
|
-
|
|
35746
|
-
|
|
35747
|
-
|
|
35748
|
-
|
|
35749
|
-
|
|
35750
|
-
|
|
35751
|
-
|
|
35752
|
-
|
|
35753
|
-
|
|
35754
|
-
|
|
35755
|
-
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35760
|
-
|
|
35761
|
-
|
|
35762
|
-
|
|
35763
|
-
|
|
35764
|
-
|
|
35765
|
-
|
|
35766
|
-
|
|
35767
|
-
|
|
35768
|
-
|
|
35769
|
-
|
|
35770
|
-
|
|
35771
|
-
ok:
|
|
35772
|
-
|
|
35773
|
-
|
|
35774
|
-
|
|
35775
|
-
|
|
35776
|
-
|
|
35777
|
-
|
|
35778
|
-
|
|
35740
|
+
return withSharedClient(async (run) => {
|
|
35741
|
+
const [seqRes, noPkRes, rlsRes] = await Promise.all([
|
|
35742
|
+
run(
|
|
35743
|
+
// pg_sequences was added in PG10. last_value can be NULL on a never-
|
|
35744
|
+
// touched sequence; we filter those out (nothing to report yet).
|
|
35745
|
+
`SELECT
|
|
35746
|
+
schemaname AS schema,
|
|
35747
|
+
sequencename AS sequence,
|
|
35748
|
+
last_value::text AS last_value,
|
|
35749
|
+
max_value::text AS max_value,
|
|
35750
|
+
(last_value::float8 / NULLIF(max_value::float8, 0))::numeric(6, 4)::float8 AS pct_used
|
|
35751
|
+
FROM pg_catalog.pg_sequences
|
|
35752
|
+
WHERE last_value IS NOT NULL
|
|
35753
|
+
AND max_value > 0
|
|
35754
|
+
AND (last_value::float8 / max_value::float8) >= $1
|
|
35755
|
+
ORDER BY pct_used DESC NULLS LAST
|
|
35756
|
+
LIMIT $2`,
|
|
35757
|
+
[seqExhaustionThreshold, limit]
|
|
35758
|
+
),
|
|
35759
|
+
run(
|
|
35760
|
+
// Declarative-partition children inherit the parent's primary key
|
|
35761
|
+
// as an indisprimary index, so the NOT EXISTS clause already
|
|
35762
|
+
// excludes them. Nothing extra needed for partitioned schemas.
|
|
35763
|
+
`SELECT
|
|
35764
|
+
n.nspname AS schema,
|
|
35765
|
+
c.relname AS "table"
|
|
35766
|
+
FROM pg_catalog.pg_class c
|
|
35767
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
35768
|
+
WHERE c.relkind = 'r'
|
|
35769
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
35770
|
+
AND n.nspname NOT LIKE 'pg_%'
|
|
35771
|
+
AND NOT EXISTS (
|
|
35772
|
+
SELECT 1 FROM pg_catalog.pg_index i
|
|
35773
|
+
WHERE i.indrelid = c.oid AND i.indisprimary
|
|
35774
|
+
)
|
|
35775
|
+
ORDER BY n.nspname, c.relname
|
|
35776
|
+
LIMIT $1`,
|
|
35777
|
+
[limit]
|
|
35778
|
+
),
|
|
35779
|
+
run(
|
|
35780
|
+
`SELECT
|
|
35781
|
+
n.nspname AS schema,
|
|
35782
|
+
c.relname AS "table"
|
|
35783
|
+
FROM pg_catalog.pg_class c
|
|
35784
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
35785
|
+
WHERE c.relkind IN ('r', 'p')
|
|
35786
|
+
AND n.nspname = ANY($1)
|
|
35787
|
+
AND NOT c.relrowsecurity
|
|
35788
|
+
ORDER BY n.nspname, c.relname
|
|
35789
|
+
LIMIT $2`,
|
|
35790
|
+
[rlsSchemas, limit]
|
|
35791
|
+
)
|
|
35792
|
+
]);
|
|
35793
|
+
const warnings = [];
|
|
35794
|
+
if (!seqRes.ok) warnings.push(`sequence_exhaustion fetch failed: ${seqRes.error}`);
|
|
35795
|
+
if (!noPkRes.ok) warnings.push(`tables_without_primary_key fetch failed: ${noPkRes.error}`);
|
|
35796
|
+
if (!rlsRes.ok) warnings.push(`public_tables_without_rls fetch failed: ${rlsRes.error}`);
|
|
35797
|
+
return {
|
|
35798
|
+
ok: true,
|
|
35799
|
+
data: {
|
|
35800
|
+
sequence_exhaustion: seqRes.ok ? seqRes.data : [],
|
|
35801
|
+
tables_without_primary_key: noPkRes.ok ? noPkRes.data : [],
|
|
35802
|
+
public_tables_without_rls: rlsRes.ok ? rlsRes.data : [],
|
|
35803
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
35804
|
+
}
|
|
35805
|
+
};
|
|
35806
|
+
});
|
|
35779
35807
|
}
|
|
35780
35808
|
},
|
|
35781
35809
|
{
|
|
@@ -35843,6 +35871,19 @@ function quoteIdent(name) {
|
|
|
35843
35871
|
function quoteQualifiedTable(name) {
|
|
35844
35872
|
return name.split(".").map((p) => quoteIdent(p)).join(".");
|
|
35845
35873
|
}
|
|
35874
|
+
function validateHypoIndex(idx) {
|
|
35875
|
+
for (const piece of idx.table.split(".")) {
|
|
35876
|
+
if (piece.includes('"')) {
|
|
35877
|
+
return `Hypothetical index table ${JSON.stringify(idx.table)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
35878
|
+
}
|
|
35879
|
+
}
|
|
35880
|
+
for (const col of idx.columns) {
|
|
35881
|
+
if (col.includes('"')) {
|
|
35882
|
+
return `Hypothetical index column ${JSON.stringify(col)} contains a double-quote; pass plain identifier names without pre-quoting.`;
|
|
35883
|
+
}
|
|
35884
|
+
}
|
|
35885
|
+
return null;
|
|
35886
|
+
}
|
|
35846
35887
|
function buildHypopgHooks(indexes) {
|
|
35847
35888
|
return {
|
|
35848
35889
|
setup: async (client) => {
|
|
@@ -35897,6 +35938,10 @@ var explainTools = [
|
|
|
35897
35938
|
if (format === "json") flags.push("FORMAT JSON");
|
|
35898
35939
|
const explainSql = flags.length > 0 ? `EXPLAIN (${flags.join(", ")}) ${sql}` : `EXPLAIN ${sql}`;
|
|
35899
35940
|
const hypoIndexes = hypothetical_indexes ?? [];
|
|
35941
|
+
for (const idx of hypoIndexes) {
|
|
35942
|
+
const err = validateHypoIndex(idx);
|
|
35943
|
+
if (err) return { ok: false, error: err };
|
|
35944
|
+
}
|
|
35900
35945
|
const hooks = hypoIndexes.length > 0 ? buildHypopgHooks(hypoIndexes) : {};
|
|
35901
35946
|
if (hypoIndexes.length > 0) {
|
|
35902
35947
|
const check2 = await runInternal(
|
|
@@ -35943,67 +35988,69 @@ var healthTools = [
|
|
|
35943
35988
|
}),
|
|
35944
35989
|
handler: async (input) => {
|
|
35945
35990
|
const { activeQueryLimit } = input;
|
|
35946
|
-
|
|
35947
|
-
|
|
35948
|
-
|
|
35949
|
-
|
|
35950
|
-
|
|
35951
|
-
|
|
35952
|
-
|
|
35953
|
-
|
|
35954
|
-
|
|
35955
|
-
|
|
35956
|
-
|
|
35957
|
-
|
|
35958
|
-
|
|
35959
|
-
|
|
35960
|
-
|
|
35961
|
-
|
|
35962
|
-
|
|
35963
|
-
|
|
35964
|
-
|
|
35965
|
-
|
|
35966
|
-
|
|
35967
|
-
|
|
35968
|
-
|
|
35969
|
-
|
|
35970
|
-
|
|
35971
|
-
|
|
35972
|
-
|
|
35973
|
-
|
|
35974
|
-
|
|
35975
|
-
|
|
35976
|
-
|
|
35977
|
-
|
|
35978
|
-
|
|
35979
|
-
|
|
35980
|
-
|
|
35981
|
-
|
|
35982
|
-
|
|
35983
|
-
|
|
35984
|
-
|
|
35985
|
-
|
|
35986
|
-
|
|
35987
|
-
|
|
35988
|
-
|
|
35989
|
-
|
|
35990
|
-
|
|
35991
|
-
|
|
35992
|
-
|
|
35993
|
-
|
|
35994
|
-
|
|
35995
|
-
|
|
35996
|
-
|
|
35997
|
-
|
|
35998
|
-
|
|
35999
|
-
|
|
36000
|
-
|
|
36001
|
-
|
|
36002
|
-
|
|
36003
|
-
|
|
36004
|
-
|
|
36005
|
-
|
|
36006
|
-
|
|
35991
|
+
return withSharedClient(async (run) => {
|
|
35992
|
+
const [versionRes, sizeRes, connsRes, activeRes, tableCountRes] = await Promise.all([
|
|
35993
|
+
run(`SELECT version() AS version`),
|
|
35994
|
+
run(
|
|
35995
|
+
`SELECT
|
|
35996
|
+
current_database() AS database,
|
|
35997
|
+
pg_size_pretty(pg_database_size(current_database())) AS size_pretty,
|
|
35998
|
+
pg_database_size(current_database())::text AS size_bytes`
|
|
35999
|
+
),
|
|
36000
|
+
run(
|
|
36001
|
+
`SELECT
|
|
36002
|
+
count(*)::text AS total,
|
|
36003
|
+
count(*) FILTER (WHERE state = 'active')::text AS active,
|
|
36004
|
+
count(*) FILTER (WHERE state = 'idle')::text AS idle,
|
|
36005
|
+
count(*) FILTER (WHERE state = 'idle in transaction')::text AS idle_in_transaction
|
|
36006
|
+
FROM pg_stat_activity
|
|
36007
|
+
WHERE datname = current_database()`
|
|
36008
|
+
),
|
|
36009
|
+
run(
|
|
36010
|
+
`SELECT
|
|
36011
|
+
pid,
|
|
36012
|
+
state,
|
|
36013
|
+
EXTRACT(EPOCH FROM (now() - query_start))::numeric(10, 2)::float8 AS duration_seconds,
|
|
36014
|
+
query,
|
|
36015
|
+
application_name
|
|
36016
|
+
FROM pg_stat_activity
|
|
36017
|
+
WHERE datname = current_database()
|
|
36018
|
+
AND state IS NOT NULL
|
|
36019
|
+
AND state <> 'idle'
|
|
36020
|
+
AND pid <> pg_backend_pid()
|
|
36021
|
+
ORDER BY query_start ASC NULLS LAST
|
|
36022
|
+
LIMIT $1`,
|
|
36023
|
+
[activeQueryLimit]
|
|
36024
|
+
),
|
|
36025
|
+
run(
|
|
36026
|
+
`SELECT count(*)::text AS count
|
|
36027
|
+
FROM pg_catalog.pg_class c
|
|
36028
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
36029
|
+
WHERE c.relkind IN ('r', 'p')
|
|
36030
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
36031
|
+
AND n.nspname NOT LIKE 'pg_toast%'
|
|
36032
|
+
AND n.nspname NOT LIKE 'pg_temp_%'`
|
|
36033
|
+
)
|
|
36034
|
+
]);
|
|
36035
|
+
if (!versionRes.ok) return versionRes;
|
|
36036
|
+
const warnings = [];
|
|
36037
|
+
if (!sizeRes.ok) warnings.push(`database fetch failed: ${sizeRes.error}`);
|
|
36038
|
+
if (!connsRes.ok) warnings.push(`connections fetch failed: ${connsRes.error}`);
|
|
36039
|
+
if (!activeRes.ok) warnings.push(`active_queries fetch failed: ${activeRes.error}`);
|
|
36040
|
+
if (!tableCountRes.ok) warnings.push(`table_count fetch failed: ${tableCountRes.error}`);
|
|
36041
|
+
return {
|
|
36042
|
+
ok: true,
|
|
36043
|
+
data: {
|
|
36044
|
+
connected: true,
|
|
36045
|
+
version: versionRes.data?.[0]?.version,
|
|
36046
|
+
database: sizeRes.ok ? sizeRes.data?.[0] : null,
|
|
36047
|
+
connections: connsRes.ok ? connsRes.data?.[0] : null,
|
|
36048
|
+
active_queries: activeRes.ok ? activeRes.data : [],
|
|
36049
|
+
table_count: tableCountRes.ok ? tableCountRes.data?.[0]?.count : null,
|
|
36050
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
36051
|
+
}
|
|
36052
|
+
};
|
|
36053
|
+
});
|
|
36007
36054
|
}
|
|
36008
36055
|
}
|
|
36009
36056
|
];
|
|
@@ -36265,48 +36312,53 @@ var schemaTools = [
|
|
|
36265
36312
|
AND child.relispartition
|
|
36266
36313
|
ORDER BY childn.nspname, child.relname
|
|
36267
36314
|
`;
|
|
36268
|
-
|
|
36269
|
-
|
|
36270
|
-
|
|
36271
|
-
|
|
36272
|
-
|
|
36273
|
-
|
|
36274
|
-
|
|
36275
|
-
|
|
36276
|
-
|
|
36277
|
-
|
|
36278
|
-
|
|
36279
|
-
|
|
36280
|
-
|
|
36281
|
-
|
|
36282
|
-
|
|
36283
|
-
|
|
36284
|
-
|
|
36285
|
-
|
|
36286
|
-
if (!fks.ok) warnings.push(`foreign_keys fetch failed: ${fks.error}`);
|
|
36287
|
-
if (!idxs.ok) warnings.push(`indexes fetch failed: ${idxs.error}`);
|
|
36288
|
-
if (!constraints.ok) warnings.push(`constraints fetch failed: ${constraints.error}`);
|
|
36289
|
-
if (!referencedBy.ok) warnings.push(`referenced_by fetch failed: ${referencedBy.error}`);
|
|
36290
|
-
if (!partitionParent.ok) warnings.push(`partition_of fetch failed: ${partitionParent.error}`);
|
|
36291
|
-
if (!partitionChildren.ok) warnings.push(`partitions fetch failed: ${partitionChildren.error}`);
|
|
36292
|
-
const parentRow = partitionParent.ok ? partitionParent.data?.[0] : void 0;
|
|
36293
|
-
return {
|
|
36294
|
-
ok: true,
|
|
36295
|
-
data: {
|
|
36296
|
-
schema,
|
|
36297
|
-
table,
|
|
36298
|
-
kind,
|
|
36299
|
-
columns: cols.data,
|
|
36300
|
-
primary_key: pk.ok ? (pk.data ?? []).map((r) => r.column_name) : [],
|
|
36301
|
-
foreign_keys: fks.ok ? fks.data : [],
|
|
36302
|
-
referenced_by: referencedBy.ok ? referencedBy.data : [],
|
|
36303
|
-
constraints: constraints.ok ? constraints.data : [],
|
|
36304
|
-
indexes: idxs.ok ? idxs.data : [],
|
|
36305
|
-
...parentRow ? { partition_of: parentRow } : {},
|
|
36306
|
-
...partitionChildren.ok && (partitionChildren.data ?? []).length > 0 ? { partitions: partitionChildren.data } : {},
|
|
36307
|
-
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
36315
|
+
return withSharedClient(async (run) => {
|
|
36316
|
+
const [kindRes, cols, pk, fks, idxs, constraints, referencedBy, partitionParent, partitionChildren] = await Promise.all([
|
|
36317
|
+
run(kindQuery, [schema, table]),
|
|
36318
|
+
run(columnsQuery, [schema, table]),
|
|
36319
|
+
run(primaryKeyQuery, [schema, table]),
|
|
36320
|
+
run(foreignKeysQuery, [schema, table]),
|
|
36321
|
+
run(indexesQuery, [schema, table]),
|
|
36322
|
+
run(constraintsQuery, [schema, table]),
|
|
36323
|
+
run(referencedByQuery, [schema, table]),
|
|
36324
|
+
run(partitionParentQuery, [schema, table]),
|
|
36325
|
+
run(partitionChildrenQuery, [schema, table])
|
|
36326
|
+
]);
|
|
36327
|
+
if (!cols.ok) return cols;
|
|
36328
|
+
if (!cols.data || cols.data.length === 0) {
|
|
36329
|
+
return {
|
|
36330
|
+
ok: false,
|
|
36331
|
+
error: `Table ${JSON.stringify(schema)}.${JSON.stringify(table)} not found.`
|
|
36332
|
+
};
|
|
36308
36333
|
}
|
|
36309
|
-
|
|
36334
|
+
const kind = kindRes.ok ? kindRes.data?.[0]?.kind ?? "table" : "table";
|
|
36335
|
+
const warnings = [];
|
|
36336
|
+
if (!pk.ok) warnings.push(`primary_key fetch failed: ${pk.error}`);
|
|
36337
|
+
if (!fks.ok) warnings.push(`foreign_keys fetch failed: ${fks.error}`);
|
|
36338
|
+
if (!idxs.ok) warnings.push(`indexes fetch failed: ${idxs.error}`);
|
|
36339
|
+
if (!constraints.ok) warnings.push(`constraints fetch failed: ${constraints.error}`);
|
|
36340
|
+
if (!referencedBy.ok) warnings.push(`referenced_by fetch failed: ${referencedBy.error}`);
|
|
36341
|
+
if (!partitionParent.ok) warnings.push(`partition_of fetch failed: ${partitionParent.error}`);
|
|
36342
|
+
if (!partitionChildren.ok) warnings.push(`partitions fetch failed: ${partitionChildren.error}`);
|
|
36343
|
+
const parentRow = partitionParent.ok ? partitionParent.data?.[0] : void 0;
|
|
36344
|
+
return {
|
|
36345
|
+
ok: true,
|
|
36346
|
+
data: {
|
|
36347
|
+
schema,
|
|
36348
|
+
table,
|
|
36349
|
+
kind,
|
|
36350
|
+
columns: cols.data,
|
|
36351
|
+
primary_key: pk.ok ? (pk.data ?? []).map((r) => r.column_name) : [],
|
|
36352
|
+
foreign_keys: fks.ok ? fks.data : [],
|
|
36353
|
+
referenced_by: referencedBy.ok ? referencedBy.data : [],
|
|
36354
|
+
constraints: constraints.ok ? constraints.data : [],
|
|
36355
|
+
indexes: idxs.ok ? idxs.data : [],
|
|
36356
|
+
...parentRow ? { partition_of: parentRow } : {},
|
|
36357
|
+
...partitionChildren.ok && (partitionChildren.data ?? []).length > 0 ? { partitions: partitionChildren.data } : {},
|
|
36358
|
+
...warnings.length > 0 ? { _warnings: warnings } : {}
|
|
36359
|
+
}
|
|
36360
|
+
};
|
|
36361
|
+
});
|
|
36310
36362
|
}
|
|
36311
36363
|
},
|
|
36312
36364
|
{
|
|
@@ -36485,19 +36537,20 @@ var statsTools = [
|
|
|
36485
36537
|
const meanCol = useExecSuffix ? "mean_exec_time" : "mean_time";
|
|
36486
36538
|
const minCol = useExecSuffix ? "min_exec_time" : "min_time";
|
|
36487
36539
|
const maxCol = useExecSuffix ? "max_exec_time" : "max_time";
|
|
36488
|
-
const orderCol = orderBy === "total_time" ? totalCol : orderBy === "mean_time" ? meanCol : "calls";
|
|
36540
|
+
const orderCol = orderBy === "total_time" ? totalCol : orderBy === "mean_time" ? meanCol : "pg_stat_statements.calls";
|
|
36489
36541
|
return runInternal(
|
|
36490
|
-
// calls
|
|
36491
|
-
//
|
|
36492
|
-
//
|
|
36542
|
+
// bigint counters (calls, rows) come back as `.text` for lossless
|
|
36543
|
+
// serialization, matching pg_seq_scan_tables / pg_unused_indexes /
|
|
36544
|
+
// pg_table_bloat. Timing fields stay as float8 because they are
|
|
36545
|
+
// inherently fractional milliseconds.
|
|
36493
36546
|
`SELECT
|
|
36494
36547
|
query,
|
|
36495
|
-
calls::
|
|
36548
|
+
calls::text AS calls,
|
|
36496
36549
|
${totalCol}::numeric(18, 2)::float8 AS total_time_ms,
|
|
36497
36550
|
${meanCol}::numeric(18, 2)::float8 AS mean_time_ms,
|
|
36498
36551
|
${minCol}::numeric(18, 2)::float8 AS min_time_ms,
|
|
36499
36552
|
${maxCol}::numeric(18, 2)::float8 AS max_time_ms,
|
|
36500
|
-
rows::
|
|
36553
|
+
rows::text AS rows,
|
|
36501
36554
|
CASE
|
|
36502
36555
|
WHEN (shared_blks_hit + shared_blks_read) > 0
|
|
36503
36556
|
THEN (shared_blks_hit::float8 / (shared_blks_hit + shared_blks_read) * 100)::numeric(5, 2)::float8
|
|
@@ -36594,7 +36647,10 @@ var statsTools = [
|
|
|
36594
36647
|
}
|
|
36595
36648
|
];
|
|
36596
36649
|
function compareVersions(a, b) {
|
|
36597
|
-
const parse3 = (v) => v.split(".").map((
|
|
36650
|
+
const parse3 = (v) => v.split(".").map((seg) => {
|
|
36651
|
+
const m = seg.match(/^\d+/);
|
|
36652
|
+
return m ? Number.parseInt(m[0], 10) : 0;
|
|
36653
|
+
});
|
|
36598
36654
|
const aa = parse3(a);
|
|
36599
36655
|
const bb = parse3(b);
|
|
36600
36656
|
const len = Math.max(aa.length, bb.length);
|
|
@@ -36606,7 +36662,7 @@ function compareVersions(a, b) {
|
|
|
36606
36662
|
}
|
|
36607
36663
|
|
|
36608
36664
|
// src/index.ts
|
|
36609
|
-
var version2 = true ? "0.
|
|
36665
|
+
var version2 = true ? "0.5.0" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
36610
36666
|
var subcommand = process.argv[2];
|
|
36611
36667
|
if (subcommand === "version" || subcommand === "--version") {
|
|
36612
36668
|
console.log(version2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/postgres-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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>",
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"zod": "^4.3.6"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
60
|
+
"node": ">=20"
|
|
61
61
|
}
|
|
62
62
|
}
|