@deepsql/mcp 0.10.2 → 0.13.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/AGENT-SETUP.md +72 -29
- package/CLAUDE.md +382 -0
- package/deepsql-phase1-lib.js +102 -25
- package/package.json +5 -4
- package/skills/SKILL_BODY.md +125 -0
- package/src/api/client.js +35 -1
- package/src/cli.js +90 -20
- package/src/cli.test.js +2 -2
- package/src/commands/analyze.js +165 -0
- package/src/commands/analyze.test.js +180 -0
- package/src/commands/explain.js +18 -34
- package/src/commands/indexes.js +306 -0
- package/src/commands/indexes.test.js +298 -0
- package/src/commands/mcp.js +439 -7
- package/src/commands/mcp.test.js +384 -0
- package/src/commands/query.js +95 -13
- package/src/commands/query.test.js +214 -0
package/deepsql-phase1-lib.js
CHANGED
|
@@ -168,8 +168,14 @@ const TOOL_DEFINITIONS = [
|
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
|
-
name: "
|
|
172
|
-
description:
|
|
171
|
+
name: "execute_sql",
|
|
172
|
+
description:
|
|
173
|
+
"Execute a SQL statement through DeepSQL. Routes through the same policy "
|
|
174
|
+
+ "gate as the SQL Editor: developers can run SELECT/WITH/SHOW/EXPLAIN; admins "
|
|
175
|
+
+ "can additionally run DML/DDL with a two-step confirmation. Pass `confirmMutation: "
|
|
176
|
+
+ "true` to confirm a mutation. EXPLAIN and EXPLAIN ANALYZE are valid SQL — just "
|
|
177
|
+
+ "type them as the query, no separate mode flag needed. Multi-statement input "
|
|
178
|
+
+ "and unsafe DELETE/UPDATE without WHERE are still rejected.",
|
|
173
179
|
inputSchema: {
|
|
174
180
|
type: "object",
|
|
175
181
|
properties: {
|
|
@@ -179,19 +185,29 @@ const TOOL_DEFINITIONS = [
|
|
|
179
185
|
},
|
|
180
186
|
query: {
|
|
181
187
|
type: "string",
|
|
182
|
-
description:
|
|
188
|
+
description:
|
|
189
|
+
"SQL to execute. Any single-statement SQL the connection's actor is "
|
|
190
|
+
+ "allowed to run: SELECT/WITH/SHOW/EXPLAIN for any role, plus DML/DDL "
|
|
191
|
+
+ "for admins.",
|
|
183
192
|
},
|
|
184
193
|
limit: {
|
|
185
194
|
type: "integer",
|
|
186
195
|
minimum: 1,
|
|
187
196
|
maximum: 1000,
|
|
188
|
-
description: "
|
|
197
|
+
description: "Row limit for SELECT results. Defaults to 100.",
|
|
189
198
|
},
|
|
190
199
|
timeoutSeconds: {
|
|
191
200
|
type: "integer",
|
|
192
201
|
minimum: 1,
|
|
193
202
|
maximum: 60,
|
|
194
|
-
description: "
|
|
203
|
+
description: "Per-query timeout. Defaults to the backend default.",
|
|
204
|
+
},
|
|
205
|
+
confirmMutation: {
|
|
206
|
+
type: "boolean",
|
|
207
|
+
description:
|
|
208
|
+
"Required `true` on the second call when running DML/DDL — the first "
|
|
209
|
+
+ "call returns `requiresConfirmation: true` with a warnings list; "
|
|
210
|
+
+ "review and re-send with confirmMutation=true to actually execute.",
|
|
195
211
|
},
|
|
196
212
|
},
|
|
197
213
|
required: ["connectionId", "query"],
|
|
@@ -199,8 +215,14 @@ const TOOL_DEFINITIONS = [
|
|
|
199
215
|
},
|
|
200
216
|
},
|
|
201
217
|
{
|
|
202
|
-
name: "
|
|
203
|
-
description:
|
|
218
|
+
name: "analyze_query_plan",
|
|
219
|
+
description:
|
|
220
|
+
"Get DeepSQL's AI-enriched analysis of a query's execution plan. Returns the "
|
|
221
|
+
+ "parsed plan tree, performance issues, index recommendations, and a written "
|
|
222
|
+
+ "summary that takes into account the connection's schema, business rules, "
|
|
223
|
+
+ "and anti-patterns. With `useAnalyze: true` the query is actually executed "
|
|
224
|
+
+ "(EXPLAIN ANALYZE semantics) — mutating statements then go through the same "
|
|
225
|
+
+ "admin/WHERE/confirmation gates as execute_sql.",
|
|
204
226
|
inputSchema: {
|
|
205
227
|
type: "object",
|
|
206
228
|
properties: {
|
|
@@ -210,7 +232,21 @@ const TOOL_DEFINITIONS = [
|
|
|
210
232
|
},
|
|
211
233
|
query: {
|
|
212
234
|
type: "string",
|
|
213
|
-
description:
|
|
235
|
+
description:
|
|
236
|
+
"The underlying SQL to plan. Do NOT wrap in EXPLAIN — the server does "
|
|
237
|
+
+ "that based on `useAnalyze`.",
|
|
238
|
+
},
|
|
239
|
+
useAnalyze: {
|
|
240
|
+
type: "boolean",
|
|
241
|
+
description:
|
|
242
|
+
"If true, run EXPLAIN ANALYZE (actually executes the query for real "
|
|
243
|
+
+ "timings). For mutating statements this requires admin role + confirm.",
|
|
244
|
+
},
|
|
245
|
+
confirmMutation: {
|
|
246
|
+
type: "boolean",
|
|
247
|
+
description:
|
|
248
|
+
"Required `true` to confirm a mutating useAnalyze=true call. Same "
|
|
249
|
+
+ "two-step flow as execute_sql.",
|
|
214
250
|
},
|
|
215
251
|
},
|
|
216
252
|
required: ["connectionId", "query"],
|
|
@@ -354,6 +390,20 @@ function buildHeaders(config, extraHeaders = {}) {
|
|
|
354
390
|
headers.Authorization = `Bearer ${config.authToken}`;
|
|
355
391
|
}
|
|
356
392
|
|
|
393
|
+
// Origin-tracking headers so the backend audit row can distinguish
|
|
394
|
+
// CLI/MCP traffic and identify which editor invoked the MCP server.
|
|
395
|
+
// `clientAgent` carries the value of DEEPSQL_MCP_USER_ID — editor configs
|
|
396
|
+
// set this to "claude-desktop", "cursor-mcp", "codex-mcp", etc.
|
|
397
|
+
if (config.clientType) {
|
|
398
|
+
headers["X-DeepSQL-Client-Type"] = config.clientType;
|
|
399
|
+
}
|
|
400
|
+
if (config.clientAgent) {
|
|
401
|
+
headers["X-DeepSQL-Client-Agent"] = config.clientAgent;
|
|
402
|
+
}
|
|
403
|
+
if (config.clientVersion) {
|
|
404
|
+
headers["X-DeepSQL-Client-Version"] = config.clientVersion;
|
|
405
|
+
}
|
|
406
|
+
|
|
357
407
|
return headers;
|
|
358
408
|
}
|
|
359
409
|
|
|
@@ -554,10 +604,10 @@ function buildToolResult(name, payload, extra = {}) {
|
|
|
554
604
|
case "analyze_slow_queries":
|
|
555
605
|
summary = summarizeSlowQueries(payload);
|
|
556
606
|
break;
|
|
557
|
-
case "
|
|
607
|
+
case "execute_sql":
|
|
558
608
|
summary = summarizeQueryResult(payload);
|
|
559
609
|
break;
|
|
560
|
-
case "
|
|
610
|
+
case "analyze_query_plan":
|
|
561
611
|
summary = summarizeExplain(payload);
|
|
562
612
|
break;
|
|
563
613
|
default:
|
|
@@ -698,42 +748,53 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
698
748
|
return buildToolResult(name, payload);
|
|
699
749
|
}
|
|
700
750
|
|
|
701
|
-
case "
|
|
751
|
+
case "execute_sql": {
|
|
702
752
|
const connectionId = String(args.connectionId || "").trim();
|
|
703
|
-
const
|
|
704
|
-
if (!
|
|
705
|
-
|
|
706
|
-
}
|
|
753
|
+
const query = String(args.query || "").trim();
|
|
754
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
755
|
+
if (!query) return buildToolError("query is required.");
|
|
707
756
|
|
|
757
|
+
// Talk to the canonical Editor endpoint. Backend enforces role-based
|
|
758
|
+
// mutation policy + per-connection ACL + chat data-access policy +
|
|
759
|
+
// WHERE-clause guard + two-step confirmation, then audits the call.
|
|
760
|
+
// Client-side parser validation removed in 0.13.0 — the backend is
|
|
761
|
+
// the source of truth and was always going to be.
|
|
708
762
|
const payload = await callDeepSqlApi(
|
|
709
763
|
config,
|
|
710
|
-
|
|
764
|
+
`/connections/${encodeURIComponent(connectionId)}/query`,
|
|
711
765
|
{
|
|
712
766
|
method: "POST",
|
|
713
767
|
json: {
|
|
714
|
-
|
|
715
|
-
query: validation.normalizedQuery,
|
|
768
|
+
query,
|
|
716
769
|
limit: clampInteger(args.limit, 1, 1000, 100),
|
|
717
770
|
timeoutSeconds: clampInteger(args.timeoutSeconds, 1, 60, null),
|
|
771
|
+
mutationConfirmed: args.confirmMutation === true,
|
|
718
772
|
},
|
|
719
773
|
},
|
|
720
774
|
);
|
|
721
775
|
|
|
776
|
+
// Surface a "requiresConfirmation" response as a non-error structured
|
|
777
|
+
// payload so the calling agent can read warnings and re-send with
|
|
778
|
+
// confirmMutation=true without parsing tool-error text.
|
|
722
779
|
return buildToolResult(name, payload);
|
|
723
780
|
}
|
|
724
781
|
|
|
725
|
-
case "
|
|
782
|
+
case "analyze_query_plan": {
|
|
726
783
|
const connectionId = String(args.connectionId || "").trim();
|
|
727
|
-
const
|
|
728
|
-
if (!
|
|
729
|
-
|
|
730
|
-
}
|
|
784
|
+
const query = String(args.query || "").trim();
|
|
785
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
786
|
+
if (!query) return buildToolError("query is required.");
|
|
731
787
|
|
|
732
|
-
|
|
788
|
+
// Route through the canonical Editor endpoint (ExplainController).
|
|
789
|
+
// For useAnalyze=true the backend applies the same mutation policy
|
|
790
|
+
// as execute_sql before wrapping in EXPLAIN ANALYZE.
|
|
791
|
+
const payload = await callDeepSqlApi(config, "/explain/analyze", {
|
|
733
792
|
method: "POST",
|
|
734
793
|
json: {
|
|
735
794
|
connectionId,
|
|
736
|
-
query
|
|
795
|
+
query,
|
|
796
|
+
useAnalyze: args.useAnalyze === true,
|
|
797
|
+
mutationConfirmed: args.confirmMutation === true,
|
|
737
798
|
},
|
|
738
799
|
});
|
|
739
800
|
|
|
@@ -749,12 +810,28 @@ function createConfigFromEnv(env = process.env) {
|
|
|
749
810
|
const rawBaseUrl = env.DEEPSQL_API_BASE_URL || "http://localhost:8080/api/";
|
|
750
811
|
const baseUrl = rawBaseUrl.endsWith("/") ? rawBaseUrl : `${rawBaseUrl}/`;
|
|
751
812
|
|
|
813
|
+
// Resolve our npm version once, lazily — `require("./package.json")`
|
|
814
|
+
// would normally pull it in, but we use a try/catch so the lib still
|
|
815
|
+
// works in test contexts where the package metadata isn't on disk.
|
|
816
|
+
let clientVersion = null;
|
|
817
|
+
try {
|
|
818
|
+
clientVersion = require("./package.json").version;
|
|
819
|
+
} catch {
|
|
820
|
+
// best-effort
|
|
821
|
+
}
|
|
822
|
+
|
|
752
823
|
return {
|
|
753
824
|
baseUrl,
|
|
754
825
|
authToken: env.DEEPSQL_AUTH_TOKEN || "",
|
|
755
826
|
timeoutMs: clampInteger(env.DEEPSQL_MCP_TIMEOUT_MS, 1000, 600000, 120000),
|
|
756
827
|
defaultUserId: env.DEEPSQL_MCP_USER_ID || "mcp-phase1",
|
|
757
828
|
defaultProjectId: env.DEEPSQL_MCP_PROJECT_ID || "mcp-phase1",
|
|
829
|
+
// Origin metadata for the backend audit row. The MCP server always
|
|
830
|
+
// identifies as `mcp`; the agent name comes from DEEPSQL_MCP_USER_ID
|
|
831
|
+
// which editor configs set to claude-desktop / cursor-mcp / codex-mcp.
|
|
832
|
+
clientType: "mcp",
|
|
833
|
+
clientAgent: env.DEEPSQL_MCP_USER_ID || null,
|
|
834
|
+
clientVersion,
|
|
758
835
|
};
|
|
759
836
|
}
|
|
760
837
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepsql/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
|
|
5
5
|
"bin": {
|
|
6
|
-
"deepsql": "
|
|
7
|
-
"deepsql-mcp": "
|
|
6
|
+
"deepsql": "bin/deepsql.js",
|
|
7
|
+
"deepsql-mcp": "deepsql-phase1-server.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./deepsql-phase1-server.js",
|
|
10
10
|
"files": [
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"CLAUDE.md",
|
|
13
13
|
"AGENT-SETUP.md",
|
|
14
14
|
"bin",
|
|
15
|
+
"skills",
|
|
15
16
|
"src",
|
|
16
17
|
"deepsql-phase1-server.js",
|
|
17
18
|
"deepsql-phase1-lib.js",
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"codex_config.customer.example.toml"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
|
-
"test": "node --test deepsql-phase1-lib.test.js src/**/*.test.js"
|
|
23
|
+
"test": "node --test deepsql-phase1-lib.test.js src/*.test.js src/**/*.test.js"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=20"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# DeepSQL — your database DBA consult
|
|
2
|
+
|
|
3
|
+
You have DeepSQL's MCP tools loaded. **DeepSQL is the source of truth for
|
|
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.
|
|
7
|
+
|
|
8
|
+
This skill triggers any time the user is doing database work. The rules
|
|
9
|
+
below are non-negotiable.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Trigger checklist — before generating any DDL, migration, or non-trivial SQL
|
|
14
|
+
|
|
15
|
+
The user said something like "add a table", "track this", "write a
|
|
16
|
+
migration", "design a model", "query the database", or "write the SELECT
|
|
17
|
+
for…". Before you generate **any** SQL or schema-shaped output, run:
|
|
18
|
+
|
|
19
|
+
1. `list_connections` — get the UUID of the connection the user means
|
|
20
|
+
(don't pass connection names anywhere; tools take UUIDs).
|
|
21
|
+
|
|
22
|
+
2. `get_brain_context(connectionId, "<one-line description of the feature/question>")`
|
|
23
|
+
— surfaces the tables, columns, FKs, training docs, and business rules
|
|
24
|
+
most relevant to the work at hand. **Read the results, don't just
|
|
25
|
+
regurgitate them.**
|
|
26
|
+
|
|
27
|
+
3. `get_schema(connectionId)` if you need a full column inventory for any
|
|
28
|
+
table `get_brain_context` surfaced. Don't infer column types from
|
|
29
|
+
variable names in the codebase — they drift.
|
|
30
|
+
|
|
31
|
+
4. `list_business_rules(connectionId, question="<feature>")` — rules the
|
|
32
|
+
feature MUST respect. If `always_filter_cancelled` is on, your
|
|
33
|
+
aggregate views inherit that filter from day one. Apply these silently;
|
|
34
|
+
don't ask the user permission to follow their own rules.
|
|
35
|
+
|
|
36
|
+
5. `get_relationships(connectionId)` if you're declaring a foreign key —
|
|
37
|
+
the brain may already infer it with a confidence score, and the FK
|
|
38
|
+
naming convention this team uses lives here.
|
|
39
|
+
|
|
40
|
+
6. `get_anti_patterns(connectionId, kind="table")` if you're committing a
|
|
41
|
+
schema shape — the brain has flagged patterns to avoid in this
|
|
42
|
+
specific database.
|
|
43
|
+
|
|
44
|
+
## Then narrate what you found before proposing schema
|
|
45
|
+
|
|
46
|
+
Don't jump straight to `CREATE TABLE …`. Tell the user what DeepSQL said:
|
|
47
|
+
|
|
48
|
+
> "DeepSQL says you already have a `customers` table with `email`,
|
|
49
|
+
> `tenant_id`, `created_at`, plus an inferred FK to
|
|
50
|
+
> `accounts.customer_id` at 0.94 confidence. The business rule
|
|
51
|
+
> `always_filter_cancelled` is on `customers.status`. The anti-pattern
|
|
52
|
+
> report flagged 'wide-table' on `customer_profiles` — adding more
|
|
53
|
+
> columns there is discouraged.
|
|
54
|
+
>
|
|
55
|
+
> **I'd extend `customers` with the two new fields you need rather
|
|
56
|
+
> than add a `users` table. Want me to draft the migration?**"
|
|
57
|
+
|
|
58
|
+
That narration is the difference between an agent that ships features fast
|
|
59
|
+
and an agent that earns the team's trust. **Make it a reflex.**
|
|
60
|
+
|
|
61
|
+
If the consult tells you to stop — there's already a table that does what
|
|
62
|
+
the user asked for, or the shape they want is on the anti-pattern list —
|
|
63
|
+
**say so**. Push back politely and propose the better path. The user
|
|
64
|
+
usually doesn't know about either; that's exactly why DeepSQL exists.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Running SQL
|
|
69
|
+
|
|
70
|
+
| You want to… | Use this tool |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Run any SQL (SELECT / EXPLAIN / DML / DDL) | `execute_sql(connectionId, query, ...)` |
|
|
73
|
+
| Get AI-enriched plan analysis for a query | `analyze_query_plan(connectionId, query, useAnalyze=false)` |
|
|
74
|
+
| Actually execute the query AND get the plan (real timings) | `analyze_query_plan(..., useAnalyze=true)` |
|
|
75
|
+
|
|
76
|
+
`EXPLAIN` and `EXPLAIN ANALYZE` are just SQL — type them as the query if
|
|
77
|
+
you want raw plan output. Use `analyze_query_plan` when you want the
|
|
78
|
+
AI-enriched analysis (issues, index recommendations, written summary).
|
|
79
|
+
|
|
80
|
+
### Mutations are role-gated and two-step
|
|
81
|
+
|
|
82
|
+
`execute_sql` enforces the same policy as the SQL Editor:
|
|
83
|
+
|
|
84
|
+
- **Developer + SELECT/WITH/SHOW/EXPLAIN** → runs immediately.
|
|
85
|
+
- **Developer + DML/DDL** → 403 `EDITOR_MUTATION_FORBIDDEN`. Don't retry.
|
|
86
|
+
Tell the user: "Your DeepSQL role doesn't allow DML/DDL on this
|
|
87
|
+
connection; ask the workspace admin to grant write access or to run the
|
|
88
|
+
change."
|
|
89
|
+
- **Admin + DML/DDL (no `confirmMutation`)** → returns
|
|
90
|
+
`requiresConfirmation: true` with a `warnings` array. **Show the
|
|
91
|
+
warnings to the user verbatim. Wait for explicit OK.** Then re-call
|
|
92
|
+
with `confirmMutation: true`. **Do not silently retry with
|
|
93
|
+
`confirmMutation: true` on the user's behalf** — that defeats the
|
|
94
|
+
confirmation step.
|
|
95
|
+
|
|
96
|
+
### Row limits
|
|
97
|
+
|
|
98
|
+
`execute_sql` defaults to 100 rows, max 1000. If you asked for "all
|
|
99
|
+
customers" and got 100, that's the limit kicking in — not the real count.
|
|
100
|
+
Either bump `limit` or `SELECT COUNT(*)` first.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Every call is audited
|
|
105
|
+
|
|
106
|
+
Every tool call you make is logged to the DeepSQL `security_events` table
|
|
107
|
+
with the user's identity, the editor that invoked the MCP server
|
|
108
|
+
(claude-desktop, cursor-mcp, codex-mcp), the connection, the truncated
|
|
109
|
+
statement, and the outcome. Workspace admins can search this. Don't do
|
|
110
|
+
anything through these tools you wouldn't be willing to defend in that
|
|
111
|
+
view.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Full reference
|
|
116
|
+
|
|
117
|
+
The complete runtime guide — every decision-tree branch, every foot-gun,
|
|
118
|
+
all three session playbooks (answer-a-question, mutation, DBA-consult) —
|
|
119
|
+
lives in `node_modules/@deepsql/mcp/CLAUDE.md`. Read it the first time
|
|
120
|
+
you handle a non-trivial database request.
|
|
121
|
+
|
|
122
|
+
Capabilities that aren't MCP-exposed yet (index recommendations, daily
|
|
123
|
+
digest, slow-query streaming optimization) live in the CLI: `deepsql
|
|
124
|
+
indexes`, `deepsql digest`, `deepsql slow-queries optimize`. Point the
|
|
125
|
+
user at them.
|
package/src/api/client.js
CHANGED
|
@@ -19,6 +19,27 @@ class ApiError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Module-level "who is talking to the backend" record. The CLI's entry
|
|
24
|
+
* point calls setClientContext() once at startup, then every request()
|
|
25
|
+
* call below stamps X-DeepSQL-Client-{Type,Agent,Version} headers without
|
|
26
|
+
* each command needing to thread that information through.
|
|
27
|
+
*
|
|
28
|
+
* type "cli" by default for everything that goes through this module
|
|
29
|
+
* agent "terminal", or the value of --caller-agent / DEEPSQL_CALLER_AGENT
|
|
30
|
+
* (set by agents like claude-code when they shell out to `deepsql`)
|
|
31
|
+
* version the npm package version, so backend audit shows which CLI build
|
|
32
|
+
*/
|
|
33
|
+
let currentClient = null;
|
|
34
|
+
|
|
35
|
+
function setClientContext(client) {
|
|
36
|
+
currentClient = client ? { ...client } : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getClientContext() {
|
|
40
|
+
return currentClient;
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
function normalizeBaseUrl(url) {
|
|
23
44
|
if (!url) throw new ApiError("No DeepSQL URL configured. Run `deepsql login --url <url>` first.");
|
|
24
45
|
return url.endsWith("/") ? url : `${url}/`;
|
|
@@ -54,6 +75,11 @@ async function request(baseUrl, pathOrUrl, { method = "GET", json, headers, toke
|
|
|
54
75
|
};
|
|
55
76
|
if (token) requestHeaders.Authorization = `Bearer ${token}`;
|
|
56
77
|
if (json != null) requestHeaders["Content-Type"] = "application/json";
|
|
78
|
+
if (currentClient) {
|
|
79
|
+
if (currentClient.type) requestHeaders["X-DeepSQL-Client-Type"] = currentClient.type;
|
|
80
|
+
if (currentClient.agent) requestHeaders["X-DeepSQL-Client-Agent"] = currentClient.agent;
|
|
81
|
+
if (currentClient.version) requestHeaders["X-DeepSQL-Client-Version"] = currentClient.version;
|
|
82
|
+
}
|
|
57
83
|
|
|
58
84
|
const controller = new AbortController();
|
|
59
85
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -114,4 +140,12 @@ function parseCookieValue(setCookies, name) {
|
|
|
114
140
|
return null;
|
|
115
141
|
}
|
|
116
142
|
|
|
117
|
-
module.exports = {
|
|
143
|
+
module.exports = {
|
|
144
|
+
ApiError,
|
|
145
|
+
request,
|
|
146
|
+
resolveUrl,
|
|
147
|
+
normalizeBaseUrl,
|
|
148
|
+
parseCookieValue,
|
|
149
|
+
setClientContext,
|
|
150
|
+
getClientContext,
|
|
151
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -21,7 +21,8 @@ const COMMANDS = {
|
|
|
21
21
|
mcp: () => require("./commands/mcp"),
|
|
22
22
|
connections: () => require("./commands/connections"),
|
|
23
23
|
query: () => require("./commands/query"),
|
|
24
|
-
|
|
24
|
+
analyze: () => require("./commands/analyze"),
|
|
25
|
+
explain: () => require("./commands/explain"), // deprecated alias for `analyze`, removed in 0.14.0
|
|
25
26
|
schema: () => require("./commands/schema"),
|
|
26
27
|
// Brain tools — give a coding agent the same retrieval context the chat
|
|
27
28
|
// pipeline uses, then let the agent generate SQL/answers itself.
|
|
@@ -30,6 +31,7 @@ const COMMANDS = {
|
|
|
30
31
|
relationships: () => require("./commands/relationships"),
|
|
31
32
|
"anti-patterns": () => require("./commands/anti-patterns"),
|
|
32
33
|
digest: () => require("./commands/digest"),
|
|
34
|
+
indexes: () => require("./commands/indexes"),
|
|
33
35
|
users: () => require("./commands/users"),
|
|
34
36
|
access: () => require("./commands/access"),
|
|
35
37
|
permissions: () => require("./commands/permissions"),
|
|
@@ -47,16 +49,17 @@ const COMMAND_LIST = [
|
|
|
47
49
|
["logout", false, "Revoke and forget the saved token"],
|
|
48
50
|
["whoami", false, "Show the user behind the saved token"],
|
|
49
51
|
["config", true, "Manage saved CLI profiles"],
|
|
50
|
-
["mcp",
|
|
52
|
+
["mcp", true, "Run the MCP server or install it into an editor config"],
|
|
51
53
|
["connections", true, "Manage database connections"],
|
|
52
|
-
["query", false, "
|
|
53
|
-
["
|
|
54
|
+
["query", false, "Execute a SQL statement (admin: DDL/DML with --write)"],
|
|
55
|
+
["analyze", false, "AI-enriched query plan analysis (use --analyze for EXPLAIN ANALYZE)"],
|
|
54
56
|
["schema", false, "Dump connection schema or DB objects as JSON"],
|
|
55
57
|
["digest", true, "Show DeepSQL daily digests"],
|
|
56
58
|
["brain-context", false, "Retrieve embedding-ranked context for a question"],
|
|
57
59
|
["business-rules", false, "List active business rules and SQL guardrails"],
|
|
58
60
|
["relationships", false, "List inferred and validated FK relationships"],
|
|
59
61
|
["anti-patterns", false, "List schema- or query-level anti-patterns"],
|
|
62
|
+
["indexes", true, "Index suggestions, usage, and health (read-only)"],
|
|
60
63
|
["users", true, "Manage workspace users (admin)"],
|
|
61
64
|
["access", true, "Manage per-connection access grants (admin)"],
|
|
62
65
|
["permissions", true, "Manage role-based permission overrides (admin)"],
|
|
@@ -65,12 +68,13 @@ const COMMAND_LIST = [
|
|
|
65
68
|
];
|
|
66
69
|
|
|
67
70
|
const GLOBAL_OPTIONS = [
|
|
68
|
-
["--url <url>",
|
|
69
|
-
["--token <tok>",
|
|
70
|
-
["--connection <name>",
|
|
71
|
-
["--
|
|
72
|
-
["-
|
|
73
|
-
["-
|
|
71
|
+
["--url <url>", "Override the DeepSQL base URL"],
|
|
72
|
+
["--token <tok>", "Override the auth token (also: DEEPSQL_AUTH_TOKEN)"],
|
|
73
|
+
["--connection <name>", "Override the active connection (also: DEEPSQL_CONNECTION)"],
|
|
74
|
+
["--caller-agent <id>", "Identify the calling agent in audit logs (also: DEEPSQL_CALLER_AGENT)"],
|
|
75
|
+
["--no-color", "Disable ANSI colors"],
|
|
76
|
+
["-h, --help", "Display help for command"],
|
|
77
|
+
["-v, --version", "Show version"],
|
|
74
78
|
];
|
|
75
79
|
|
|
76
80
|
// ─── Per-command help blocks ────────────────────────────────────────────────
|
|
@@ -110,9 +114,23 @@ const COMMAND_HELP = {
|
|
|
110
114
|
},
|
|
111
115
|
|
|
112
116
|
mcp: {
|
|
113
|
-
description: "Run the stdio MCP server
|
|
114
|
-
usage: "deepsql mcp [--url <url>]",
|
|
115
|
-
|
|
117
|
+
description: "Run the stdio MCP server, or install DeepSQL into an editor's MCP config (+ the DBA-consult skill).",
|
|
118
|
+
usage: "deepsql mcp [--url <url>]\n deepsql mcp config (--install | --print) --for <editor> [--force] [--no-skill] [--path <p>]",
|
|
119
|
+
subcommands: [
|
|
120
|
+
["(no args)", "Run the stdio MCP server with the saved token"],
|
|
121
|
+
["config --install --for <editor>", "Write a DeepSQL entry into the editor's MCP config AND install the DBA-consult skill (backs up on overwrite)"],
|
|
122
|
+
["config --print --for <editor>", "Print the snippets (config + skill) without touching disk"],
|
|
123
|
+
],
|
|
124
|
+
options: [
|
|
125
|
+
["--for <editor>", "claude-code, claude-desktop, cursor, or codex"],
|
|
126
|
+
["--install", "Write the entry to the editor's default config path AND install the skill"],
|
|
127
|
+
["--print", "Emit the snippets only (no filesystem writes)"],
|
|
128
|
+
["--force", "Overwrite an existing DeepSQL entry/skill"],
|
|
129
|
+
["--no-skill", "Skip the DBA-consult skill install (MCP server config only)"],
|
|
130
|
+
["--path <p>", "Override the default MCP config path (advanced; doesn't affect the skill path)"],
|
|
131
|
+
["--url <url>", "Profile to bind the spawned MCP server to"],
|
|
132
|
+
],
|
|
133
|
+
notes: "The installed entry runs `deepsql mcp`, which uses the saved profile — no token is embedded in the editor config. The skill teaches the agent to consult DeepSQL BEFORE generating DDL or non-trivial SQL.",
|
|
116
134
|
},
|
|
117
135
|
|
|
118
136
|
config: {
|
|
@@ -156,25 +174,30 @@ const COMMAND_HELP = {
|
|
|
156
174
|
},
|
|
157
175
|
|
|
158
176
|
query: {
|
|
159
|
-
description: "
|
|
177
|
+
description: "Execute a SQL statement against a connection. Same policy gate as the web SQL Editor: developers can run SELECT/WITH/SHOW/EXPLAIN; admins can additionally run DML/DDL with a two-step confirm.",
|
|
160
178
|
usage: 'deepsql query "<sql>" --connection <name> [options]',
|
|
161
179
|
options: [
|
|
162
180
|
["--connection <name>", "Connection to run against"],
|
|
163
181
|
["--limit <n>", "Row limit (1–1000, default 100)"],
|
|
164
182
|
["--timeout-seconds <n>", "Statement timeout in seconds (1–60)"],
|
|
165
183
|
["--file <path>", "Read SQL from a file instead of argv"],
|
|
184
|
+
["--write", "Confirm a mutation upfront (skips interactive prompt; scripts/CI)"],
|
|
166
185
|
["--json", "Raw JSON output"],
|
|
167
186
|
],
|
|
187
|
+
notes: "EXPLAIN and EXPLAIN ANALYZE are valid SQL — type them directly. For the AI-enriched plan analysis, use `deepsql analyze`.",
|
|
168
188
|
},
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
description: "
|
|
172
|
-
usage: 'deepsql
|
|
190
|
+
analyze: {
|
|
191
|
+
description: "AI-enriched query plan analysis. Returns the parsed plan tree, performance issues, index recommendations, and a written summary that takes the connection's schema and business rules into account.",
|
|
192
|
+
usage: 'deepsql analyze "<sql>" --connection <name> [options]',
|
|
173
193
|
options: [
|
|
174
|
-
["--connection <name>",
|
|
175
|
-
["--
|
|
176
|
-
["--
|
|
194
|
+
["--connection <name>", "Connection to run against"],
|
|
195
|
+
["--analyze", "Use EXPLAIN ANALYZE (actually executes the query)"],
|
|
196
|
+
["--write", "Confirm an ANALYZE-of-mutation upfront (skips interactive prompt)"],
|
|
197
|
+
["--file <path>", "Read SQL from a file instead of argv"],
|
|
198
|
+
["--json", "Raw JSON output"],
|
|
177
199
|
],
|
|
200
|
+
notes: "Pass the underlying SQL, not `EXPLAIN <sql>` — the server wraps it. With --analyze on a mutation, the same admin role + WHERE + confirmation gates as `query` apply.",
|
|
178
201
|
},
|
|
179
202
|
|
|
180
203
|
schema: {
|
|
@@ -241,6 +264,26 @@ const COMMAND_HELP = {
|
|
|
241
264
|
],
|
|
242
265
|
},
|
|
243
266
|
|
|
267
|
+
indexes: {
|
|
268
|
+
description: "Index suggestions, usage, and health — read-only in V1.",
|
|
269
|
+
usage: "deepsql indexes <subcommand> [options]",
|
|
270
|
+
subcommands: [
|
|
271
|
+
["list [--all] [--status <s>]", "Index recommendations (defaults to PENDING)"],
|
|
272
|
+
["missing", "Missing-index suggestions from the advisor"],
|
|
273
|
+
["health", "Comprehensive index health report"],
|
|
274
|
+
["unused", "Indexes the engine has not used"],
|
|
275
|
+
["duplicates", "Duplicate or redundant indexes"],
|
|
276
|
+
["usage <table>", "Per-table index usage statistics"],
|
|
277
|
+
],
|
|
278
|
+
options: [
|
|
279
|
+
["--connection <name>", "Connection to inspect"],
|
|
280
|
+
["--all", "Include APPLIED and DISMISSED in `list`"],
|
|
281
|
+
["--status PENDING|APPLIED|DISMISSED", "Filter `list` by status"],
|
|
282
|
+
["--json", "Raw JSON output"],
|
|
283
|
+
],
|
|
284
|
+
notes: "V1 is read-only. Apply, dismiss, delete, and generate are not exposed by the CLI yet.",
|
|
285
|
+
},
|
|
286
|
+
|
|
244
287
|
users: {
|
|
245
288
|
description: "Manage workspace users (admin).",
|
|
246
289
|
usage: "deepsql users <subcommand> [options]",
|
|
@@ -500,6 +543,20 @@ function buildOpts(parsed) {
|
|
|
500
543
|
queryText: f.queryText || null,
|
|
501
544
|
sampleQuery: f.sampleQuery || null,
|
|
502
545
|
historyId: f.historyId || null,
|
|
546
|
+
// Indexes
|
|
547
|
+
all: !!f.all,
|
|
548
|
+
status: f.status || null,
|
|
549
|
+
// mcp config installer
|
|
550
|
+
install: !!f.install,
|
|
551
|
+
print: !!f.print,
|
|
552
|
+
for: f.for || null,
|
|
553
|
+
path: f.path || null,
|
|
554
|
+
noSkill: !!f.noSkill,
|
|
555
|
+
// SQL execution
|
|
556
|
+
write: !!f.write,
|
|
557
|
+
analyze: !!f.analyze,
|
|
558
|
+
// Origin tagging for audit
|
|
559
|
+
callerAgent: f.callerAgent || null,
|
|
503
560
|
// Setup wizard
|
|
504
561
|
force: !!f.force,
|
|
505
562
|
skipEmail: !!f.skipEmail,
|
|
@@ -557,6 +614,19 @@ async function main(rawArgv = process.argv.slice(2), io = {}) {
|
|
|
557
614
|
const parsed = parseArgs(restArgs);
|
|
558
615
|
const opts = buildOpts(parsed);
|
|
559
616
|
|
|
617
|
+
// Stamp origin headers on every backend request the command will make.
|
|
618
|
+
// `clientType` is fixed for CLI traffic; `clientAgent` defaults to
|
|
619
|
+
// "terminal" but agents that shell out can override via --caller-agent or
|
|
620
|
+
// the DEEPSQL_CALLER_AGENT env var, so audit rows distinguish "human in a
|
|
621
|
+
// terminal" from "Claude Code invoked deepsql query."
|
|
622
|
+
const { setClientContext } = require("./api/client");
|
|
623
|
+
const pkg = require("../package.json");
|
|
624
|
+
setClientContext({
|
|
625
|
+
type: "cli",
|
|
626
|
+
agent: opts.callerAgent || process.env.DEEPSQL_CALLER_AGENT || "terminal",
|
|
627
|
+
version: pkg.version,
|
|
628
|
+
});
|
|
629
|
+
|
|
560
630
|
try {
|
|
561
631
|
const mod = loader();
|
|
562
632
|
await mod.run(opts, { stdout, stderr });
|
package/src/cli.test.js
CHANGED
|
@@ -73,8 +73,8 @@ test("every command in the catalog has a COMMAND_HELP entry", () => {
|
|
|
73
73
|
const { main: _main } = require("./cli");
|
|
74
74
|
void _main;
|
|
75
75
|
const expected = [
|
|
76
|
-
"login","logout","whoami","config","mcp","connections","query","
|
|
77
|
-
"digest","brain-context","business-rules","relationships","anti-patterns",
|
|
76
|
+
"login","logout","whoami","config","mcp","connections","query","analyze","schema",
|
|
77
|
+
"digest","brain-context","business-rules","relationships","anti-patterns","indexes",
|
|
78
78
|
"users","access","permissions","slow-queries","setup",
|
|
79
79
|
];
|
|
80
80
|
for (const name of expected) {
|