@indiekitai/pg-dash 0.3.9 → 0.4.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # pg-dash
4
4
 
5
- **The AI-native PostgreSQL health checker.** One command to audit your database, 18 MCP tools for AI-assisted optimization, CI integration for automated checks.
5
+ **The AI-native PostgreSQL health checker.** One command to audit your database, 23 MCP tools for AI-assisted optimization, CI integration for automated checks.
6
6
 
7
7
  Not another monitoring dashboard — pg-dash is built to fit into your **AI coding workflow**:
8
8
 
@@ -47,7 +47,7 @@ The Dashboard is there when you need it. But the real power is in the CLI, MCP,
47
47
  | pganalyze | $149+/mo | SaaS signup | ❌ | ❌ |
48
48
  | Grafana+Prometheus | Free | 3 services | ❌ | ❌ |
49
49
  | pgAdmin | Free | Complex UI | ❌ | ❌ |
50
- | **pg-dash** | **Free** | **One command** | **18 MCP tools** | **`--ci --diff`** |
50
+ | **pg-dash** | **Free** | **One command** | **23 MCP tools** | **`--ci --diff`** |
51
51
 
52
52
  ## Features
53
53
 
@@ -118,7 +118,7 @@ The Dashboard is there when you need it. But the real power is in the CLI, MCP,
118
118
  - `pg_dash_compare_env` MCP tool: ask your AI "what's different between local and staging?"
119
119
 
120
120
  ### 🤖 MCP Server
121
- - 18 tools for AI agent integration
121
+ - 23 tools for AI agent integration
122
122
  - `pg-dash-mcp postgres://...` — works with Claude, Cursor, etc.
123
123
 
124
124
  ### 🖥️ CLI
@@ -202,7 +202,7 @@ pg-dash-mcp postgres://user:pass@host/db
202
202
  PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
203
203
  ```
204
204
 
205
- ### Available Tools (18)
205
+ ### Available Tools (23)
206
206
 
207
207
  | Tool | Description |
208
208
  |------|-------------|
@@ -224,6 +224,11 @@ PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
224
224
  | `pg_dash_analyze_query` | Deep EXPLAIN analysis with automatic index suggestions |
225
225
  | `pg_dash_query_regressions` | Detect queries that degraded >50% vs historical baseline |
226
226
  | `pg_dash_compare_env` | Compare schema and health between two database environments |
227
+ | `pg_dash_unused_indexes` | Find unused indexes that waste space and slow down writes |
228
+ | `pg_dash_bloat` | Detect table bloat (dead tuples) that slow down queries |
229
+ | `pg_dash_autovacuum` | Check autovacuum health — which tables are stale or never vacuumed |
230
+ | `pg_dash_locks` | Show active lock waits and long-running blocking queries |
231
+ | `pg_dash_config_check` | Audit PostgreSQL configuration and get tuning recommendations |
227
232
 
228
233
  ## MCP Setup
229
234
 
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # pg-dash
4
4
 
5
- **AI 原生的 PostgreSQL 健康检查工具。** 一条命令审计数据库,18 个 MCP 工具让 AI 帮你优化,CI 集成自动检查。
5
+ **AI 原生的 PostgreSQL 健康检查工具。** 一条命令审计数据库,23 个 MCP 工具让 AI 帮你优化,CI 集成自动检查。
6
6
 
7
7
  不是又一个监控面板 —— pg-dash 是为 **AI 编程工作流** 设计的:
8
8
 
@@ -47,7 +47,7 @@ Dashboard 需要时可以用。但真正的核心能力在 CLI、MCP 和 CI。
47
47
  | pganalyze | $149+/月 | SaaS 注册 | ❌ | ❌ |
48
48
  | Grafana+Prometheus | 免费 | 配置 3 个服务 | ❌ | ❌ |
49
49
  | pgAdmin | 免费 | 界面复杂 | ❌ | ❌ |
50
- | **pg-dash** | **免费** | **一条命令** | **18 个 MCP 工具** | **`--ci --diff`** |
50
+ | **pg-dash** | **免费** | **一条命令** | **23 个 MCP 工具** | **`--ci --diff`** |
51
51
 
52
52
  ## 功能
53
53
 
@@ -118,7 +118,7 @@ Dashboard 需要时可以用。但真正的核心能力在 CLI、MCP 和 CI。
118
118
  - `pg_dash_compare_env` MCP 工具:直接问 AI "本地和预发有什么差异?"
119
119
 
120
120
  ### 🤖 MCP Server
121
- - 18 个工具,支持 AI Agent 集成
121
+ - 23 个工具,支持 AI Agent 集成
122
122
  - `pg-dash-mcp postgres://...` —— 可配合 Claude、Cursor 等使用
123
123
 
124
124
  ### 🖥️ CLI
@@ -202,7 +202,7 @@ pg-dash-mcp postgres://user:pass@host/db
202
202
  PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
203
203
  ```
204
204
 
205
- ### 可用工具(18 个)
205
+ ### 可用工具(23 个)
206
206
 
207
207
  | 工具 | 描述 |
208
208
  |------|------|
@@ -224,6 +224,11 @@ PG_DASH_CONNECTION_STRING=postgres://... pg-dash-mcp
224
224
  | `pg_dash_analyze_query` | 深度 EXPLAIN 分析,自动生成索引建议 |
225
225
  | `pg_dash_query_regressions` | 检测比历史基线慢超过 50% 的查询 |
226
226
  | `pg_dash_compare_env` | 对比两个数据库环境的 Schema 和健康状态 |
227
+ | `pg_dash_unused_indexes` | 发现从未被使用的索引(浪费空间、拖慢写入) |
228
+ | `pg_dash_bloat` | 检测表膨胀(dead tuples 过多) |
229
+ | `pg_dash_autovacuum` | Autovacuum 健康状态——哪些表长期未 vacuum |
230
+ | `pg_dash_locks` | 显示活跃锁等待链和长时间阻塞的查询 |
231
+ | `pg_dash_config_check` | 审计 PostgreSQL 配置,给出调优建议 |
227
232
 
228
233
  ## MCP 配置
229
234
 
package/dist/mcp.js CHANGED
@@ -1902,6 +1902,456 @@ async function analyzeMigration(sql, pool2) {
1902
1902
  };
1903
1903
  }
1904
1904
 
1905
+ // src/server/unused-indexes.ts
1906
+ function formatBytes(bytes) {
1907
+ if (bytes < 1024) return "< 1 KB";
1908
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
1909
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1910
+ if (bytes < 1024 ** 4) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
1911
+ return `${(bytes / 1024 ** 4).toFixed(1)} TB`;
1912
+ }
1913
+ async function getUnusedIndexes(pool2) {
1914
+ const [indexResult, bgwriterResult] = await Promise.all([
1915
+ pool2.query(`
1916
+ SELECT
1917
+ s.schemaname,
1918
+ s.relname AS table_name,
1919
+ s.indexrelname AS index_name,
1920
+ pg_relation_size(s.indexrelid) AS index_size_bytes,
1921
+ s.idx_scan,
1922
+ i.indexdef
1923
+ FROM pg_stat_user_indexes s
1924
+ JOIN pg_indexes i ON s.schemaname = i.schemaname
1925
+ AND s.relname = i.tablename
1926
+ AND s.indexrelname = i.indexname
1927
+ WHERE s.schemaname = 'public'
1928
+ AND s.idx_scan = 0
1929
+ AND i.indexdef NOT LIKE '%UNIQUE%'
1930
+ AND s.indexrelname NOT LIKE '%_pkey'
1931
+ ORDER BY pg_relation_size(s.indexrelid) DESC
1932
+ `),
1933
+ pool2.query(`SELECT stats_reset FROM pg_stat_bgwriter`)
1934
+ ]);
1935
+ const statsReset = bgwriterResult.rows[0]?.stats_reset ? new Date(bgwriterResult.rows[0].stats_reset).toISOString() : null;
1936
+ const indexes = indexResult.rows.map((row) => {
1937
+ const sizeBytes = parseInt(row.index_size_bytes, 10) || 0;
1938
+ const index = row.index_name;
1939
+ const table = row.table_name;
1940
+ return {
1941
+ schema: row.schemaname,
1942
+ table,
1943
+ index,
1944
+ indexSize: formatBytes(sizeBytes),
1945
+ indexSizeBytes: sizeBytes,
1946
+ scans: parseInt(row.idx_scan, 10) || 0,
1947
+ lastUsed: statsReset,
1948
+ suggestion: `Index ${index} on ${table} has never been used (0 scans). Consider dropping it: DROP INDEX CONCURRENTLY "${index.replace(/"/g, '""')}"`
1949
+ };
1950
+ });
1951
+ const totalWastedBytes = indexes.reduce((sum, idx) => sum + idx.indexSizeBytes, 0);
1952
+ return {
1953
+ indexes,
1954
+ totalWastedBytes,
1955
+ totalWasted: formatBytes(totalWastedBytes),
1956
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
1957
+ };
1958
+ }
1959
+
1960
+ // src/server/bloat.ts
1961
+ function getSuggestion(table, bloatPercent) {
1962
+ if (bloatPercent >= 50) {
1963
+ return `HIGH bloat on ${table} (${bloatPercent}% dead rows). Run: VACUUM ANALYZE ${table}`;
1964
+ } else if (bloatPercent >= 20) {
1965
+ return `Moderate bloat on ${table} (${bloatPercent}% dead rows). Consider VACUUM ANALYZE ${table}`;
1966
+ } else {
1967
+ return `Minor bloat on ${table} (${bloatPercent}% dead rows). Autovacuum should handle this.`;
1968
+ }
1969
+ }
1970
+ async function getBloatReport(pool2) {
1971
+ const result = await pool2.query(`
1972
+ SELECT
1973
+ schemaname,
1974
+ relname AS table_name,
1975
+ n_live_tup,
1976
+ n_dead_tup,
1977
+ last_autovacuum,
1978
+ last_vacuum
1979
+ FROM pg_stat_user_tables
1980
+ WHERE schemaname = 'public'
1981
+ AND (n_live_tup + n_dead_tup) > 0
1982
+ ORDER BY (n_dead_tup::float / (n_live_tup + n_dead_tup)) DESC
1983
+ `);
1984
+ const tables = [];
1985
+ for (const row of result.rows) {
1986
+ const live = parseInt(row.n_live_tup, 10) || 0;
1987
+ const dead = parseInt(row.n_dead_tup, 10) || 0;
1988
+ const total = live + dead;
1989
+ if (total === 0) continue;
1990
+ const bloatPercent = Math.round(dead / total * 1e3) / 10;
1991
+ if (bloatPercent < 10) continue;
1992
+ const table = row.table_name;
1993
+ tables.push({
1994
+ schema: row.schemaname,
1995
+ table,
1996
+ liveRows: live,
1997
+ deadRows: dead,
1998
+ bloatPercent,
1999
+ lastAutoVacuum: row.last_autovacuum ? new Date(row.last_autovacuum).toISOString() : null,
2000
+ lastVacuum: row.last_vacuum ? new Date(row.last_vacuum).toISOString() : null,
2001
+ suggestion: getSuggestion(table, bloatPercent)
2002
+ });
2003
+ }
2004
+ tables.sort((a, b) => b.bloatPercent - a.bloatPercent);
2005
+ return {
2006
+ tables,
2007
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
2008
+ };
2009
+ }
2010
+
2011
+ // src/server/autovacuum.ts
2012
+ function classifyStatus(lastAutoVacuum, deadTuples, vacuumCount) {
2013
+ if (lastAutoVacuum === null) return "never";
2014
+ const daysSince = (Date.now() - lastAutoVacuum.getTime()) / (1e3 * 60 * 60 * 24);
2015
+ if (daysSince > 7 && deadTuples > 1e4) return "overdue";
2016
+ if (daysSince > 3) return "stale";
2017
+ return "ok";
2018
+ }
2019
+ function getSuggestion2(status, table) {
2020
+ switch (status) {
2021
+ case "never":
2022
+ return `Table ${table} has never been autovacuumed. Check if autovacuum is enabled and the table has enough churn.`;
2023
+ case "overdue":
2024
+ return `Table ${table} is overdue for vacuum and has many dead tuples. Run: VACUUM ANALYZE ${table}`;
2025
+ case "stale":
2026
+ return `Table ${table} hasn't been vacuumed in over 3 days. Monitor for bloat.`;
2027
+ case "ok":
2028
+ return null;
2029
+ }
2030
+ }
2031
+ async function getAutovacuumReport(pool2) {
2032
+ const [tableResult, settingsResult] = await Promise.all([
2033
+ pool2.query(`
2034
+ SELECT
2035
+ schemaname, relname,
2036
+ last_autovacuum, last_autoanalyze,
2037
+ n_dead_tup, n_live_tup,
2038
+ autovacuum_count, autoanalyze_count
2039
+ FROM pg_stat_user_tables
2040
+ WHERE schemaname = 'public'
2041
+ ORDER BY n_dead_tup DESC
2042
+ `),
2043
+ pool2.query(`
2044
+ SELECT name, setting
2045
+ FROM pg_settings
2046
+ WHERE name IN ('autovacuum', 'autovacuum_vacuum_cost_delay', 'autovacuum_max_workers', 'autovacuum_naptime')
2047
+ `)
2048
+ ]);
2049
+ const tables = tableResult.rows.map((row) => {
2050
+ const lastAutoVacuumDate = row.last_autovacuum ? new Date(row.last_autovacuum) : null;
2051
+ const deadTuples = parseInt(row.n_dead_tup, 10) || 0;
2052
+ const liveTuples = parseInt(row.n_live_tup, 10) || 0;
2053
+ const vacuumCount = parseInt(row.autovacuum_count, 10) || 0;
2054
+ const analyzeCount = parseInt(row.autoanalyze_count, 10) || 0;
2055
+ const status = classifyStatus(lastAutoVacuumDate, deadTuples, vacuumCount);
2056
+ const table = row.relname;
2057
+ return {
2058
+ schema: row.schemaname,
2059
+ table,
2060
+ lastAutoVacuum: lastAutoVacuumDate ? lastAutoVacuumDate.toISOString() : null,
2061
+ lastAutoAnalyze: row.last_autoanalyze ? new Date(row.last_autoanalyze).toISOString() : null,
2062
+ deadTuples,
2063
+ liveTuples,
2064
+ vacuumCount,
2065
+ analyzeCount,
2066
+ status,
2067
+ suggestion: getSuggestion2(status, table)
2068
+ };
2069
+ });
2070
+ const settingsMap = /* @__PURE__ */ new Map();
2071
+ for (const row of settingsResult.rows) {
2072
+ settingsMap.set(row.name, row.setting);
2073
+ }
2074
+ return {
2075
+ tables,
2076
+ settings: {
2077
+ autovacuumEnabled: settingsMap.get("autovacuum") !== "off",
2078
+ vacuumCostDelay: `${settingsMap.get("autovacuum_vacuum_cost_delay") ?? "2"}ms`,
2079
+ autovacuumMaxWorkers: parseInt(settingsMap.get("autovacuum_max_workers") ?? "3", 10),
2080
+ autovacuumNaptime: `${settingsMap.get("autovacuum_naptime") ?? "60"}s`
2081
+ },
2082
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
2083
+ };
2084
+ }
2085
+
2086
+ // src/server/locks.ts
2087
+ function formatDurationSecs(secs) {
2088
+ const h = Math.floor(secs / 3600);
2089
+ const m = Math.floor(secs % 3600 / 60);
2090
+ const s = secs % 60;
2091
+ return [
2092
+ String(h).padStart(2, "0"),
2093
+ String(m).padStart(2, "0"),
2094
+ String(s).padStart(2, "0")
2095
+ ].join(":");
2096
+ }
2097
+ async function getLockReport(pool2) {
2098
+ const [locksResult, longResult] = await Promise.all([
2099
+ pool2.query(`
2100
+ SELECT
2101
+ blocked.pid AS blocked_pid,
2102
+ blocked.query AS blocked_query,
2103
+ EXTRACT(EPOCH FROM (NOW() - blocked.query_start))::int AS blocked_secs,
2104
+ blocking.pid AS blocking_pid,
2105
+ blocking.query AS blocking_query,
2106
+ EXTRACT(EPOCH FROM (NOW() - blocking.query_start))::int AS blocking_secs,
2107
+ blocked_locks.relation::regclass::text AS table_name,
2108
+ blocked_locks.locktype
2109
+ FROM pg_catalog.pg_locks blocked_locks
2110
+ JOIN pg_catalog.pg_stat_activity blocked ON blocked.pid = blocked_locks.pid
2111
+ JOIN pg_catalog.pg_locks blocking_locks
2112
+ ON blocking_locks.locktype = blocked_locks.locktype
2113
+ AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
2114
+ AND blocking_locks.pid != blocked_locks.pid
2115
+ AND blocking_locks.granted = true
2116
+ JOIN pg_catalog.pg_stat_activity blocking ON blocking.pid = blocking_locks.pid
2117
+ WHERE NOT blocked_locks.granted
2118
+ `),
2119
+ pool2.query(`
2120
+ SELECT
2121
+ pid,
2122
+ EXTRACT(EPOCH FROM (NOW() - query_start))::int AS duration_secs,
2123
+ query,
2124
+ state,
2125
+ wait_event_type
2126
+ FROM pg_stat_activity
2127
+ WHERE state != 'idle'
2128
+ AND query_start IS NOT NULL
2129
+ AND EXTRACT(EPOCH FROM (NOW() - query_start)) > 5
2130
+ AND query NOT LIKE '%pg_stat_activity%'
2131
+ ORDER BY duration_secs DESC
2132
+ LIMIT 20
2133
+ `)
2134
+ ]);
2135
+ const seen = /* @__PURE__ */ new Set();
2136
+ const waitingLocks = [];
2137
+ for (const row of locksResult.rows) {
2138
+ const key = `${row.blocked_pid}:${row.blocking_pid}`;
2139
+ if (!seen.has(key)) {
2140
+ seen.add(key);
2141
+ waitingLocks.push({
2142
+ blockedPid: parseInt(row.blocked_pid, 10),
2143
+ blockedQuery: row.blocked_query,
2144
+ blockedDuration: formatDurationSecs(parseInt(row.blocked_secs, 10) || 0),
2145
+ blockingPid: parseInt(row.blocking_pid, 10),
2146
+ blockingQuery: row.blocking_query,
2147
+ blockingDuration: formatDurationSecs(parseInt(row.blocking_secs, 10) || 0),
2148
+ table: row.table_name ?? null,
2149
+ lockType: row.locktype
2150
+ });
2151
+ }
2152
+ }
2153
+ const longRunningQueries = longResult.rows.map((row) => ({
2154
+ pid: parseInt(row.pid, 10),
2155
+ duration: formatDurationSecs(parseInt(row.duration_secs, 10) || 0),
2156
+ query: row.query,
2157
+ state: row.state,
2158
+ waitEventType: row.wait_event_type ?? null
2159
+ }));
2160
+ return {
2161
+ waitingLocks,
2162
+ longRunningQueries,
2163
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
2164
+ };
2165
+ }
2166
+
2167
+ // src/server/config-checker.ts
2168
+ function settingToBytes(value, unit) {
2169
+ const v = parseFloat(value);
2170
+ if (!unit) return v;
2171
+ switch (unit.toLowerCase()) {
2172
+ case "b":
2173
+ return v;
2174
+ case "kb":
2175
+ return v * 1024;
2176
+ case "8kb":
2177
+ return v * 8 * 1024;
2178
+ // shared_buffers, effective_cache_size
2179
+ case "mb":
2180
+ return v * 1024 * 1024;
2181
+ case "gb":
2182
+ return v * 1024 * 1024 * 1024;
2183
+ default:
2184
+ return v;
2185
+ }
2186
+ }
2187
+ function settingToMb(value, unit) {
2188
+ return settingToBytes(value, unit) / (1024 * 1024);
2189
+ }
2190
+ function formatMemSetting(rawValue, unit) {
2191
+ if (!rawValue) return "unknown";
2192
+ const bytes = settingToBytes(rawValue, unit ?? "");
2193
+ if (bytes <= 0 || isNaN(bytes)) return rawValue;
2194
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}GB`;
2195
+ if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)}MB`;
2196
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
2197
+ return `${bytes}B`;
2198
+ }
2199
+ async function getConfigReport(pool2) {
2200
+ const result = await pool2.query(`
2201
+ SELECT name, setting, unit
2202
+ FROM pg_settings
2203
+ WHERE name IN (
2204
+ 'max_connections', 'shared_buffers', 'work_mem',
2205
+ 'effective_cache_size', 'maintenance_work_mem', 'wal_buffers',
2206
+ 'checkpoint_completion_target', 'random_page_cost',
2207
+ 'autovacuum_vacuum_scale_factor', 'autovacuum_analyze_scale_factor',
2208
+ 'log_min_duration_statement', 'idle_in_transaction_session_timeout',
2209
+ 'effective_io_concurrency'
2210
+ )
2211
+ `);
2212
+ const settings = {};
2213
+ for (const row of result.rows) {
2214
+ settings[row.name] = { setting: row.setting, unit: row.unit ?? void 0 };
2215
+ }
2216
+ const recommendations = [];
2217
+ const get = (name) => settings[name]?.setting ?? null;
2218
+ const getUnit = (name) => settings[name]?.unit;
2219
+ const sharedBuffersSetting = get("shared_buffers");
2220
+ if (sharedBuffersSetting !== null) {
2221
+ const mb = settingToMb(sharedBuffersSetting, getUnit("shared_buffers"));
2222
+ if (mb < 128) {
2223
+ recommendations.push({
2224
+ setting: "shared_buffers",
2225
+ currentValue: `${Math.round(mb)}MB`,
2226
+ recommendedValue: "256MB",
2227
+ reason: "shared_buffers should be at least 25% of RAM; typical starting point is 256MB\u20131GB",
2228
+ severity: "warning",
2229
+ docs: "https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS"
2230
+ });
2231
+ }
2232
+ }
2233
+ const workMemSetting = get("work_mem");
2234
+ if (workMemSetting !== null) {
2235
+ const mb = settingToMb(workMemSetting, getUnit("work_mem"));
2236
+ if (mb <= 4) {
2237
+ recommendations.push({
2238
+ setting: "work_mem",
2239
+ currentValue: "4MB",
2240
+ recommendedValue: "16MB",
2241
+ reason: "work_mem of 4MB is conservative; consider 16MB\u201364MB for analytical queries (but multiply by max_connections for total)",
2242
+ severity: "info",
2243
+ docs: "https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM"
2244
+ });
2245
+ }
2246
+ }
2247
+ const cctSetting = get("checkpoint_completion_target");
2248
+ if (cctSetting !== null) {
2249
+ const v = parseFloat(cctSetting);
2250
+ if (v < 0.9) {
2251
+ recommendations.push({
2252
+ setting: "checkpoint_completion_target",
2253
+ currentValue: cctSetting,
2254
+ recommendedValue: "0.9",
2255
+ reason: "Set to 0.9 to spread checkpoint I/O over 90% of checkpoint interval",
2256
+ severity: "warning",
2257
+ docs: "https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-CHECKPOINT-COMPLETION-TARGET"
2258
+ });
2259
+ }
2260
+ }
2261
+ const rpcSetting = get("random_page_cost");
2262
+ if (rpcSetting !== null) {
2263
+ const v = parseFloat(rpcSetting);
2264
+ if (v > 2) {
2265
+ recommendations.push({
2266
+ setting: "random_page_cost",
2267
+ currentValue: rpcSetting,
2268
+ recommendedValue: "1.1",
2269
+ reason: "If using SSDs, set random_page_cost=1.1 (default 4.0 is tuned for spinning disks)",
2270
+ severity: "info",
2271
+ docs: "https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-RANDOM-PAGE-COST"
2272
+ });
2273
+ }
2274
+ }
2275
+ const avsfSetting = get("autovacuum_vacuum_scale_factor");
2276
+ if (avsfSetting !== null) {
2277
+ const v = parseFloat(avsfSetting);
2278
+ if (v >= 0.2) {
2279
+ recommendations.push({
2280
+ setting: "autovacuum_vacuum_scale_factor",
2281
+ currentValue: avsfSetting,
2282
+ recommendedValue: "0.05",
2283
+ reason: "Consider lowering to 0.05\u20130.1 for large tables to vacuum more frequently",
2284
+ severity: "info",
2285
+ docs: "https://www.postgresql.org/docs/current/runtime-config-autovacuum.html#GUC-AUTOVACUUM-VACUUM-SCALE-FACTOR"
2286
+ });
2287
+ }
2288
+ }
2289
+ const lmdsSetting = get("log_min_duration_statement");
2290
+ if (lmdsSetting !== null && parseInt(lmdsSetting, 10) === -1) {
2291
+ recommendations.push({
2292
+ setting: "log_min_duration_statement",
2293
+ currentValue: "-1",
2294
+ recommendedValue: "1000",
2295
+ reason: "Consider setting to 1000 (log queries > 1s) for performance monitoring",
2296
+ severity: "info",
2297
+ docs: "https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-MIN-DURATION-STATEMENT"
2298
+ });
2299
+ }
2300
+ const iitsSetting = get("idle_in_transaction_session_timeout");
2301
+ if (iitsSetting !== null && parseInt(iitsSetting, 10) === 0) {
2302
+ recommendations.push({
2303
+ setting: "idle_in_transaction_session_timeout",
2304
+ currentValue: "0",
2305
+ recommendedValue: "60000",
2306
+ reason: "Set idle_in_transaction_session_timeout=60000 (60s) to prevent stuck transactions from holding locks",
2307
+ severity: "warning",
2308
+ docs: "https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT"
2309
+ });
2310
+ }
2311
+ const eicSetting = get("effective_io_concurrency");
2312
+ if (eicSetting !== null && parseInt(eicSetting, 10) === 1) {
2313
+ recommendations.push({
2314
+ setting: "effective_io_concurrency",
2315
+ currentValue: "1",
2316
+ recommendedValue: "200",
2317
+ reason: "If using SSDs, set effective_io_concurrency=200 for better parallel I/O",
2318
+ severity: "info",
2319
+ docs: "https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-EFFECTIVE-IO-CONCURRENCY"
2320
+ });
2321
+ }
2322
+ const mwmSetting = get("maintenance_work_mem");
2323
+ if (mwmSetting !== null) {
2324
+ const mb = settingToMb(mwmSetting, getUnit("maintenance_work_mem"));
2325
+ if (mb <= 64) {
2326
+ recommendations.push({
2327
+ setting: "maintenance_work_mem",
2328
+ currentValue: "64MB",
2329
+ recommendedValue: "256MB",
2330
+ reason: "Consider 256MB for faster VACUUM and index builds",
2331
+ severity: "info",
2332
+ docs: "https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM"
2333
+ });
2334
+ }
2335
+ }
2336
+ const maxConnSetting = get("max_connections");
2337
+ const serverInfo = {
2338
+ maxConnections: maxConnSetting !== null ? parseInt(maxConnSetting, 10) : 0,
2339
+ sharedBuffers: formatMemSetting(sharedBuffersSetting, getUnit("shared_buffers")),
2340
+ workMem: formatMemSetting(workMemSetting, getUnit("work_mem")),
2341
+ effectiveCacheSize: formatMemSetting(get("effective_cache_size"), getUnit("effective_cache_size")),
2342
+ maintenanceWorkMem: formatMemSetting(mwmSetting, getUnit("maintenance_work_mem")),
2343
+ walBuffers: get("wal_buffers") ?? "",
2344
+ checkpointCompletionTarget: cctSetting ?? "",
2345
+ randomPageCost: rpcSetting ?? "",
2346
+ autovacuumVacuumScaleFactor: avsfSetting ?? ""
2347
+ };
2348
+ return {
2349
+ recommendations,
2350
+ serverInfo,
2351
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
2352
+ };
2353
+ }
2354
+
1905
2355
  // src/mcp.ts
1906
2356
  import Database2 from "better-sqlite3";
1907
2357
  import path3 from "path";
@@ -2253,6 +2703,46 @@ server.tool(
2253
2703
  }
2254
2704
  }
2255
2705
  );
2706
+ server.tool("pg_dash_unused_indexes", "Find unused indexes that waste space and slow down writes", {}, async () => {
2707
+ try {
2708
+ const report = await getUnusedIndexes(pool);
2709
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
2710
+ } catch (err) {
2711
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
2712
+ }
2713
+ });
2714
+ server.tool("pg_dash_bloat", "Detect table bloat (dead tuples) that slow down queries", {}, async () => {
2715
+ try {
2716
+ const report = await getBloatReport(pool);
2717
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
2718
+ } catch (err) {
2719
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
2720
+ }
2721
+ });
2722
+ server.tool("pg_dash_autovacuum", "Check autovacuum health \u2014 which tables are stale or never vacuumed", {}, async () => {
2723
+ try {
2724
+ const report = await getAutovacuumReport(pool);
2725
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
2726
+ } catch (err) {
2727
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
2728
+ }
2729
+ });
2730
+ server.tool("pg_dash_locks", "Show active lock waits and long-running queries blocking the database", {}, async () => {
2731
+ try {
2732
+ const report = await getLockReport(pool);
2733
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
2734
+ } catch (err) {
2735
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
2736
+ }
2737
+ });
2738
+ server.tool("pg_dash_config_check", "Audit PostgreSQL configuration settings and get tuning recommendations", {}, async () => {
2739
+ try {
2740
+ const report = await getConfigReport(pool);
2741
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
2742
+ } catch (err) {
2743
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
2744
+ }
2745
+ });
2256
2746
  var transport = new StdioServerTransport();
2257
2747
  await server.connect(transport);
2258
2748
  //# sourceMappingURL=mcp.js.map