@deepsql/mcp 0.13.4 → 0.16.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 +33 -10
- package/deepsql-phase1-lib.js +178 -0
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +169 -33
- package/src/cli.js +36 -13
- package/src/commands/indexes.js +379 -20
- package/src/commands/indexes.test.js +231 -0
- package/src/commands/login.js +46 -7
- package/src/commands/login.test.js +111 -0
package/CLAUDE.md
CHANGED
|
@@ -33,8 +33,9 @@ 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
|
|
37
|
-
returned by `list_connections`)
|
|
36
|
+
The MCP server exposes 12 tools. They all take a `connectionId` (UUID
|
|
37
|
+
returned by `list_connections`) except `apply_index_recommendation`,
|
|
38
|
+
which takes a server-resolved `recommendationId`.
|
|
38
39
|
|
|
39
40
|
| Tool | Purpose |
|
|
40
41
|
|---|---|
|
|
@@ -46,6 +47,8 @@ returned by `list_connections`).
|
|
|
46
47
|
| `get_relationships` | Inferred + validated foreign keys with confidence scores. Many real DBs lack declared FKs; this fills the gap. |
|
|
47
48
|
| `get_anti_patterns` | Schema-level (`kind=table`) or query-level (`kind=query`) anti-patterns. |
|
|
48
49
|
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples. Read-only; doesn't trigger new work. |
|
|
50
|
+
| `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
|
+
| **`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. |
|
|
49
52
|
| **`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. |
|
|
50
53
|
| **`analyze_query_plan`** | **AI-enriched plan analysis** for a query. Returns the parsed plan tree, performance issues, index recommendations, and a written summary that takes the connection's schema and business rules into account. Pass `useAnalyze: true` to run `EXPLAIN ANALYZE` (actually executes the query). |
|
|
51
54
|
|
|
@@ -206,13 +209,27 @@ gates that protect `execute_sql` kick in. You'll get back
|
|
|
206
209
|
get `requiresConfirmation` — surface the warnings to the user verbatim, wait
|
|
207
210
|
for them to say yes, then re-call with `confirmMutation: true`.
|
|
208
211
|
|
|
209
|
-
**"What indexes should we add?"** →
|
|
210
|
-
|
|
211
|
-
terminal
|
|
212
|
+
**"What indexes should we add?"** → `get_index_recommendations`. Returns
|
|
213
|
+
the workload-weighted top-N (default 5) with net benefit, contributing
|
|
214
|
+
queries, and HypoPG cost-delta when available. The terminal equivalent
|
|
215
|
+
is `deepsql indexes top` (same data, same ranking); `deepsql indexes
|
|
216
|
+
missing` / `health` / `unused` / `duplicates` cover the catalog-level
|
|
217
|
+
diagnostics that complement the workload-weighted advisor.
|
|
218
|
+
|
|
219
|
+
**"How much faster will this index actually make things?"** →
|
|
220
|
+
`apply_index_recommendation` with `mode: "DRY_RUN"` (default). On
|
|
221
|
+
Postgres it uses HypoPG to install a virtual index, EXPLAINs each
|
|
222
|
+
contributing query, and reports the planner cost delta — no writes
|
|
223
|
+
hit the database. If the user wants real timings, pass
|
|
224
|
+
`mode: "APPLY_AND_MEASURE"` plus `confirm: true` to actually create
|
|
225
|
+
the index (CONCURRENTLY) and run `EXPLAIN ANALYZE` before/after.
|
|
212
226
|
|
|
213
227
|
**"What changed recently / what should I worry about?"** → Tell the user to
|
|
214
228
|
run `deepsql digest` (today) or `deepsql digest 7` (last seven). The digest
|
|
215
|
-
isn't MCP-exposed.
|
|
229
|
+
isn't MCP-exposed. The digest now includes a workload-weighted **Index Wins**
|
|
230
|
+
section that surfaces the same top-N recommendations `get_index_recommendations`
|
|
231
|
+
returns, with the `deepsql indexes apply <id> --mode dry-run` CTA — so a user
|
|
232
|
+
who skims the digest in Slack can flow directly into the apply path.
|
|
216
233
|
|
|
217
234
|
**"Are there foreign keys between X and Y?"** → `get_relationships`. Many
|
|
218
235
|
real-world DBs lack declared FKs but DeepSQL's brain infers them; check the
|
|
@@ -305,10 +322,16 @@ them at the terminal command rather than trying to fake it through
|
|
|
305
322
|
|
|
306
323
|
| Capability | CLI command |
|
|
307
324
|
|---|---|
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
|
|
|
311
|
-
|
|
|
325
|
+
| Workload-weighted advisor (terminal mirror of `get_index_recommendations`) | `deepsql indexes top [--limit N]` |
|
|
326
|
+
| Apply / dry-run an advisor recommendation (terminal mirror of `apply_index_recommendation`) | `deepsql indexes apply <id> [--mode dry-run\|apply\|apply-and-measure] [--confirm]` |
|
|
327
|
+
| Force a fresh accumulation cycle | `deepsql indexes refresh` |
|
|
328
|
+
| Full recommendation detail incl. contributing queries | `deepsql indexes show <id>` |
|
|
329
|
+
| Dismiss a recommendation | `deepsql indexes dismiss <id>` |
|
|
330
|
+
| Browse all recommendations (any status) | `deepsql indexes list [--all\|--status …]` |
|
|
331
|
+
| Catalog: missing-index suggestions | `deepsql indexes missing` |
|
|
332
|
+
| Catalog: unused / duplicate index detection | `deepsql indexes unused`, `deepsql indexes duplicates` |
|
|
333
|
+
| Catalog: per-table index usage stats | `deepsql indexes usage <table>` |
|
|
334
|
+
| Catalog: index health report | `deepsql indexes health` |
|
|
312
335
|
| Daily digest (anomalies + AI commentary) | `deepsql digest`, `deepsql digest 7` |
|
|
313
336
|
| Streaming AI optimization for a slow query | `deepsql slow-queries optimize --query-id <id>` |
|
|
314
337
|
|
package/deepsql-phase1-lib.js
CHANGED
|
@@ -143,6 +143,56 @@ const TOOL_DEFINITIONS = [
|
|
|
143
143
|
additionalProperties: false,
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: "apply_index_recommendation",
|
|
148
|
+
description:
|
|
149
|
+
"Apply (or dry-run) an index recommendation and measure the before/after benefit on the queries that motivated it. " +
|
|
150
|
+
"Default mode is DRY_RUN — no writes; uses HypoPG (Postgres-only) to install a virtual index in the session, EXPLAIN-diffs the cost on each contributing query, then resets. " +
|
|
151
|
+
"APPLY mode runs the real DDL (CREATE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY on Postgres so the operation doesn't lock the table). " +
|
|
152
|
+
"APPLY_AND_MEASURE additionally runs EXPLAIN ANALYZE before and after for wall-clock timings — slowest, only opt in when you're OK executing the contributing queries against the target DB. " +
|
|
153
|
+
"Both APPLY modes require `confirm: true` — write operations don't happen by accident. " +
|
|
154
|
+
"Returns the executed DDL, the planner-cost delta, per-sample measurements (each contributing query's before/after cost), and an aggregate improvement percentage.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
recommendationId: { type: "string", description: "Recommendation row id (from get_index_recommendations)." },
|
|
159
|
+
mode: {
|
|
160
|
+
type: "string",
|
|
161
|
+
enum: ["DRY_RUN", "APPLY", "APPLY_AND_MEASURE"],
|
|
162
|
+
description: "Default DRY_RUN. APPLY mutates the database; APPLY_AND_MEASURE also runs EXPLAIN ANALYZE."
|
|
163
|
+
},
|
|
164
|
+
confirm: {
|
|
165
|
+
type: "boolean",
|
|
166
|
+
description: "Required `true` for APPLY and APPLY_AND_MEASURE. Defaults to false."
|
|
167
|
+
},
|
|
168
|
+
concurrent: {
|
|
169
|
+
type: "boolean",
|
|
170
|
+
description: "Postgres-only. When true (default), CREATE/DROP runs CONCURRENTLY (no table lock, but waits for every pre-existing transaction). Set false on small dev tables when the brief ACCESS EXCLUSIVE lock is acceptable."
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: ["recommendationId"],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "get_index_recommendations",
|
|
179
|
+
description:
|
|
180
|
+
"Get DeepSQL's pre-computed top index recommendations for a connection. Recommendations are workload-weighted (Σ calls × mean_exec_time, the pganalyze / Microsoft DTA 'total time' ROI metric) and aggregated across many slow-query log fetches over a configurable lookback (default 30 days). Column ordering for composite indexes follows industry rules (equality before range, selectivity-ranked, ORDER BY suffix only with full-equality prefix, capped at 3 columns). Covers both CREATE_INDEX and DROP_INDEX (unused + redundant prefix-duplicate) candidates. Each result carries net benefit (workload − write cost), the contributing query fingerprints, and call/duration metrics — so a caller can audit *why* each suggestion exists rather than trust a heuristic. Returns top N (default 5) PENDING recommendations.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
185
|
+
limit: {
|
|
186
|
+
type: "integer",
|
|
187
|
+
minimum: 1,
|
|
188
|
+
maximum: 50,
|
|
189
|
+
description: "Number of recommendations to return. Defaults to 5.",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
required: ["connectionId"],
|
|
193
|
+
additionalProperties: false,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
146
196
|
{
|
|
147
197
|
name: "analyze_slow_queries",
|
|
148
198
|
description:
|
|
@@ -551,6 +601,92 @@ function summarizeAntiPatterns(payload, kind) {
|
|
|
551
601
|
return `${list.length} query anti-pattern(s)${sevStr ? ` (${sevStr})` : ""}.`;
|
|
552
602
|
}
|
|
553
603
|
|
|
604
|
+
function formatMillisHuman(ms) {
|
|
605
|
+
if (ms == null || !Number.isFinite(ms) || ms <= 0) return null;
|
|
606
|
+
if (ms >= 86_400_000) return `${(ms / 86_400_000).toFixed(1)}d`;
|
|
607
|
+
if (ms >= 3_600_000) return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
608
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
609
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
610
|
+
return `${ms}ms`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function summarizeApplyResult(payload) {
|
|
614
|
+
if (!payload || typeof payload !== "object") {
|
|
615
|
+
return "Apply call returned no body.";
|
|
616
|
+
}
|
|
617
|
+
const status = payload.status || "?";
|
|
618
|
+
const mode = payload.mode || "?";
|
|
619
|
+
if (status === "BLOCKED_NEEDS_CONFIRMATION") {
|
|
620
|
+
return `[${mode}] blocked — pass confirm=true to mutate the database.`;
|
|
621
|
+
}
|
|
622
|
+
if (status === "NOT_FOUND") {
|
|
623
|
+
return `[${mode}] recommendation not found: ${payload.recommendationId || "?"}`;
|
|
624
|
+
}
|
|
625
|
+
if (status === "NO_USABLE_SAMPLES") {
|
|
626
|
+
return `[${mode}] no literal-bearing contributing queries available; cannot measure.`;
|
|
627
|
+
}
|
|
628
|
+
if (status === "FAILED") {
|
|
629
|
+
return `[${mode}] failed: ${payload.message || "(no message)"}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const lines = [];
|
|
633
|
+
lines.push(`[${mode}] ${status} — ${payload.executedDdl || "(no ddl)"}`);
|
|
634
|
+
if (payload.beforeCost != null && payload.afterCost != null) {
|
|
635
|
+
const pct = payload.costReductionPct;
|
|
636
|
+
lines.push(
|
|
637
|
+
` planner cost: ${payload.beforeCost.toFixed(0)} → ${payload.afterCost.toFixed(0)}` +
|
|
638
|
+
(pct != null ? ` (${pct >= 0 ? "−" : "+"}${Math.abs(pct).toFixed(1)}%)` : "")
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (payload.beforeWallTimeMs != null && payload.afterWallTimeMs != null) {
|
|
642
|
+
const pct = payload.wallTimeImprovementPct;
|
|
643
|
+
lines.push(
|
|
644
|
+
` wall time: ${payload.beforeWallTimeMs.toFixed(1)}ms → ${payload.afterWallTimeMs.toFixed(1)}ms` +
|
|
645
|
+
(pct != null ? ` (${pct >= 0 ? "−" : "+"}${Math.abs(pct).toFixed(1)}%)` : "")
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (Array.isArray(payload.samples) && payload.samples.length) {
|
|
649
|
+
lines.push(` ${payload.samples.length} contributing query sample(s):`);
|
|
650
|
+
for (const s of payload.samples.slice(0, 5)) {
|
|
651
|
+
const before = s.beforeCost != null ? s.beforeCost.toFixed(0) : "?";
|
|
652
|
+
const after = s.afterCost != null ? s.afterCost.toFixed(0) : "?";
|
|
653
|
+
lines.push(
|
|
654
|
+
` fp=${(s.fingerprint || "?").slice(0, 12)} cost ${before} → ${after}` +
|
|
655
|
+
(s.error ? ` (error: ${s.error})` : "")
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return lines.join("\n");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function summarizeIndexRecommendations(payload) {
|
|
663
|
+
const list = Array.isArray(payload) ? payload : payload?.recommendations || [];
|
|
664
|
+
if (!list.length) {
|
|
665
|
+
return "No pending index recommendations. The scheduler may not have run yet, or the workload has none worth flagging.";
|
|
666
|
+
}
|
|
667
|
+
const lines = list.slice(0, 10).map((rec, idx) => {
|
|
668
|
+
const table = rec.tableName || "?";
|
|
669
|
+
const prio = rec.priority || "?";
|
|
670
|
+
const occ = rec.occurrenceCount != null ? `seen ${rec.occurrenceCount}×` : "";
|
|
671
|
+
const net = formatMillisHuman(rec.netBenefitMs);
|
|
672
|
+
const writeCost = formatMillisHuman(rec.writeCostScore);
|
|
673
|
+
const evidence = rec.evidenceCount != null && rec.evidenceCount > 0
|
|
674
|
+
? `${rec.evidenceCount} ev` : "";
|
|
675
|
+
const isDrop = rec.kind === "DROP_INDEX";
|
|
676
|
+
const action = isDrop ? "DROP" : "CREATE";
|
|
677
|
+
const target = isDrop
|
|
678
|
+
? `${table}.${rec.indexName || "?"} (unused)`
|
|
679
|
+
: `${table}(${rec.columnNames || "?"})`;
|
|
680
|
+
// Net benefit is the DBA-grade signal — surface it prominently.
|
|
681
|
+
const benefitClause = net
|
|
682
|
+
? `net=${net} saved` + (writeCost ? `, write=${writeCost}` : "")
|
|
683
|
+
: (rec.estimatedImpact != null ? `impact ${rec.estimatedImpact}` : "");
|
|
684
|
+
const meta = [prio, occ, benefitClause, evidence].filter(Boolean).join(", ");
|
|
685
|
+
return `${idx + 1}. [${action}] ${target}${meta ? ` — ${meta}` : ""}`;
|
|
686
|
+
});
|
|
687
|
+
return `Top ${list.length} pending index recommendation(s):\n${lines.join("\n")}`;
|
|
688
|
+
}
|
|
689
|
+
|
|
554
690
|
function summarizeSlowQueries(payload) {
|
|
555
691
|
// Backend returns SlowQueryAnalysis with `topSlowQueries` (the field name
|
|
556
692
|
// varies; tolerate both `queries` and `topSlowQueries`).
|
|
@@ -649,6 +785,12 @@ function buildToolResult(name, payload, extra = {}) {
|
|
|
649
785
|
case "analyze_slow_queries":
|
|
650
786
|
summary = summarizeSlowQueries(payload);
|
|
651
787
|
break;
|
|
788
|
+
case "get_index_recommendations":
|
|
789
|
+
summary = summarizeIndexRecommendations(payload);
|
|
790
|
+
break;
|
|
791
|
+
case "apply_index_recommendation":
|
|
792
|
+
summary = summarizeApplyResult(payload);
|
|
793
|
+
break;
|
|
652
794
|
case "execute_sql":
|
|
653
795
|
summary = summarizeQueryResult(payload);
|
|
654
796
|
break;
|
|
@@ -775,6 +917,40 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
775
917
|
return buildToolResult(name, payload, { kind });
|
|
776
918
|
}
|
|
777
919
|
|
|
920
|
+
case "get_index_recommendations": {
|
|
921
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
922
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
923
|
+
const limit = clampInteger(args.limit, 1, 50, 5);
|
|
924
|
+
const payload = await callDeepSqlApi(
|
|
925
|
+
config,
|
|
926
|
+
`/index-recommendations/${encodeURIComponent(connectionId)}/top?limit=${limit}`,
|
|
927
|
+
);
|
|
928
|
+
return buildToolResult(name, payload);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
case "apply_index_recommendation": {
|
|
932
|
+
const recommendationId = String(args.recommendationId || "").trim();
|
|
933
|
+
if (!recommendationId) return buildToolError("recommendationId is required.");
|
|
934
|
+
const mode = String(args.mode || "DRY_RUN").toUpperCase();
|
|
935
|
+
if (!["DRY_RUN", "APPLY", "APPLY_AND_MEASURE"].includes(mode)) {
|
|
936
|
+
return buildToolError(`Unknown mode: ${mode}. Expected DRY_RUN, APPLY, or APPLY_AND_MEASURE.`);
|
|
937
|
+
}
|
|
938
|
+
const confirm = args.confirm === true;
|
|
939
|
+
if ((mode === "APPLY" || mode === "APPLY_AND_MEASURE") && !confirm) {
|
|
940
|
+
return buildToolError(
|
|
941
|
+
`Mode ${mode} mutates the target database. Re-call with confirm=true to proceed.`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const concurrent = args.concurrent === false ? false : true;
|
|
945
|
+
const qs = `?mode=${encodeURIComponent(mode)}&confirm=${confirm}&concurrent=${concurrent}`;
|
|
946
|
+
const payload = await callDeepSqlApi(
|
|
947
|
+
config,
|
|
948
|
+
`/index-recommendations/${encodeURIComponent(recommendationId)}/apply${qs}`,
|
|
949
|
+
{ method: "POST" },
|
|
950
|
+
);
|
|
951
|
+
return buildToolResult(name, payload);
|
|
952
|
+
}
|
|
953
|
+
|
|
778
954
|
case "analyze_slow_queries": {
|
|
779
955
|
const connectionId = String(args.connectionId || "").trim();
|
|
780
956
|
if (!connectionId) return buildToolError("connectionId is required.");
|
|
@@ -898,5 +1074,7 @@ module.exports = {
|
|
|
898
1074
|
stripTrailingSemicolons,
|
|
899
1075
|
stripSqlComments,
|
|
900
1076
|
stripSqlStringLiterals,
|
|
1077
|
+
summarizeApplyResult,
|
|
1078
|
+
summarizeIndexRecommendations,
|
|
901
1079
|
validateReadOnlySql,
|
|
902
1080
|
};
|
package/package.json
CHANGED
package/skills/SKILL_BODY.md
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# DeepSQL — your database DBA consult
|
|
2
2
|
|
|
3
|
-
You have
|
|
4
|
-
the live schema, business rules, FK relationships, and anti-patterns of
|
|
5
|
-
the database the user is working against.** Treat it the way a thoughtful
|
|
6
|
-
engineer treats a DBA: consult before you commit anything schema-shaped.
|
|
3
|
+
You have two DeepSQL surfaces available:
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
1. **MCP tools** — JSON-RPC tools loaded into your session (10 of them).
|
|
6
|
+
Use these for in-session retrieval and SQL execution.
|
|
7
|
+
|
|
8
|
+
2. **`deepsql` CLI** — a shell binary on the user's PATH (~19 commands).
|
|
9
|
+
Use this for things the MCP doesn't expose (index health, daily
|
|
10
|
+
digest, slow-query streaming, connection management, admin ops, the
|
|
11
|
+
plan-analysis `analyze` command, etc.). You can shell out to the CLI
|
|
12
|
+
yourself when appropriate; you can also point the user at the
|
|
13
|
+
command if it's interactive.
|
|
14
|
+
|
|
15
|
+
**DeepSQL is the source of truth for the live schema, business rules, FK
|
|
16
|
+
relationships, and anti-patterns of the database the user is working
|
|
17
|
+
against.** Treat it the way a thoughtful engineer treats a DBA: consult
|
|
18
|
+
before you commit anything schema-shaped. This skill triggers any time
|
|
19
|
+
the user is doing database work. The rules below are non-negotiable.
|
|
10
20
|
|
|
11
21
|
---
|
|
12
22
|
|
|
@@ -65,21 +75,115 @@ usually doesn't know about either; that's exactly why DeepSQL exists.
|
|
|
65
75
|
|
|
66
76
|
---
|
|
67
77
|
|
|
68
|
-
##
|
|
78
|
+
## MCP tools — your in-session toolkit
|
|
69
79
|
|
|
70
|
-
|
|
|
80
|
+
| Tool | When to call |
|
|
71
81
|
|---|---|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
82
|
+
| `list_connections` | Always first; get the UUID. |
|
|
83
|
+
| `get_brain_context(connectionId, question)` | Step 2 of the checklist above. The most important tool. |
|
|
84
|
+
| `get_schema(connectionId)` | Full column inventory for the tables you're touching. |
|
|
85
|
+
| `get_database_objects(connectionId)` | Tables/views/functions/procedures (broader than schema). |
|
|
86
|
+
| `list_business_rules(connectionId, question?)` | Rules the SQL must respect. |
|
|
87
|
+
| `get_relationships(connectionId)` | Foreign keys (declared + inferred-with-confidence). |
|
|
88
|
+
| `get_anti_patterns(connectionId, kind="table"\|"query")` | Patterns to avoid in this DB. |
|
|
89
|
+
| `analyze_slow_queries(connectionId, thresholdMs?, limit?)` | Snapshot of slow queries from live stats. |
|
|
90
|
+
| `execute_sql(connectionId, query, ...)` | Run any SQL — SELECT for everyone, DML/DDL for admins (two-step confirm). |
|
|
91
|
+
| `analyze_query_plan(connectionId, query, useAnalyze=false)` | AI-enriched plan analysis (issues + index recs + summary). |
|
|
75
92
|
|
|
76
93
|
`EXPLAIN` and `EXPLAIN ANALYZE` are just SQL — type them as the query if
|
|
77
94
|
you want raw plan output. Use `analyze_query_plan` when you want the
|
|
78
|
-
AI-enriched analysis
|
|
95
|
+
AI-enriched analysis.
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
---
|
|
81
98
|
|
|
82
|
-
`
|
|
99
|
+
## `deepsql` CLI — for everything the MCP doesn't expose
|
|
100
|
+
|
|
101
|
+
The CLI is the user's primary interface to DeepSQL. As a coding agent,
|
|
102
|
+
**you can shell out to it** to do things the MCP doesn't have, or to
|
|
103
|
+
help the user when interactive setup is required (login, MCP install,
|
|
104
|
+
new connection registration). Always pass `--caller-agent <your-name>`
|
|
105
|
+
on shell-outs so the audit log captures the chain ("deepsql query
|
|
106
|
+
called by claude-code via deepsql CLI").
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
deepsql query "SELECT 1" --connection prod-pg --caller-agent claude-code --json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Command catalog (19 top-level commands)
|
|
113
|
+
|
|
114
|
+
| Command | What it does | MCP equivalent? |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `deepsql login` | Authorize CLI against a DeepSQL host (browser PKCE / device code / password) | none — interactive only |
|
|
117
|
+
| `deepsql logout` | Revoke the saved token | none |
|
|
118
|
+
| `deepsql whoami` | Show the logged-in user, role, URL, pinned connection | none |
|
|
119
|
+
| `deepsql config show\|set-default <url>\|path` | Manage saved profiles | none |
|
|
120
|
+
| `deepsql mcp` | Run the stdio MCP server | this skill spawns it |
|
|
121
|
+
| `deepsql mcp config --install --for <editor>` | Install MCP entry + this skill into editor config | none — interactive |
|
|
122
|
+
| `deepsql connections list\|use\|current\|unset\|schema\|add\|update\|remove\|test\|show\|init` | Full connection CRUD | partial: `list_connections`, `get_schema` |
|
|
123
|
+
| `deepsql query "<sql>" --connection <c>` | Execute SQL (admin: `--write` for mutations) | `execute_sql` |
|
|
124
|
+
| `deepsql analyze "<sql>" --connection <c>` | AI plan analysis (`--analyze` for EXPLAIN ANALYZE) | `analyze_query_plan` |
|
|
125
|
+
| `deepsql schema [tables\|objects] --connection <c>` | Dump full schema as JSON | `get_schema` / `get_database_objects` |
|
|
126
|
+
| `deepsql brain-context "<question>" --connection <c>` | Same retrieval as the MCP tool | `get_brain_context` |
|
|
127
|
+
| `deepsql business-rules --connection <c>` | List active business rules | `list_business_rules` |
|
|
128
|
+
| `deepsql relationships --connection <c>` | Inferred + validated FKs | `get_relationships` |
|
|
129
|
+
| `deepsql anti-patterns --connection <c> [--kind table\|query]` | Anti-patterns | `get_anti_patterns` |
|
|
130
|
+
| `deepsql digest [N]\|list\|show <id>` | **CLI-only**: daily digest of slow queries + AI commentary | none |
|
|
131
|
+
| `deepsql indexes list\|missing\|health\|unused\|duplicates\|usage <table>` | **CLI-only**: index recommendations and usage stats | none |
|
|
132
|
+
| `deepsql slow-queries latest\|history\|analyze\|optimize\|delete` | Slow-query analyses; `optimize` streams AI optimization steps live (SSE) | partial: `analyze_slow_queries` |
|
|
133
|
+
| `deepsql users list\|get\|add\|set-role\|lock\|unlock\|disable\|resend-invite\|reset-password\|delete` | **Admin-only, CLI-only**: workspace user management | none |
|
|
134
|
+
| `deepsql access list\|grant\|revoke\|policy <user> <conn>` | **Admin-only, CLI-only**: per-connection access grants + chat policy editing in $EDITOR | none |
|
|
135
|
+
| `deepsql permissions list\|override\|reset` | **Admin-only, CLI-only**: role-based permission overrides | none |
|
|
136
|
+
| `deepsql setup` | **Admin-only**: post-install wizard for SMTP/email + Slack | none |
|
|
137
|
+
|
|
138
|
+
Run `deepsql <command> --help` for option-level detail. Run
|
|
139
|
+
`deepsql --help` for the live catalog (the CLI is the source of truth
|
|
140
|
+
if this table goes stale).
|
|
141
|
+
|
|
142
|
+
### CLI-only capabilities — point the user at these or run them
|
|
143
|
+
|
|
144
|
+
When the user asks for something only the CLI does, either run it via
|
|
145
|
+
shell-out (with `--caller-agent`) or tell the user the exact command:
|
|
146
|
+
|
|
147
|
+
| User asks for | Run / suggest |
|
|
148
|
+
|---|---|
|
|
149
|
+
| "What changed in the database recently?" / "Today's report" | `deepsql digest` (most recent) or `deepsql digest 7` (last week) |
|
|
150
|
+
| "What indexes are we missing?" / "Index advice" | `deepsql indexes missing --connection <c>` |
|
|
151
|
+
| "Are any indexes unused?" / "Index bloat" | `deepsql indexes unused --connection <c>` |
|
|
152
|
+
| "Duplicate indexes?" | `deepsql indexes duplicates --connection <c>` |
|
|
153
|
+
| "Index health on this connection" | `deepsql indexes health --connection <c>` |
|
|
154
|
+
| "Indexes on `<table>` and how often they're used" | `deepsql indexes usage <table> --connection <c>` |
|
|
155
|
+
| "Optimize this slow query with AI" / "Stream me a fix" | `deepsql slow-queries optimize --connection <c> --query-id <id>` |
|
|
156
|
+
| "Add a new database connection" | `deepsql connections add` (interactive) or `--from-file <path>` |
|
|
157
|
+
| "Test a connection without saving" | `deepsql connections test --from-file <path>` |
|
|
158
|
+
| "Trigger brain re-initialization for this connection" | `deepsql connections init <name> --wait` |
|
|
159
|
+
| "Add a user / change a user's role" | `deepsql users add` / `deepsql users set-role <ref> <role>` |
|
|
160
|
+
| "Grant / revoke connection access" | `deepsql access grant --user <ref> --connection <c> --level read\|write\|admin` |
|
|
161
|
+
| "Edit the chat data-access policy for `<user>` on `<conn>`" | `deepsql access policy <user> <conn>` (opens `$EDITOR`) |
|
|
162
|
+
|
|
163
|
+
### Shelling out from the agent — the convention
|
|
164
|
+
|
|
165
|
+
When you run `deepsql` yourself:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
deepsql <command> [options] --caller-agent <your-agent-id> --json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- **Always pass `--caller-agent`** (or set `DEEPSQL_CALLER_AGENT` env
|
|
172
|
+
var once) so the audit row captures the chain. Use a stable id like
|
|
173
|
+
`claude-code`, `cursor`, or `codex`.
|
|
174
|
+
- **Prefer `--json`** for parseable output. Without it, output is
|
|
175
|
+
pretty-printed for humans and harder to parse.
|
|
176
|
+
- **Don't chain destructive ops without confirmation.** `deepsql
|
|
177
|
+
connections remove`, `deepsql users delete`, `deepsql permissions
|
|
178
|
+
override --revoke`, etc. all support `--yes` to skip the prompt — but
|
|
179
|
+
YOU should not pass `--yes` unless the user has explicitly approved
|
|
180
|
+
the specific action.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Mutations are role-gated and two-step (both MCP and CLI)
|
|
185
|
+
|
|
186
|
+
`execute_sql` (MCP) and `deepsql query` (CLI) enforce the same policy:
|
|
83
187
|
|
|
84
188
|
- **Developer + SELECT/WITH/SHOW/EXPLAIN** → runs immediately.
|
|
85
189
|
- **Developer + DML/DDL** → 403 `EDITOR_MUTATION_FORBIDDEN`. Don't retry.
|
|
@@ -89,26 +193,51 @@ AI-enriched analysis (issues, index recommendations, written summary).
|
|
|
89
193
|
- **Admin + DML/DDL (no `confirmMutation`)** → returns
|
|
90
194
|
`requiresConfirmation: true` with a `warnings` array. **Show the
|
|
91
195
|
warnings to the user verbatim. Wait for explicit OK.** Then re-call
|
|
92
|
-
with `confirmMutation: true
|
|
93
|
-
|
|
196
|
+
with `confirmMutation: true` (MCP) or `--write` (CLI). **Do not
|
|
197
|
+
silently retry on the user's behalf** — that defeats the
|
|
94
198
|
confirmation step.
|
|
95
199
|
|
|
96
|
-
|
|
200
|
+
## Row limits
|
|
201
|
+
|
|
202
|
+
`execute_sql` / `deepsql query` defaults to 100 rows, max 1000. If you
|
|
203
|
+
asked for "all customers" and got 100, that's the limit kicking in —
|
|
204
|
+
not the real count. Either bump `--limit`/`limit:` or `SELECT COUNT(*)`
|
|
205
|
+
first.
|
|
97
206
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Helping the user when DeepSQL itself isn't set up
|
|
210
|
+
|
|
211
|
+
The user might not have DeepSQL fully wired up when they ask their
|
|
212
|
+
first database question. If you get a clear "not configured" error,
|
|
213
|
+
run / suggest these:
|
|
214
|
+
|
|
215
|
+
| Symptom | Fix |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `deepsql --version` not found / not on PATH | `npm install -g @deepsql/mcp@latest` (Node ≥ 20). |
|
|
218
|
+
| MCP tools missing from your session | Run `deepsql mcp config --install --for claude-code` (or `cursor`/`codex`/`claude-desktop`) on the user's machine. They restart the editor. |
|
|
219
|
+
| CLI says "No saved DeepSQL profile" | `deepsql login --url https://<their-deepsql-host>` (browser flow on desktop, `--device` for SSH boxes). |
|
|
220
|
+
| MCP tools error with 401 | Token expired. `deepsql logout && deepsql login --url <host>` then restart the editor. |
|
|
221
|
+
| `connections list` is empty | Walk the user through `deepsql connections add` interactively, or use `--from-file <path>` with a JSON config file. |
|
|
222
|
+
| `connections test` reports "Missing privileges: SELECT, ..." | The DB user has insufficient grants. The connection saves anyway (read access is enough for some features), but flag it. |
|
|
223
|
+
| Connection config schema | `deepsql connections schema --json` (JSON Schema for the input format). |
|
|
101
224
|
|
|
102
225
|
---
|
|
103
226
|
|
|
104
|
-
## Every call is audited
|
|
227
|
+
## Every call is audited (both surfaces)
|
|
105
228
|
|
|
106
|
-
Every tool call
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
229
|
+
Every MCP tool call AND every `deepsql` CLI invocation that hits the
|
|
230
|
+
backend is logged to the DeepSQL `security_events` table with:
|
|
231
|
+
|
|
232
|
+
- the user's identity (from the bearer token or saved profile)
|
|
233
|
+
- the editor / agent that originated the request (`claude-code`,
|
|
234
|
+
`cursor`, `codex`, etc. — from the `--caller-agent` flag or
|
|
235
|
+
`DEEPSQL_MCP_USER_ID` env var)
|
|
236
|
+
- the surface (`mcp` vs `cli`)
|
|
237
|
+
- the connection, the truncated statement, and the outcome
|
|
238
|
+
|
|
239
|
+
Workspace admins can search this. Don't do anything through these
|
|
240
|
+
tools you wouldn't be willing to defend in that view.
|
|
112
241
|
|
|
113
242
|
---
|
|
114
243
|
|
|
@@ -116,10 +245,17 @@ view.
|
|
|
116
245
|
|
|
117
246
|
The complete runtime guide — every decision-tree branch, every foot-gun,
|
|
118
247
|
all three session playbooks (answer-a-question, mutation, DBA-consult) —
|
|
119
|
-
lives in `
|
|
120
|
-
|
|
248
|
+
lives in the package's `CLAUDE.md`. After install:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
node_modules/@deepsql/mcp/CLAUDE.md # local install
|
|
252
|
+
$(npm root -g)/@deepsql/mcp/CLAUDE.md # global install
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The CLI is its own source of truth too — if this table drifts from
|
|
256
|
+
reality, run:
|
|
121
257
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
258
|
+
```bash
|
|
259
|
+
deepsql --help # live command list
|
|
260
|
+
deepsql <command> --help # full options for one command
|
|
261
|
+
```
|
package/src/cli.js
CHANGED
|
@@ -59,7 +59,7 @@ const COMMAND_LIST = [
|
|
|
59
59
|
["business-rules", false, "List active business rules and SQL guardrails"],
|
|
60
60
|
["relationships", false, "List inferred and validated FK relationships"],
|
|
61
61
|
["anti-patterns", false, "List schema- or query-level anti-patterns"],
|
|
62
|
-
["indexes", true, "
|
|
62
|
+
["indexes", true, "Workload-weighted index advisor + catalog diagnostics (top, apply, list, missing, health, unused, duplicates, usage)"],
|
|
63
63
|
["users", true, "Manage workspace users (admin)"],
|
|
64
64
|
["access", true, "Manage per-connection access grants (admin)"],
|
|
65
65
|
["permissions", true, "Manage role-based permission overrides (admin)"],
|
|
@@ -265,23 +265,41 @@ const COMMAND_HELP = {
|
|
|
265
265
|
},
|
|
266
266
|
|
|
267
267
|
indexes: {
|
|
268
|
-
description:
|
|
268
|
+
description:
|
|
269
|
+
"Unified index advisor — workload-weighted recommendations + catalog diagnostics. " +
|
|
270
|
+
"`apply` is the one mutation; dry-run is the default and uses HypoPG on Postgres " +
|
|
271
|
+
"to estimate cost-delta without writes. Write modes (apply / apply-and-measure) " +
|
|
272
|
+
"require --confirm.",
|
|
269
273
|
usage: "deepsql indexes <subcommand> [options]",
|
|
270
274
|
subcommands: [
|
|
271
|
-
|
|
272
|
-
["
|
|
273
|
-
["
|
|
274
|
-
["
|
|
275
|
-
["
|
|
276
|
-
["
|
|
275
|
+
// Advisor (workload-weighted)
|
|
276
|
+
["top [--limit N]", "Pre-computed top-N pending recommendations (default 5, max 50) — ranked by net benefit, with HypoPG cost-delta + contributing-query evidence"],
|
|
277
|
+
["list [--all] [--status PENDING|APPLIED|DISMISSED]", "All recommendations (defaults to PENDING) — simpler view than `top`"],
|
|
278
|
+
["show <id> --connection <name>", "Full detail: workload, write-cost, HypoPG cost, contributing queries"],
|
|
279
|
+
["refresh", "Force a fresh accumulation cycle (POST /generate)"],
|
|
280
|
+
["apply <id> [--mode <m>] [--confirm] [--no-concurrent]", "Run or dry-run a recommendation with before/after measurement"],
|
|
281
|
+
["dismiss <id>", "Mark a recommendation as DISMISSED"],
|
|
282
|
+
// Catalog diagnostics
|
|
283
|
+
["missing", "Missing-index suggestions from the catalog advisor"],
|
|
284
|
+
["health", "Comprehensive index health report"],
|
|
285
|
+
["unused", "Indexes the engine has not used (live pg_stat_user_indexes / sys.* probe)"],
|
|
286
|
+
["duplicates", "Duplicate or redundant indexes"],
|
|
287
|
+
["usage <table>", "Per-table index usage statistics"],
|
|
277
288
|
],
|
|
278
289
|
options: [
|
|
279
|
-
["--connection <name>",
|
|
280
|
-
["--
|
|
281
|
-
["--
|
|
282
|
-
["--
|
|
290
|
+
["--connection <name>", "Connection to inspect"],
|
|
291
|
+
["--limit <n>", "top: number of recommendations (default 5, max 50)"],
|
|
292
|
+
["--all", "list: include APPLIED and DISMISSED rows"],
|
|
293
|
+
["--status PENDING|APPLIED|DISMISSED", "list: filter by status"],
|
|
294
|
+
["--mode dry-run|apply|apply-and-measure", "apply: defaults to dry-run (HypoPG; no writes)"],
|
|
295
|
+
["--confirm", "apply: required for apply / apply-and-measure (write modes)"],
|
|
296
|
+
["--no-concurrent", "apply: skip CREATE/DROP INDEX CONCURRENTLY (small dev tables)"],
|
|
297
|
+
["--json", "Raw backend JSON instead of the terminal-friendly table"],
|
|
283
298
|
],
|
|
284
|
-
notes:
|
|
299
|
+
notes:
|
|
300
|
+
"Mirrors the MCP `get_index_recommendations` + `apply_index_recommendation` tools — " +
|
|
301
|
+
"use this when you're at a terminal, the MCP tools when you're inside an AI client. " +
|
|
302
|
+
"Same backend, same data, same safety contract.",
|
|
285
303
|
},
|
|
286
304
|
|
|
287
305
|
users: {
|
|
@@ -360,6 +378,7 @@ const COMMAND_HELP = {
|
|
|
360
378
|
],
|
|
361
379
|
notes: "Org and LLM config are set at install time and are NOT touched by this wizard.",
|
|
362
380
|
},
|
|
381
|
+
|
|
363
382
|
};
|
|
364
383
|
|
|
365
384
|
// ─── Color helpers ──────────────────────────────────────────────────────────
|
|
@@ -564,6 +583,10 @@ function buildOpts(parsed) {
|
|
|
564
583
|
skipComplete: !!f.skipComplete,
|
|
565
584
|
// Confirmations
|
|
566
585
|
yes: !!f.yes || !!f.y,
|
|
586
|
+
// Index recommendations (apply tool)
|
|
587
|
+
mode: f.mode || null,
|
|
588
|
+
confirm: !!f.confirm,
|
|
589
|
+
noConcurrent: !!f.noConcurrent,
|
|
567
590
|
// Connection management
|
|
568
591
|
fromFile: f.fromFile || null,
|
|
569
592
|
fromStdin: !!f.fromStdin,
|