@deepsql/mcp 0.16.0 → 0.18.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/CLAUDE.md +13 -2
- package/README.md +14 -2
- package/deepsql-phase1-lib.js +542 -0
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +21 -3
- package/src/auth/store.js +3 -3
- package/src/cli.js +30 -0
- package/src/commands/growth.js +458 -0
- package/src/commands/growth.test.js +439 -0
- package/src/commands/mcp.js +9 -9
- package/src/commands/slow-queries.js +260 -1
- package/src/user-home.js +29 -0
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
|
|
|
@@ -47,6 +47,13 @@ which takes a server-resolved `recommendationId`.
|
|
|
47
47
|
| `get_relationships` | Inferred + validated foreign keys with confidence scores. Many real DBs lack declared FKs; this fills the gap. |
|
|
48
48
|
| `get_anti_patterns` | Schema-level (`kind=table`) or query-level (`kind=query`) anti-patterns. |
|
|
49
49
|
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples. Read-only; doesn't trigger new work. |
|
|
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
|
+
| `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. |
|
|
50
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. |
|
|
51
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. |
|
|
52
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. |
|
|
@@ -333,7 +340,11 @@ them at the terminal command rather than trying to fake it through
|
|
|
333
340
|
| Catalog: per-table index usage stats | `deepsql indexes usage <table>` |
|
|
334
341
|
| Catalog: index health report | `deepsql indexes health` |
|
|
335
342
|
| Daily digest (anomalies + AI commentary) | `deepsql digest`, `deepsql digest 7` |
|
|
336
|
-
| 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>` |
|
|
337
348
|
|
|
338
349
|
These are reachable from any terminal where `deepsql` is installed and
|
|
339
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
|
@@ -217,6 +217,221 @@ const TOOL_DEFINITIONS = [
|
|
|
217
217
|
additionalProperties: false,
|
|
218
218
|
},
|
|
219
219
|
},
|
|
220
|
+
{
|
|
221
|
+
name: "get_slow_query_timeline",
|
|
222
|
+
description:
|
|
223
|
+
"Get the day-by-day timeline for one slow query from DeepSQL's 30-day analytics store. "
|
|
224
|
+
+ "The query is identified by its stable fingerprint (the MD5 of the normalized query — "
|
|
225
|
+
+ "the `queryId` field returned by analyze_slow_queries). Returns one point per day with "
|
|
226
|
+
+ "call count, mean/max execution time, and the regression factor versus the previous day. "
|
|
227
|
+
+ "Use this to answer 'is this query getting slower over time'.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
232
|
+
fingerprint: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: "Stable query fingerprint (MD5 of the normalized query).",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
required: ["connectionId", "fingerprint"],
|
|
238
|
+
additionalProperties: false,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "get_query_regressions",
|
|
243
|
+
description:
|
|
244
|
+
"List slow queries that regressed (got slower) on the most recent daily analysis run. "
|
|
245
|
+
+ "Each result carries the fingerprint, normalized SQL, current mean execution time, and "
|
|
246
|
+
+ "the regression factor (this period's mean / the previous period's mean). Read-only.",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
251
|
+
minFactor: {
|
|
252
|
+
type: "number",
|
|
253
|
+
minimum: 1,
|
|
254
|
+
description: "Minimum slowdown multiple to report. Defaults to 1.5 (≥50% slower).",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ["connectionId"],
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
},
|
|
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
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "get_table_growth",
|
|
377
|
+
description:
|
|
378
|
+
"Get table size / row-count growth trends for a connection from DeepSQL's persistent stats history. "
|
|
379
|
+
+ "Returns three parallel time series (sizeOverTime, growthOverTime, rowCountOverTime) plus per-table "
|
|
380
|
+
+ "headline rollups suitable for answering questions like \"which tables are growing fastest?\" or "
|
|
381
|
+
+ "\"how much has `orders` grown in the last month?\". Backed by snapshots stored in DeepSQL's "
|
|
382
|
+
+ "`table_stats_history`, not live `pg_total_relation_size()` probes — so it can show growth velocity "
|
|
383
|
+
+ "and bloat over time without re-scanning the customer's database.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
388
|
+
tableName: {
|
|
389
|
+
type: "string",
|
|
390
|
+
description: "Optional table name to scope the trends to a single table. Omit for all tables.",
|
|
391
|
+
},
|
|
392
|
+
days: {
|
|
393
|
+
type: "integer",
|
|
394
|
+
minimum: 1,
|
|
395
|
+
maximum: 365,
|
|
396
|
+
description: "Lookback window in days. Defaults to 30.",
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
required: ["connectionId"],
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "get_growth_anomalies",
|
|
405
|
+
description:
|
|
406
|
+
"Get growth anomalies DeepSQL flagged on a connection — sudden size or row spikes that exceeded the "
|
|
407
|
+
+ "configured thresholds (percentage growth, absolute byte growth, statistical z-score). Each anomaly "
|
|
408
|
+
+ "carries severity (CRITICAL / WARNING / INFO), the before/after sizes, an anomaly type "
|
|
409
|
+
+ "(PERCENTAGE_GROWTH, ABSOLUTE_GROWTH, STATISTICAL_ANOMALY, ROW_SPIKE, NEW_TABLE), a human-readable "
|
|
410
|
+
+ "description, and a confidence score. Use this BEFORE walking the user through a plan to optimize a "
|
|
411
|
+
+ "table — the anomaly may be the root cause they should investigate first.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
416
|
+
tableName: {
|
|
417
|
+
type: "string",
|
|
418
|
+
description: "Optional table name to scope to one table. Omit for the whole connection.",
|
|
419
|
+
},
|
|
420
|
+
unacknowledgedOnly: {
|
|
421
|
+
type: "boolean",
|
|
422
|
+
description: "When true, only return anomalies the operator hasn't acked yet. Defaults to false.",
|
|
423
|
+
},
|
|
424
|
+
days: {
|
|
425
|
+
type: "integer",
|
|
426
|
+
minimum: 1,
|
|
427
|
+
maximum: 365,
|
|
428
|
+
description: "Lookback window in days. Defaults to 30.",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
required: ["connectionId"],
|
|
432
|
+
additionalProperties: false,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
220
435
|
{
|
|
221
436
|
name: "execute_sql",
|
|
222
437
|
description:
|
|
@@ -740,6 +955,185 @@ function summarizeSlowQueries(payload) {
|
|
|
740
955
|
+ parts.join("");
|
|
741
956
|
}
|
|
742
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
|
+
|
|
1053
|
+
function summarizeTableGrowth(payload) {
|
|
1054
|
+
// Backend returns { success, trends: { sizeOverTime[], growthOverTime[],
|
|
1055
|
+
// rowCountOverTime[] }, days }. We don't want to dump the raw arrays into
|
|
1056
|
+
// the agent's context — collapse to a per-table headline and a top-3
|
|
1057
|
+
// "growing fastest" list.
|
|
1058
|
+
const trends = payload?.trends || {};
|
|
1059
|
+
const sizeOverTime = Array.isArray(trends.sizeOverTime) ? trends.sizeOverTime : [];
|
|
1060
|
+
if (sizeOverTime.length === 0) {
|
|
1061
|
+
return "No table-growth history for this connection in the requested window. "
|
|
1062
|
+
+ "The customer may not have stats-snapshot collection enabled yet.";
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Roll up per-table: first vs last snapshot.
|
|
1066
|
+
const byTable = new Map();
|
|
1067
|
+
for (const point of sizeOverTime) {
|
|
1068
|
+
const t = point.table || "(unknown)";
|
|
1069
|
+
if (!byTable.has(t)) byTable.set(t, []);
|
|
1070
|
+
byTable.get(t).push(point);
|
|
1071
|
+
}
|
|
1072
|
+
const rows = [];
|
|
1073
|
+
for (const [table, points] of byTable.entries()) {
|
|
1074
|
+
points.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
1075
|
+
const first = points[0];
|
|
1076
|
+
const last = points[points.length - 1];
|
|
1077
|
+
const firstBytes = first?.sizeBytes ?? 0;
|
|
1078
|
+
const lastBytes = last?.sizeBytes ?? 0;
|
|
1079
|
+
const deltaBytes = lastBytes - firstBytes;
|
|
1080
|
+
rows.push({ table, firstBytes, lastBytes, deltaBytes });
|
|
1081
|
+
}
|
|
1082
|
+
rows.sort((a, b) => Math.abs(b.deltaBytes) - Math.abs(a.deltaBytes));
|
|
1083
|
+
|
|
1084
|
+
const days = payload?.days != null ? `${payload.days}d` : "window";
|
|
1085
|
+
const top = rows.slice(0, 3).map((r) => {
|
|
1086
|
+
const arrow = r.deltaBytes >= 0 ? "↑" : "↓";
|
|
1087
|
+
const pct = r.firstBytes > 0
|
|
1088
|
+
? ` (${r.deltaBytes >= 0 ? "+" : ""}${((r.deltaBytes / r.firstBytes) * 100).toFixed(1)}%)`
|
|
1089
|
+
: "";
|
|
1090
|
+
return `${r.table} ${arrow} ${formatBytesHumanLib(Math.abs(r.deltaBytes))}${pct}`;
|
|
1091
|
+
});
|
|
1092
|
+
return `${rows.length} table(s) with growth data over ${days}. `
|
|
1093
|
+
+ `Most-changed: ${top.join("; ")}.`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function summarizeGrowthAnomalies(payload) {
|
|
1097
|
+
// Backend returns { success, anomalies[], statistics: { total, warning,
|
|
1098
|
+
// critical, info, acknowledged, unacknowledged } }. Agents should know
|
|
1099
|
+
// BEFORE drilling into a slow query whether a recent growth anomaly is
|
|
1100
|
+
// the real root cause.
|
|
1101
|
+
const list = Array.isArray(payload?.anomalies) ? payload.anomalies : [];
|
|
1102
|
+
if (list.length === 0) {
|
|
1103
|
+
return "No growth anomalies detected in the requested window.";
|
|
1104
|
+
}
|
|
1105
|
+
const stats = payload?.statistics || {};
|
|
1106
|
+
const total = stats.total ?? list.length;
|
|
1107
|
+
const crit = stats.critical ?? 0;
|
|
1108
|
+
const warn = stats.warning ?? 0;
|
|
1109
|
+
const unack = stats.unacknowledged ?? 0;
|
|
1110
|
+
|
|
1111
|
+
// Surface the worst recent one so the agent has something concrete to
|
|
1112
|
+
// reference without having to walk the structured payload.
|
|
1113
|
+
const worst = list.find((a) => a && a.severity === "CRITICAL")
|
|
1114
|
+
|| list.find((a) => a && a.severity === "WARNING")
|
|
1115
|
+
|| list[0];
|
|
1116
|
+
const worstLine = worst
|
|
1117
|
+
? ` Top: [${worst.severity || "INFO"}] ${worst.tableName || "?"} — `
|
|
1118
|
+
+ `${worst.anomalyType || "growth"}`
|
|
1119
|
+
+ (worst.sizeGrowthPercent != null
|
|
1120
|
+
? ` ${worst.sizeGrowthPercent > 0 ? "+" : ""}${worst.sizeGrowthPercent.toFixed(1)}%`
|
|
1121
|
+
: "")
|
|
1122
|
+
: "";
|
|
1123
|
+
return `${total} growth anomal${total === 1 ? "y" : "ies"} `
|
|
1124
|
+
+ `(${crit} critical, ${warn} warning, ${unack} unacknowledged).${worstLine}`;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function formatBytesHumanLib(bytes) {
|
|
1128
|
+
if (bytes == null) return "?";
|
|
1129
|
+
const abs = Math.abs(bytes);
|
|
1130
|
+
if (abs < 1024) return `${bytes} B`;
|
|
1131
|
+
if (abs < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1132
|
+
if (abs < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1133
|
+
if (abs < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
1134
|
+
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
743
1137
|
function summarizeQueryResult(payload) {
|
|
744
1138
|
const result = payload?.result || payload?.data || payload;
|
|
745
1139
|
const rowCount = result?.rowCount ?? 0;
|
|
@@ -785,6 +1179,27 @@ function buildToolResult(name, payload, extra = {}) {
|
|
|
785
1179
|
case "analyze_slow_queries":
|
|
786
1180
|
summary = summarizeSlowQueries(payload);
|
|
787
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;
|
|
1197
|
+
case "get_table_growth":
|
|
1198
|
+
summary = summarizeTableGrowth(payload);
|
|
1199
|
+
break;
|
|
1200
|
+
case "get_growth_anomalies":
|
|
1201
|
+
summary = summarizeGrowthAnomalies(payload);
|
|
1202
|
+
break;
|
|
788
1203
|
case "get_index_recommendations":
|
|
789
1204
|
summary = summarizeIndexRecommendations(payload);
|
|
790
1205
|
break;
|
|
@@ -969,6 +1384,126 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
969
1384
|
return buildToolResult(name, payload);
|
|
970
1385
|
}
|
|
971
1386
|
|
|
1387
|
+
case "get_slow_query_timeline": {
|
|
1388
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1389
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1390
|
+
const fingerprint = String(args.fingerprint || "").trim();
|
|
1391
|
+
if (!fingerprint) return buildToolError("fingerprint is required.");
|
|
1392
|
+
const payload = await callDeepSqlApi(
|
|
1393
|
+
config,
|
|
1394
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/timeline/${encodeURIComponent(fingerprint)}`,
|
|
1395
|
+
);
|
|
1396
|
+
return buildToolResult(name, payload);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
case "get_query_regressions": {
|
|
1400
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1401
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1402
|
+
const minFactor = args.minFactor != null ? Number(args.minFactor) : 1.5;
|
|
1403
|
+
const payload = await callDeepSqlApi(
|
|
1404
|
+
config,
|
|
1405
|
+
`/slow-query-analytics/${encodeURIComponent(connectionId)}/regressions?minFactor=${minFactor}`,
|
|
1406
|
+
);
|
|
1407
|
+
return buildToolResult(name, payload);
|
|
1408
|
+
}
|
|
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
|
+
|
|
1472
|
+
case "get_table_growth": {
|
|
1473
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1474
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1475
|
+
const params = [];
|
|
1476
|
+
const days = clampInteger(args.days, 1, 365, 30);
|
|
1477
|
+
params.push(`days=${days}`);
|
|
1478
|
+
if (args.tableName) {
|
|
1479
|
+
params.push(`tableName=${encodeURIComponent(String(args.tableName))}`);
|
|
1480
|
+
}
|
|
1481
|
+
const payload = await callDeepSqlApi(
|
|
1482
|
+
config,
|
|
1483
|
+
`/growth-monitoring/trends/${encodeURIComponent(connectionId)}?${params.join("&")}`,
|
|
1484
|
+
);
|
|
1485
|
+
return buildToolResult(name, payload);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
case "get_growth_anomalies": {
|
|
1489
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
1490
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
1491
|
+
const params = [];
|
|
1492
|
+
const days = clampInteger(args.days, 1, 365, 30);
|
|
1493
|
+
params.push(`days=${days}`);
|
|
1494
|
+
if (args.tableName) {
|
|
1495
|
+
params.push(`tableName=${encodeURIComponent(String(args.tableName))}`);
|
|
1496
|
+
}
|
|
1497
|
+
if (args.unacknowledgedOnly === true) {
|
|
1498
|
+
params.push("unacknowledgedOnly=true");
|
|
1499
|
+
}
|
|
1500
|
+
const payload = await callDeepSqlApi(
|
|
1501
|
+
config,
|
|
1502
|
+
`/growth-monitoring/anomalies/${encodeURIComponent(connectionId)}?${params.join("&")}`,
|
|
1503
|
+
);
|
|
1504
|
+
return buildToolResult(name, payload);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
972
1507
|
case "execute_sql": {
|
|
973
1508
|
const connectionId = String(args.connectionId || "").trim();
|
|
974
1509
|
const query = String(args.query || "").trim();
|
|
@@ -1075,6 +1610,13 @@ module.exports = {
|
|
|
1075
1610
|
stripSqlComments,
|
|
1076
1611
|
stripSqlStringLiterals,
|
|
1077
1612
|
summarizeApplyResult,
|
|
1613
|
+
summarizeGrowthAnomalies,
|
|
1078
1614
|
summarizeIndexRecommendations,
|
|
1615
|
+
summarizeQuerySamples,
|
|
1616
|
+
summarizeSlowQueryCustomers,
|
|
1617
|
+
summarizeSlowQueryInsights,
|
|
1618
|
+
summarizeSlowQueryOptimization,
|
|
1619
|
+
summarizeTableGrowth,
|
|
1620
|
+
summarizeTrackedQueries,
|
|
1079
1621
|
validateReadOnlySql,
|
|
1080
1622
|
};
|