@deepsql/mcp 0.17.0 → 0.18.1
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/CLAUDE.md +11 -2
- package/README.md +14 -2
- package/deepsql-phase1-lib.js +291 -0
- package/package.json +4 -1
- package/skills/SKILL_BODY.md +14 -3
- package/src/commands/slow-queries.js +145 -1
package/CLAUDE.md
CHANGED
|
@@ -33,7 +33,7 @@ server, and the statement you ran.** Don't be sloppy.
|
|
|
33
33
|
|
|
34
34
|
## The tools you have
|
|
35
35
|
|
|
36
|
-
The MCP server exposes
|
|
36
|
+
The MCP server exposes 21 tools. They all take a `connectionId` (UUID
|
|
37
37
|
returned by `list_connections`) except `apply_index_recommendation`,
|
|
38
38
|
which takes a server-resolved `recommendationId`.
|
|
39
39
|
|
|
@@ -49,6 +49,11 @@ which takes a server-resolved `recommendationId`.
|
|
|
49
49
|
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples. Read-only; doesn't trigger new work. |
|
|
50
50
|
| `get_slow_query_timeline` | Day-by-day timeline for one query from the 30-day analytics store — call count, mean/max time, regression factor per day. Identify the query by its fingerprint (the `queryId` from `analyze_slow_queries`). Answers "is this query getting slower". |
|
|
51
51
|
| `get_query_regressions` | Slow queries that regressed (got slower) on the latest daily analysis run, ranked by slowdown factor. Read-only. |
|
|
52
|
+
| `list_tracked_queries` | All slow-query fingerprints in the 30-day analytics store, with call counts, mean/max exec time, and regression flags. Use to discover what's worth drilling into before pulling a timeline or samples. |
|
|
53
|
+
| `get_slow_query_customers` | Tenants/customers ranked by total slow-query time. Includes resolved customer name when a lookup table is configured. Answers "which customer is driving the load?" |
|
|
54
|
+
| `get_query_samples` | Literal SQL samples (with actual bind values substituted) for a fingerprint, slowest-first. Use to reproduce an execution, get a real EXPLAIN plan, or see how different callers use the same query shape. |
|
|
55
|
+
| `get_slow_query_insights` | Pre-computed AI insights for slow queries grouped by `kind`: `hotspots` (most total DB time), `remediation` (actionable fixes), `tail-risk` (p95/max outliers), `plan-drift` (execution plan changed), `skew` (one tenant disproportionately loaded). Default `all` returns the combined list. Accepts `window` (`LAST_24_HOURS` / `LAST_7_DAYS` / `LAST_30_DAYS`) and `limit`. |
|
|
56
|
+
| `optimize_slow_query` | AI-generated optimization recommendations for a specific SQL — index suggestions, query rewrites, and estimated impact. Synchronous (non-streaming); pass `avgExecutionTimeMs` to anchor the impact estimate. |
|
|
52
57
|
| `get_index_recommendations` | **Workload-weighted DBA-grade index advisor.** Pre-computed top-N (default 5) recommendations ranked by net benefit (`Σ calls × mean_exec_time` − write-cost). Each result carries up to 5 contributing query fingerprints, the role each column played, and optional HypoPG cost-delta on Postgres. Covers both `CREATE_INDEX` and `DROP_INDEX` (unused + redundant-prefix) candidates. |
|
|
53
58
|
| **`apply_index_recommendation`** | **The only write-capable MCP tool.** Apply (or dry-run) a recommendation against its target connection and measure the before/after benefit on contributing queries. `DRY_RUN` (default) uses HypoPG (Postgres-only) for zero-write cost-delta. `APPLY` runs real `CREATE/DROP INDEX CONCURRENTLY` (configurable via `concurrent`). `APPLY_AND_MEASURE` additionally runs `EXPLAIN ANALYZE` for wall-clock timings. Write modes require `confirm: true`. The DDL is server-generated from the recommendation row — clients never supply SQL. |
|
|
54
59
|
| **`execute_sql`** | **Run any SQL statement.** Policy is server-enforced: developers can run SELECT/WITH/SHOW/EXPLAIN; admins can also run DML/DDL with a two-step confirmation. EXPLAIN and EXPLAIN ANALYZE are just SQL — no separate flag. |
|
|
@@ -335,7 +340,11 @@ them at the terminal command rather than trying to fake it through
|
|
|
335
340
|
| Catalog: per-table index usage stats | `deepsql indexes usage <table>` |
|
|
336
341
|
| Catalog: index health report | `deepsql indexes health` |
|
|
337
342
|
| Daily digest (anomalies + AI commentary) | `deepsql digest`, `deepsql digest 7` |
|
|
338
|
-
| Streaming AI optimization for a slow query | `deepsql slow-queries optimize --query-id <id>` |
|
|
343
|
+
| Streaming AI optimization for a slow query | `deepsql slow-queries optimize --query-id <id>` (or MCP `optimize_slow_query` for a synchronous version) |
|
|
344
|
+
| Customers driving the most slow-query load | `deepsql slow-queries customers --connection <c>` (or MCP `get_slow_query_customers`) |
|
|
345
|
+
| Literal SQL samples for a fingerprint | `deepsql slow-queries samples <fingerprint> --connection <c>` (or MCP `get_query_samples`) |
|
|
346
|
+
| AI-flagged hotspots / tail-risk / plan-drift | `deepsql slow-queries insights --connection <c> [--kind hotspots]` (or MCP `get_slow_query_insights`) |
|
|
347
|
+
| Trigger an immediate daily analysis run | `deepsql slow-queries trigger --connection <c>` |
|
|
339
348
|
|
|
340
349
|
These are reachable from any terminal where `deepsql` is installed and
|
|
341
350
|
logged in; the saved profile is shared with the MCP server.
|
package/README.md
CHANGED
|
@@ -52,8 +52,9 @@ Restart the editor for the entry to load.
|
|
|
52
52
|
|
|
53
53
|
## What the MCP server exposes
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
at the SQL layer
|
|
55
|
+
21 tools, all read-only at the schema/retrieval layer and policy-gated
|
|
56
|
+
at the SQL layer (only `execute_sql`, `analyze_query_plan` with
|
|
57
|
+
`useAnalyze=true`, and `apply_index_recommendation` can write):
|
|
57
58
|
|
|
58
59
|
| Tool | Purpose |
|
|
59
60
|
|---|---|
|
|
@@ -64,7 +65,18 @@ at the SQL layer:
|
|
|
64
65
|
| `list_business_rules` | Active business rules and SQL guardrails for a connection |
|
|
65
66
|
| `get_relationships` | Inferred + validated foreign keys with confidence scores |
|
|
66
67
|
| `get_anti_patterns` | Schema-level or query-level anti-patterns |
|
|
68
|
+
| `get_index_recommendations` | Pre-computed top-N workload-weighted index recommendations |
|
|
69
|
+
| `apply_index_recommendation` | Apply (or dry-run with HypoPG) an index recommendation and measure benefit |
|
|
67
70
|
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples |
|
|
71
|
+
| `get_slow_query_timeline` | Day-by-day timeline for one fingerprint from the 30-day analytics store |
|
|
72
|
+
| `get_query_regressions` | Slow queries that regressed on the latest daily run |
|
|
73
|
+
| `list_tracked_queries` | All fingerprints tracked in the 30-day store |
|
|
74
|
+
| `get_slow_query_customers` | Tenants ranked by total slow-query load |
|
|
75
|
+
| `get_query_samples` | Literal SQL samples (with bind values) for one fingerprint |
|
|
76
|
+
| `get_slow_query_insights` | Pre-computed AI insights — hotspots / remediation / tail-risk / plan-drift / skew |
|
|
77
|
+
| `optimize_slow_query` | AI optimization recommendations (index DDL + query rewrites) for a specific SQL |
|
|
78
|
+
| `get_table_growth` | Per-table size/row growth from persistent stats history |
|
|
79
|
+
| `get_growth_anomalies` | DeepSQL-flagged sudden growth spikes with severity and root-cause hints |
|
|
68
80
|
| `execute_sql` | Run any SQL — backend enforces role-based policy (developers read-only, admins can mutate with two-step confirm) |
|
|
69
81
|
| `analyze_query_plan` | AI-enriched plan analysis (parsed plan tree, performance issues, index recommendations, written summary that uses the connection's schema + business rules) |
|
|
70
82
|
|
package/deepsql-phase1-lib.js
CHANGED
|
@@ -258,6 +258,120 @@ const TOOL_DEFINITIONS = [
|
|
|
258
258
|
additionalProperties: false,
|
|
259
259
|
},
|
|
260
260
|
},
|
|
261
|
+
{
|
|
262
|
+
name: "list_tracked_queries",
|
|
263
|
+
description:
|
|
264
|
+
"List all slow query fingerprints tracked in DeepSQL's 30-day rolling analytics store. "
|
|
265
|
+
+ "Each entry carries the stable fingerprint (MD5 of normalized SQL), a normalized query "
|
|
266
|
+
+ "sample, delta call count, mean/max execution time, and the regression factor versus the "
|
|
267
|
+
+ "previous day. Use this to discover which queries are worth investigating before pulling "
|
|
268
|
+
+ "timelines or samples.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
273
|
+
},
|
|
274
|
+
required: ["connectionId"],
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "get_slow_query_customers",
|
|
280
|
+
description:
|
|
281
|
+
"List all tenants / customers ranked by total slow-query time for a connection. "
|
|
282
|
+
+ "Each entry includes the raw customer id, resolved customer name (when a lookup "
|
|
283
|
+
+ "table is configured), total number of distinct slow queries attributed to them, "
|
|
284
|
+
+ "and the total cumulative execution time. Use this to answer 'which customer is "
|
|
285
|
+
+ "driving the most database load?'",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
290
|
+
},
|
|
291
|
+
required: ["connectionId"],
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "get_query_samples",
|
|
297
|
+
description:
|
|
298
|
+
"Retrieve literal SQL samples (with actual bind values substituted in) for a specific "
|
|
299
|
+
+ "query fingerprint, ordered slowest-first. Samples are captured from the slow-query log "
|
|
300
|
+
+ "or performance-schema and stored in DeepSQL's analytics store. Use these to reproduce "
|
|
301
|
+
+ "a slow execution, get a real EXPLAIN plan, or understand how different callers use the "
|
|
302
|
+
+ "same query shape.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
307
|
+
fingerprint: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Stable query fingerprint (MD5 of the normalized query).",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
required: ["connectionId", "fingerprint"],
|
|
313
|
+
additionalProperties: false,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "get_slow_query_insights",
|
|
318
|
+
description:
|
|
319
|
+
"Retrieve AI-enriched slow-query insights for a connection. Insights are pre-computed "
|
|
320
|
+
+ "by DeepSQL's daily analysis and grouped into kinds: `hotspots` (queries consuming "
|
|
321
|
+
+ "the most total DB time), `remediation` (actionable fix recommendations), `tail-risk` "
|
|
322
|
+
+ "(high-variance queries with dangerous p95/max outliers), `plan-drift` (queries whose "
|
|
323
|
+
+ "execution plan changed), and `skew` (queries showing disproportionate load from one "
|
|
324
|
+
+ "tenant). Use `kind=all` (default) for the combined list.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {
|
|
328
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
329
|
+
kind: {
|
|
330
|
+
type: "string",
|
|
331
|
+
enum: ["all", "hotspots", "remediation", "tail-risk", "plan-drift", "skew"],
|
|
332
|
+
description: "Insight category to fetch. Defaults to 'all'.",
|
|
333
|
+
},
|
|
334
|
+
window: {
|
|
335
|
+
type: "string",
|
|
336
|
+
enum: ["LAST_24_HOURS", "LAST_7_DAYS", "LAST_30_DAYS"],
|
|
337
|
+
description: "Time window for the insights. Defaults to 'LAST_7_DAYS'.",
|
|
338
|
+
},
|
|
339
|
+
limit: {
|
|
340
|
+
type: "integer",
|
|
341
|
+
minimum: 1,
|
|
342
|
+
maximum: 100,
|
|
343
|
+
description: "Maximum number of insights to return. Defaults to 10.",
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
required: ["connectionId"],
|
|
347
|
+
additionalProperties: false,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "optimize_slow_query",
|
|
352
|
+
description:
|
|
353
|
+
"Get AI-generated optimization recommendations for a specific slow query — including "
|
|
354
|
+
+ "index suggestions, query rewrites, and expected performance impact. DeepSQL inspects "
|
|
355
|
+
+ "the query against the connection's schema, existing indexes, and workload patterns. "
|
|
356
|
+
+ "Pass `avgExecutionTimeMs` to anchor the impact estimate to a real baseline.",
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: {
|
|
360
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
361
|
+
queryText: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "The SQL query to optimize. Literal or normalized form both accepted.",
|
|
364
|
+
},
|
|
365
|
+
avgExecutionTimeMs: {
|
|
366
|
+
type: "number",
|
|
367
|
+
minimum: 0,
|
|
368
|
+
description: "Optional observed average execution time in ms — anchors impact estimates.",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
required: ["connectionId", "queryText"],
|
|
372
|
+
additionalProperties: false,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
261
375
|
{
|
|
262
376
|
name: "get_table_growth",
|
|
263
377
|
description:
|
|
@@ -841,6 +955,101 @@ function summarizeSlowQueries(payload) {
|
|
|
841
955
|
+ parts.join("");
|
|
842
956
|
}
|
|
843
957
|
|
|
958
|
+
function summarizeTrackedQueries(payload) {
|
|
959
|
+
const list = Array.isArray(payload) ? payload : [];
|
|
960
|
+
if (list.length === 0) {
|
|
961
|
+
return "No tracked queries yet. The daily analysis runs at 01:30, or trigger one with trigger_slow_query_analysis.";
|
|
962
|
+
}
|
|
963
|
+
const top = list
|
|
964
|
+
.slice()
|
|
965
|
+
.sort((a, b) => (b.meanExecMs ?? 0) - (a.meanExecMs ?? 0))
|
|
966
|
+
.slice(0, 5)
|
|
967
|
+
.map((q) => {
|
|
968
|
+
const fp = String(q.fingerprint || "").slice(0, 8);
|
|
969
|
+
const ms = q.meanExecMs != null ? `${Math.round(q.meanExecMs)}ms` : "?ms";
|
|
970
|
+
const calls = q.callsDelta != null ? ` ×${q.callsDelta}` : "";
|
|
971
|
+
const reg = q.regressionFactor != null && q.regressionFactor > 1
|
|
972
|
+
? ` ⚠ ${q.regressionFactor.toFixed(2)}x slower` : "";
|
|
973
|
+
const sql = String(q.normalizedSql || "").replace(/\s+/g, " ").slice(0, 60);
|
|
974
|
+
return ` ${fp}… avg=${ms}${calls}${reg} — ${sql}`;
|
|
975
|
+
});
|
|
976
|
+
const regCount = list.filter((q) => q.regressionFactor != null && q.regressionFactor > 1).length;
|
|
977
|
+
return `${list.length} tracked query/queries (30-day window)${regCount > 0 ? `, ${regCount} regressed` : ""}.\nSlowest:\n${top.join("\n")}`;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function summarizeSlowQueryCustomers(payload) {
|
|
981
|
+
const list = Array.isArray(payload) ? payload : [];
|
|
982
|
+
if (list.length === 0) {
|
|
983
|
+
return "No customer attribution data yet. Configure a tenant column in slow-query analytics settings.";
|
|
984
|
+
}
|
|
985
|
+
const top = list.slice(0, 5).map((c, i) => {
|
|
986
|
+
const name = c.customerName || c.customerId || "unknown";
|
|
987
|
+
const queries = c.queryCount != null ? `${c.queryCount} queries` : "";
|
|
988
|
+
const totalMs = c.totalExecMs != null ? `, ${Math.round(c.totalExecMs / 1000)}s total` : "";
|
|
989
|
+
return ` ${i + 1}. ${name}${queries ? ` — ${queries}` : ""}${totalMs}`;
|
|
990
|
+
});
|
|
991
|
+
return `${list.length} customer(s) with slow queries. Top by load:\n${top.join("\n")}`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function summarizeQuerySamples(payload) {
|
|
995
|
+
const list = Array.isArray(payload) ? payload : [];
|
|
996
|
+
if (list.length === 0) {
|
|
997
|
+
return "No samples found for this query fingerprint.";
|
|
998
|
+
}
|
|
999
|
+
const times = list.map((s) => s.execMs).filter((t) => t != null);
|
|
1000
|
+
const minMs = times.length ? Math.min(...times) : null;
|
|
1001
|
+
const maxMs = times.length ? Math.max(...times) : null;
|
|
1002
|
+
const avgMs = times.length ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : null;
|
|
1003
|
+
const sources = [...new Set(list.map((s) => s.source).filter(Boolean))].join(", ");
|
|
1004
|
+
return `${list.length} sample(s)`
|
|
1005
|
+
+ (minMs != null ? `, min=${minMs}ms avg=${avgMs}ms max=${maxMs}ms` : "")
|
|
1006
|
+
+ (sources ? `, source: ${sources}` : "")
|
|
1007
|
+
+ `. Use rawSql for EXPLAIN.`;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function summarizeSlowQueryInsights(payload) {
|
|
1011
|
+
const list = Array.isArray(payload) ? payload : [];
|
|
1012
|
+
if (list.length === 0) {
|
|
1013
|
+
return "No insights found for the requested window/kind. Run analyze-now or wait for the next daily pass.";
|
|
1014
|
+
}
|
|
1015
|
+
const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
1016
|
+
for (const ins of list) {
|
|
1017
|
+
const sev = (ins.severity || "LOW").toUpperCase();
|
|
1018
|
+
if (sev in bySeverity) bySeverity[sev]++;
|
|
1019
|
+
}
|
|
1020
|
+
const sevLine = Object.entries(bySeverity)
|
|
1021
|
+
.filter(([, n]) => n > 0)
|
|
1022
|
+
.map(([k, n]) => `${n} ${k.toLowerCase()}`)
|
|
1023
|
+
.join(", ");
|
|
1024
|
+
const top = list.slice(0, 3).map((ins, i) => {
|
|
1025
|
+
const sev = ins.severity ? `[${ins.severity}] ` : "";
|
|
1026
|
+
const title = ins.title || ins.insightType || "insight";
|
|
1027
|
+
const desc = String(ins.description || ins.message || "").replace(/\s+/g, " ").slice(0, 120);
|
|
1028
|
+
return ` ${i + 1}. ${sev}${title}${desc ? ` — ${desc}` : ""}`;
|
|
1029
|
+
});
|
|
1030
|
+
return `${list.length} insight(s) (${sevLine || "none by severity"}).\n${top.join("\n")}`;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function summarizeSlowQueryOptimization(payload) {
|
|
1034
|
+
if (!payload) return "No optimization result returned.";
|
|
1035
|
+
const recList = Array.isArray(payload.recommendations) ? payload.recommendations : [];
|
|
1036
|
+
const idxList = Array.isArray(payload.suggestedIndexes) ? payload.suggestedIndexes : [];
|
|
1037
|
+
const parts = [];
|
|
1038
|
+
if (payload.optimizedQuery) {
|
|
1039
|
+
parts.push("Optimized query available in `optimizedQuery`.");
|
|
1040
|
+
}
|
|
1041
|
+
if (idxList.length > 0) {
|
|
1042
|
+
parts.push(`${idxList.length} index suggestion(s): ${idxList.map((x) => x.indexDdl || x.column || JSON.stringify(x)).slice(0, 3).join("; ")}`);
|
|
1043
|
+
}
|
|
1044
|
+
if (recList.length > 0) {
|
|
1045
|
+
parts.push(`${recList.length} recommendation(s): ${recList.map((r) => r.title || r.summary || JSON.stringify(r)).slice(0, 3).join("; ")}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (payload.estimatedImprovementPercent != null) {
|
|
1048
|
+
parts.push(`Estimated improvement: ${payload.estimatedImprovementPercent.toFixed(0)}%`);
|
|
1049
|
+
}
|
|
1050
|
+
return parts.length > 0 ? parts.join(". ") + "." : "Optimization complete. See structuredContent for details.";
|
|
1051
|
+
}
|
|
1052
|
+
|
|
844
1053
|
function summarizeTableGrowth(payload) {
|
|
845
1054
|
// Backend returns { success, trends: { sizeOverTime[], growthOverTime[],
|
|
846
1055
|
// rowCountOverTime[] }, days }. We don't want to dump the raw arrays into
|
|
@@ -970,6 +1179,21 @@ function buildToolResult(name, payload, extra = {}) {
|
|
|
970
1179
|
case "analyze_slow_queries":
|
|
971
1180
|
summary = summarizeSlowQueries(payload);
|
|
972
1181
|
break;
|
|
1182
|
+
case "list_tracked_queries":
|
|
1183
|
+
summary = summarizeTrackedQueries(payload);
|
|
1184
|
+
break;
|
|
1185
|
+
case "get_slow_query_customers":
|
|
1186
|
+
summary = summarizeSlowQueryCustomers(payload);
|
|
1187
|
+
break;
|
|
1188
|
+
case "get_query_samples":
|
|
1189
|
+
summary = summarizeQuerySamples(payload);
|
|
1190
|
+
break;
|
|
1191
|
+
case "get_slow_query_insights":
|
|
1192
|
+
summary = summarizeSlowQueryInsights(payload);
|
|
1193
|
+
break;
|
|
1194
|
+
case "optimize_slow_query":
|
|
1195
|
+
summary = summarizeSlowQueryOptimization(payload);
|
|
1196
|
+
break;
|
|
973
1197
|
case "get_table_growth":
|
|
974
1198
|
summary = summarizeTableGrowth(payload);
|
|
975
1199
|
break;
|
|
@@ -1183,6 +1407,68 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
1183
1407
|
return buildToolResult(name, payload);
|
|
1184
1408
|
}
|
|
1185
1409
|
|
|
1410
|
+
case "list_tracked_queries": {
|
|
1411
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1412
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1413
|
+
const payload = await callDeepSqlApi(
|
|
1414
|
+
config,
|
|
1415
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/queries`,
|
|
1416
|
+
);
|
|
1417
|
+
return buildToolResult(name, payload);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
case "get_slow_query_customers": {
|
|
1421
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1422
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1423
|
+
const payload = await callDeepSqlApi(
|
|
1424
|
+
config,
|
|
1425
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/customers`,
|
|
1426
|
+
);
|
|
1427
|
+
return buildToolResult(name, payload);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
case "get_query_samples": {
|
|
1431
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1432
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1433
|
+
const fingerprint = String(args.fingerprint || "").trim();
|
|
1434
|
+
if (!fingerprint) return buildToolError("fingerprint is required.");
|
|
1435
|
+
const payload = await callDeepSqlApi(
|
|
1436
|
+
config,
|
|
1437
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/query/${encodeURIComponent(fingerprint)}/samples`,
|
|
1438
|
+
);
|
|
1439
|
+
return buildToolResult(name, payload);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
case "get_slow_query_insights": {
|
|
1443
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1444
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1445
|
+
const kind = String(args.kind || "all").toLowerCase();
|
|
1446
|
+
const window = String(args.window || "LAST_7_DAYS").toUpperCase();
|
|
1447
|
+
const limit = clampInteger(args.limit, 1, 100, 10);
|
|
1448
|
+
const subPath = kind === "all" ? "" : `/${encodeURIComponent(kind)}`;
|
|
1449
|
+
const payload = await callDeepSqlApi(
|
|
1450
|
+
config,
|
|
1451
|
+
`/slow-queries/insights/${encodeURIComponent(connectionId)}${subPath}?window=${encodeURIComponent(window)}&limit=${limit}`,
|
|
1452
|
+
);
|
|
1453
|
+
return buildToolResult(name, payload);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
case "optimize_slow_query": {
|
|
1457
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1458
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1459
|
+
const queryText = String(args.queryText || "").trim();
|
|
1460
|
+
if (!queryText) return buildToolError("queryText is required.");
|
|
1461
|
+
const json = { connectionId, queryText };
|
|
1462
|
+
if (args.avgExecutionTimeMs != null) {
|
|
1463
|
+
json.avgExecutionTimeMs = Number(args.avgExecutionTimeMs);
|
|
1464
|
+
}
|
|
1465
|
+
const payload = await callDeepSqlApi(config, "/slow-queries/optimize", {
|
|
1466
|
+
method: "POST",
|
|
1467
|
+
json,
|
|
1468
|
+
});
|
|
1469
|
+
return buildToolResult(name, payload);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1186
1472
|
case "get_table_growth": {
|
|
1187
1473
|
const connectionId = String(args.connectionId || "").trim();
|
|
1188
1474
|
if (!connectionId) return buildToolError("connectionId is required.");
|
|
@@ -1326,6 +1612,11 @@ module.exports = {
|
|
|
1326
1612
|
summarizeApplyResult,
|
|
1327
1613
|
summarizeGrowthAnomalies,
|
|
1328
1614
|
summarizeIndexRecommendations,
|
|
1615
|
+
summarizeQuerySamples,
|
|
1616
|
+
summarizeSlowQueryCustomers,
|
|
1617
|
+
summarizeSlowQueryInsights,
|
|
1618
|
+
summarizeSlowQueryOptimization,
|
|
1329
1619
|
summarizeTableGrowth,
|
|
1620
|
+
summarizeTrackedQueries,
|
|
1330
1621
|
validateReadOnlySql,
|
|
1331
1622
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepsql/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deepsql": "bin/deepsql.js",
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
"license": "UNLICENSED",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@inquirer/prompts": "^8.4.2"
|
|
31
|
+
},
|
|
32
|
+
"overrides": {
|
|
33
|
+
"mute-stream": "^3.0.0"
|
|
31
34
|
}
|
|
32
35
|
}
|
package/skills/SKILL_BODY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
You have two DeepSQL surfaces available:
|
|
4
4
|
|
|
5
|
-
1. **MCP tools** — JSON-RPC tools loaded into your session (
|
|
5
|
+
1. **MCP tools** — JSON-RPC tools loaded into your session (21 of them).
|
|
6
6
|
Use these for in-session retrieval and SQL execution.
|
|
7
7
|
|
|
8
8
|
2. **`deepsql` CLI** — a shell binary on the user's PATH (~19 commands).
|
|
@@ -87,6 +87,13 @@ usually doesn't know about either; that's exactly why DeepSQL exists.
|
|
|
87
87
|
| `get_relationships(connectionId)` | Foreign keys (declared + inferred-with-confidence). |
|
|
88
88
|
| `get_anti_patterns(connectionId, kind="table"\|"query")` | Patterns to avoid in this DB. |
|
|
89
89
|
| `analyze_slow_queries(connectionId, thresholdMs?, limit?)` | Snapshot of slow queries from live stats. |
|
|
90
|
+
| `get_slow_query_timeline(connectionId, fingerprint)` | Day-by-day timeline for one fingerprint: call count, mean/max time, regression factor per day. Answers "is this query getting slower". |
|
|
91
|
+
| `get_query_regressions(connectionId, minFactor?)` | Slow queries that got slower on the latest daily analysis run, ranked by slowdown factor. |
|
|
92
|
+
| `list_tracked_queries(connectionId)` | All fingerprints in the 30-day analytics store. Browse first, then drill into one with `get_slow_query_timeline` / `get_query_samples`. |
|
|
93
|
+
| `get_slow_query_customers(connectionId)` | Tenants ranked by total slow-query time — answers "which customer is driving the load?" |
|
|
94
|
+
| `get_query_samples(connectionId, fingerprint)` | Literal SQL samples with bind values for a fingerprint, slowest-first. Use to reproduce an execution or run a real EXPLAIN. |
|
|
95
|
+
| `get_slow_query_insights(connectionId, kind?, window?, limit?)` | Pre-computed AI insights — `hotspots`, `remediation`, `tail-risk`, `plan-drift`, `skew`, or `all` (default). |
|
|
96
|
+
| `optimize_slow_query(connectionId, queryText, avgExecutionTimeMs?)` | AI optimization recommendations for a specific SQL — index suggestions, query rewrites, estimated impact. |
|
|
90
97
|
| `get_table_growth(connectionId, tableName?, days?)` | Persistent stats history: per-table size/row time series + headline rollups. Use to answer "which tables are growing fastest?" or "how much has X grown in the last month?" without scanning the live DB. |
|
|
91
98
|
| `get_growth_anomalies(connectionId, tableName?, unacknowledgedOnly?, days?)` | DeepSQL-flagged sudden growth spikes with severity (CRITICAL/WARNING/INFO), anomaly type, before/after sizes, confidence score. Check this BEFORE walking the user through a slow-query plan — a recent growth anomaly is often the real root cause. |
|
|
92
99
|
| `execute_sql(connectionId, query, ...)` | Run any SQL — SELECT for everyone, DML/DDL for admins (two-step confirm). |
|
|
@@ -132,7 +139,7 @@ deepsql query "SELECT 1" --connection prod-pg --caller-agent claude-code --json
|
|
|
132
139
|
| `deepsql digest [N]\|list\|show <id>` | **CLI-only**: daily digest of slow queries + AI commentary | none |
|
|
133
140
|
| `deepsql growth trends\|history\|anomalies\|ack\|capture\|config` | Table growth analytics (size/row trends, detected anomalies, alert thresholds) | partial: `get_table_growth`, `get_growth_anomalies` |
|
|
134
141
|
| `deepsql indexes list\|missing\|health\|unused\|duplicates\|usage <table>` | **CLI-only**: index recommendations and usage stats | none |
|
|
135
|
-
| `deepsql slow-queries latest\|history\|analyze\|optimize\|delete` |
|
|
142
|
+
| `deepsql slow-queries latest\|history\|analyze\|optimize\|delete\|trends\|regressions\|timeline\|customers\|samples\|insights\|trigger` | Full slow-query toolkit. `optimize` streams AI optimization steps live (SSE); `customers` / `samples` / `insights` mirror the analytics MCP tools; `trigger` runs an immediate daily analysis. | mostly mirrored in MCP — use CLI for streaming optimize and one-shot triggers |
|
|
136
143
|
| `deepsql users list\|get\|add\|set-role\|lock\|unlock\|disable\|resend-invite\|reset-password\|delete` | **Admin-only, CLI-only**: workspace user management | none |
|
|
137
144
|
| `deepsql access list\|grant\|revoke\|policy <user> <conn>` | **Admin-only, CLI-only**: per-connection access grants + chat policy editing in $EDITOR | none |
|
|
138
145
|
| `deepsql permissions list\|override\|reset` | **Admin-only, CLI-only**: role-based permission overrides | none |
|
|
@@ -159,7 +166,11 @@ shell-out (with `--caller-agent`) or tell the user the exact command:
|
|
|
159
166
|
| "Duplicate indexes?" | `deepsql indexes duplicates --connection <c>` |
|
|
160
167
|
| "Index health on this connection" | `deepsql indexes health --connection <c>` |
|
|
161
168
|
| "Indexes on `<table>` and how often they're used" | `deepsql indexes usage <table> --connection <c>` |
|
|
162
|
-
| "Optimize this slow query with AI" / "Stream me a fix" | `deepsql slow-queries optimize --connection <c> --query-id <id>` |
|
|
169
|
+
| "Optimize this slow query with AI" / "Stream me a fix" | `deepsql slow-queries optimize --connection <c> --query-id <id>` (streamed) — or MCP `optimize_slow_query(connectionId, queryText)` for a one-shot JSON response |
|
|
170
|
+
| "Which customer is driving the most slow queries?" | MCP: `get_slow_query_customers(connectionId)`. CLI: `deepsql slow-queries customers --connection <c>` |
|
|
171
|
+
| "Show me the actual SQL run for this fingerprint" | MCP: `get_query_samples(connectionId, fingerprint)`. CLI: `deepsql slow-queries samples <fingerprint> --connection <c>` |
|
|
172
|
+
| "What slow-query insights / hotspots / tail-risk has DeepSQL found?" | MCP: `get_slow_query_insights(connectionId, kind?, window?)`. CLI: `deepsql slow-queries insights --connection <c> [--kind hotspots]` |
|
|
173
|
+
| "Trigger slow-query analysis now" / "Re-run the daily analysis" | CLI: `deepsql slow-queries trigger --connection <c>` |
|
|
163
174
|
| "Add a new database connection" | `deepsql connections add` (interactive) or `--from-file <path>` |
|
|
164
175
|
| "Test a connection without saving" | `deepsql connections test --from-file <path>` |
|
|
165
176
|
| "Trigger brain re-initialization for this connection" | `deepsql connections init <name> --wait` |
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
* deepsql slow-queries optimize --connection <name> --query-id <id>
|
|
12
12
|
* (streams SSE; use --query-text to skip history lookup)
|
|
13
13
|
* deepsql slow-queries delete --history-id <id> [--yes]
|
|
14
|
+
* deepsql slow-queries trends --connection <name> [--json]
|
|
15
|
+
* deepsql slow-queries regressions --connection <name> [--min-factor <n>] [--json]
|
|
16
|
+
* deepsql slow-queries timeline <fingerprint> --connection <name> [--json]
|
|
17
|
+
* deepsql slow-queries customers --connection <name> [--json]
|
|
18
|
+
* deepsql slow-queries samples <fingerprint> --connection <name> [--limit <n>] [--json]
|
|
19
|
+
* deepsql slow-queries insights --connection <name>
|
|
20
|
+
* [--kind hotspots|remediation|tail-risk|plan-drift|skew]
|
|
21
|
+
* [--window LAST_7_DAYS|LAST_24_HOURS|LAST_30_DAYS] [--limit <n>] [--json]
|
|
22
|
+
* deepsql slow-queries trigger --connection <name> [--json]
|
|
14
23
|
*
|
|
15
24
|
* The optimize subcommand follows the SSE protocol from
|
|
16
25
|
* /slow-queries/optimize/stream — `step` events go to stderr, the final
|
|
@@ -32,6 +41,10 @@ const SUBCOMMANDS = {
|
|
|
32
41
|
trends: cmdTrends,
|
|
33
42
|
regressions: cmdRegressions,
|
|
34
43
|
timeline: cmdTimeline,
|
|
44
|
+
customers: cmdCustomers,
|
|
45
|
+
samples: cmdSamples,
|
|
46
|
+
insights: cmdInsights,
|
|
47
|
+
trigger: cmdTrigger,
|
|
35
48
|
};
|
|
36
49
|
|
|
37
50
|
async function run(opts, io = {}) {
|
|
@@ -39,7 +52,8 @@ async function run(opts, io = {}) {
|
|
|
39
52
|
if (!sub) {
|
|
40
53
|
throw new Error(
|
|
41
54
|
"Usage: deepsql slow-queries "
|
|
42
|
-
+ "<latest|history|analyze|optimize|delete|trends|regressions|timeline
|
|
55
|
+
+ "<latest|history|analyze|optimize|delete|trends|regressions|timeline"
|
|
56
|
+
+ "|customers|samples|insights|trigger> ...");
|
|
43
57
|
}
|
|
44
58
|
const handler = SUBCOMMANDS[sub];
|
|
45
59
|
if (!handler) throw new Error(`Unknown slow-queries subcommand: ${sub}.`);
|
|
@@ -374,6 +388,136 @@ async function cmdTimeline(opts, { stdout = process.stdout } = {}) {
|
|
|
374
388
|
})));
|
|
375
389
|
}
|
|
376
390
|
|
|
391
|
+
// ─── customers ─────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
async function cmdCustomers(opts, { stdout = process.stdout } = {}) {
|
|
394
|
+
const session = resolveSession(opts);
|
|
395
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
396
|
+
const rows = await request(
|
|
397
|
+
session.baseUrl,
|
|
398
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/customers`,
|
|
399
|
+
{ token: session.token },
|
|
400
|
+
);
|
|
401
|
+
const items = Array.isArray(rows) ? rows : [];
|
|
402
|
+
if (opts.json) {
|
|
403
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (items.length === 0) {
|
|
407
|
+
stdout.write("No customer attribution data yet. Configure a tenant column in analytics settings.\n");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
printTable(stdout, [
|
|
411
|
+
{ key: "rank", label: "#" },
|
|
412
|
+
{ key: "customer", label: "CUSTOMER" },
|
|
413
|
+
{ key: "name", label: "NAME" },
|
|
414
|
+
{ key: "queries", label: "QUERIES" },
|
|
415
|
+
{ key: "totalSec", label: "TOTAL (s)" },
|
|
416
|
+
], items.map((c, i) => ({
|
|
417
|
+
rank: String(i + 1),
|
|
418
|
+
customer: trim(c.customerId || "", 22),
|
|
419
|
+
name: trim(c.customerName || "—", 28),
|
|
420
|
+
queries: String(c.queryCount ?? "?"),
|
|
421
|
+
totalSec: c.totalExecMs != null ? (c.totalExecMs / 1000).toFixed(1) : "?",
|
|
422
|
+
})));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── samples ───────────────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
async function cmdSamples(opts, { stdout = process.stdout } = {}) {
|
|
428
|
+
const session = resolveSession(opts);
|
|
429
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
430
|
+
const fingerprint = opts.positional[0];
|
|
431
|
+
if (!fingerprint) {
|
|
432
|
+
throw new Error("Usage: deepsql slow-queries samples <fingerprint> --connection <name>");
|
|
433
|
+
}
|
|
434
|
+
const limit = parseNum(opts.limit) || 10;
|
|
435
|
+
const rows = await request(
|
|
436
|
+
session.baseUrl,
|
|
437
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}`
|
|
438
|
+
+ `/query/${encodeURIComponent(fingerprint)}/samples`,
|
|
439
|
+
{ token: session.token },
|
|
440
|
+
);
|
|
441
|
+
const items = Array.isArray(rows) ? rows.slice(0, limit) : [];
|
|
442
|
+
if (opts.json) {
|
|
443
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (items.length === 0) {
|
|
447
|
+
stdout.write("No samples found for that fingerprint.\n");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
printTable(stdout, [
|
|
451
|
+
{ key: "execMs", label: "EXEC MS" },
|
|
452
|
+
{ key: "examined", label: "ROWS EXAM" },
|
|
453
|
+
{ key: "sent", label: "ROWS SENT" },
|
|
454
|
+
{ key: "source", label: "SOURCE" },
|
|
455
|
+
{ key: "captured", label: "CAPTURED AT" },
|
|
456
|
+
{ key: "sql", label: "SQL" },
|
|
457
|
+
], items.map((s) => ({
|
|
458
|
+
execMs: s.execMs != null ? String(Math.round(s.execMs)) : "?",
|
|
459
|
+
examined: String(s.rowsExamined ?? "?"),
|
|
460
|
+
sent: String(s.rowsSent ?? "?"),
|
|
461
|
+
source: trim(s.source || "?", 14),
|
|
462
|
+
captured: formatTime(s.capturedAt),
|
|
463
|
+
sql: trim(s.rawSql || "", 60),
|
|
464
|
+
})));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ─── insights ──────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
async function cmdInsights(opts, { stdout = process.stdout } = {}) {
|
|
470
|
+
const session = resolveSession(opts);
|
|
471
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
472
|
+
const kind = (opts.kind || "").toLowerCase();
|
|
473
|
+
const window = (opts.window || "LAST_7_DAYS").toUpperCase();
|
|
474
|
+
const limit = parseNum(opts.limit) || 10;
|
|
475
|
+
const subPath = kind && kind !== "all" ? `/${encodeURIComponent(kind)}` : "";
|
|
476
|
+
const rows = await request(
|
|
477
|
+
session.baseUrl,
|
|
478
|
+
`/slow-queries/insights/${encodeURIComponent(connectionId)}${subPath}`
|
|
479
|
+
+ `?window=${encodeURIComponent(window)}&limit=${limit}`,
|
|
480
|
+
{ token: session.token },
|
|
481
|
+
);
|
|
482
|
+
const items = Array.isArray(rows) ? rows : [];
|
|
483
|
+
if (opts.json) {
|
|
484
|
+
stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (items.length === 0) {
|
|
488
|
+
stdout.write(`No insights for window=${window}${kind ? ` kind=${kind}` : ""}.\n`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
for (let i = 0; i < items.length; i++) {
|
|
492
|
+
const ins = items[i];
|
|
493
|
+
const sev = ins.severity ? `[${ins.severity}] ` : "";
|
|
494
|
+
const title = ins.title || ins.insightType || "insight";
|
|
495
|
+
const desc = String(ins.description || ins.message || "").replace(/\s+/g, " ").trim();
|
|
496
|
+
stdout.write(`${i + 1}. ${sev}${title}\n`);
|
|
497
|
+
if (desc) stdout.write(` ${desc}\n`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── trigger ───────────────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
async function cmdTrigger(opts, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
504
|
+
const session = resolveSession(opts);
|
|
505
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
506
|
+
stderr.write(`Triggering slow-query analysis for ${opts.connection}…\n`);
|
|
507
|
+
const result = await request(
|
|
508
|
+
session.baseUrl,
|
|
509
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/analyze-now`,
|
|
510
|
+
{ method: "POST", token: session.token, timeoutMs: 300000 },
|
|
511
|
+
);
|
|
512
|
+
if (opts.json) {
|
|
513
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const runId = result?.analysisRunId || result?.runId || "?";
|
|
517
|
+
const health = result?.overallHealth || result?.health || "";
|
|
518
|
+
stdout.write(`Analysis started. Run ID: ${runId}${health ? ` Health: ${health}` : ""}\n`);
|
|
519
|
+
}
|
|
520
|
+
|
|
377
521
|
function printTrendRows(stdout, items) {
|
|
378
522
|
printTable(stdout, [
|
|
379
523
|
{ key: "fingerprint", label: "QUERY ID" },
|