@deepsql/mcp 0.10.1 → 0.10.2
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/package.json +1 -1
- package/src/cli.js +393 -127
- package/src/cli.test.js +81 -1
- package/CLAUDE.md +0 -330
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - value flags: --url <url>, --token <t>, --connection <name>, --limit 50
|
|
11
11
|
* - subcommands: deepsql connections list, deepsql config show
|
|
12
12
|
* - positional: deepsql brain-context "which tables hold orders?"
|
|
13
|
+
* - per-command -h/--help: prints that command's usage block.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
const COMMANDS = {
|
|
@@ -36,128 +37,379 @@ const COMMANDS = {
|
|
|
36
37
|
setup: () => require("./commands/setup"),
|
|
37
38
|
};
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
40
|
+
// ─── Top-level command catalog (rendered into the root help) ────────────────
|
|
41
|
+
//
|
|
42
|
+
// `sub: true` marks commands that have nested subcommands. They get a `*`
|
|
43
|
+
// suffix in the listing and reveal their full usage via `<command> --help`.
|
|
44
|
+
|
|
45
|
+
const COMMAND_LIST = [
|
|
46
|
+
["login", false, "Authorize this CLI with a DeepSQL instance"],
|
|
47
|
+
["logout", false, "Revoke and forget the saved token"],
|
|
48
|
+
["whoami", false, "Show the user behind the saved token"],
|
|
49
|
+
["config", true, "Manage saved CLI profiles"],
|
|
50
|
+
["mcp", false, "Run the stdio MCP server using the saved token"],
|
|
51
|
+
["connections", true, "Manage database connections"],
|
|
52
|
+
["query", false, "Run a read-only SQL statement"],
|
|
53
|
+
["explain", false, "Get an EXPLAIN plan (no ANALYZE)"],
|
|
54
|
+
["schema", false, "Dump connection schema or DB objects as JSON"],
|
|
55
|
+
["digest", true, "Show DeepSQL daily digests"],
|
|
56
|
+
["brain-context", false, "Retrieve embedding-ranked context for a question"],
|
|
57
|
+
["business-rules", false, "List active business rules and SQL guardrails"],
|
|
58
|
+
["relationships", false, "List inferred and validated FK relationships"],
|
|
59
|
+
["anti-patterns", false, "List schema- or query-level anti-patterns"],
|
|
60
|
+
["users", true, "Manage workspace users (admin)"],
|
|
61
|
+
["access", true, "Manage per-connection access grants (admin)"],
|
|
62
|
+
["permissions", true, "Manage role-based permission overrides (admin)"],
|
|
63
|
+
["slow-queries", true, "Read, trigger, and stream slow-query analyses (admin)"],
|
|
64
|
+
["setup", false, "Post-install wizard for SMTP/email and Slack"],
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const GLOBAL_OPTIONS = [
|
|
68
|
+
["--url <url>", "Override the DeepSQL base URL"],
|
|
69
|
+
["--token <tok>", "Override the auth token (also: DEEPSQL_AUTH_TOKEN)"],
|
|
70
|
+
["--connection <name>", "Override the active connection (also: DEEPSQL_CONNECTION)"],
|
|
71
|
+
["--no-color", "Disable ANSI colors"],
|
|
72
|
+
["-h, --help", "Display help for command"],
|
|
73
|
+
["-v, --version", "Show version"],
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// ─── Per-command help blocks ────────────────────────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// Each entry is { description, usage, subcommands?, options?, notes? }.
|
|
79
|
+
// Subcommands and options render as two-column tables (name | description).
|
|
80
|
+
// Centralized here so individual command modules don't each carry a HELP
|
|
81
|
+
// constant; the dispatcher renders them on `<command> --help`.
|
|
82
|
+
|
|
83
|
+
const COMMAND_HELP = {
|
|
84
|
+
login: {
|
|
85
|
+
description: "Authorize this CLI with a DeepSQL instance.",
|
|
86
|
+
usage: "deepsql login [options]",
|
|
87
|
+
options: [
|
|
88
|
+
["--url <url>", "DeepSQL base URL to authorize against"],
|
|
89
|
+
["--browser", "Use browser callback (PKCE) — default on desktops"],
|
|
90
|
+
["--device", "Use device-code flow — default on headless boxes"],
|
|
91
|
+
["--password", "Direct email+password login (fresh self-host VMs)"],
|
|
92
|
+
["--no-browser", "Force device-code even on a desktop"],
|
|
93
|
+
["--email <email>", "Email for --password flow"],
|
|
94
|
+
["--password-stdin", "Read password from stdin (non-interactive)"],
|
|
95
|
+
["--label <name>", "Label the saved token for `whoami`"],
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
logout: {
|
|
100
|
+
description: "Revoke and forget the saved token.",
|
|
101
|
+
usage: "deepsql logout [--url <url>]",
|
|
102
|
+
options: [
|
|
103
|
+
["--url <url>", "Profile to log out of (default: current default profile)"],
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
whoami: {
|
|
108
|
+
description: "Show the user behind the saved token.",
|
|
109
|
+
usage: "deepsql whoami [--url <url>]",
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
mcp: {
|
|
113
|
+
description: "Run the stdio MCP server using the saved token.",
|
|
114
|
+
usage: "deepsql mcp [--url <url>]",
|
|
115
|
+
notes: "Used by editor MCP configs (Cursor, Claude Desktop) so the config never has to embed a raw token.",
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
config: {
|
|
119
|
+
description: "Manage saved CLI profiles (one per DeepSQL URL).",
|
|
120
|
+
usage: "deepsql config <subcommand> [options]",
|
|
121
|
+
subcommands: [
|
|
122
|
+
["show", "List saved profiles (default)"],
|
|
123
|
+
["set-default <url>", "Set the default profile"],
|
|
124
|
+
["path", "Print the auth file path"],
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
connections: {
|
|
129
|
+
description: "Manage database connections — list, pin, full CRUD.",
|
|
130
|
+
usage: "deepsql connections <subcommand> [options]",
|
|
131
|
+
subcommands: [
|
|
132
|
+
["list [--json]", "List database connections (active default marked with *)"],
|
|
133
|
+
["use <name>", "Pin <name> as the active default for this profile"],
|
|
134
|
+
["current", "Print the active default (exit 1 if none)"],
|
|
135
|
+
["unset", "Clear the active default for this profile"],
|
|
136
|
+
["schema [--json]", "Print the JSON Schema for the connection config"],
|
|
137
|
+
["add [options]", "Create a connection (interactive or from JSON)"],
|
|
138
|
+
["update <name> --from-file <p>", "PATCH-style update; omitted secrets preserved"],
|
|
139
|
+
["remove <name> [--yes]", "Delete a connection (DELETE /connections)"],
|
|
140
|
+
["test [<name> | --from-file <p>]", "Validate a connection without saving"],
|
|
141
|
+
["show <name> [--json]", "Show a connection's config (secrets masked)"],
|
|
142
|
+
["init <name> [--force] [--wait]", "Trigger brain re-initialization"],
|
|
143
|
+
],
|
|
144
|
+
options: [
|
|
145
|
+
["--from-file <p>", "Read connection JSON from file"],
|
|
146
|
+
["--from-stdin", "Read connection JSON from stdin"],
|
|
147
|
+
["--upsert", "PUT instead of POST on add if name collision"],
|
|
148
|
+
["--no-test", "Skip pre-save POST /connections/test"],
|
|
149
|
+
["--wait", "Poll brain init to COMPLETED/FAILED"],
|
|
150
|
+
["--delete-after", "rm the --from-file path on success"],
|
|
151
|
+
["--cloud", "Prompt for cloud/instance metadata"],
|
|
152
|
+
["--allow-plaintext-secrets", "Allow plaintext secrets in JSON input"],
|
|
153
|
+
["--json", "Print JSON output where supported"],
|
|
154
|
+
["--yes", "Assume yes for confirmations"],
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
query: {
|
|
159
|
+
description: "Run a read-only SQL statement (parser-enforced, ACL-checked).",
|
|
160
|
+
usage: 'deepsql query "<sql>" --connection <name> [options]',
|
|
161
|
+
options: [
|
|
162
|
+
["--connection <name>", "Connection to run against"],
|
|
163
|
+
["--limit <n>", "Row limit (1–1000, default 100)"],
|
|
164
|
+
["--timeout-seconds <n>", "Statement timeout in seconds (1–60)"],
|
|
165
|
+
["--file <path>", "Read SQL from a file instead of argv"],
|
|
166
|
+
["--json", "Raw JSON output"],
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
explain: {
|
|
171
|
+
description: "Get an EXPLAIN plan (no ANALYZE — read-only enforced).",
|
|
172
|
+
usage: 'deepsql explain "<sql>" --connection <name> [options]',
|
|
173
|
+
options: [
|
|
174
|
+
["--connection <name>", "Connection to run against"],
|
|
175
|
+
["--file <path>", "Read SQL from a file instead of argv"],
|
|
176
|
+
["--json", "Raw JSON output"],
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
schema: {
|
|
181
|
+
description: "Dump connection schema or database objects as JSON.",
|
|
182
|
+
usage: "deepsql schema [tables|objects] --connection <name>",
|
|
183
|
+
subcommands: [
|
|
184
|
+
["tables", "Tables, columns, FKs (default)"],
|
|
185
|
+
["objects", "Database objects (views, procedures, etc.)"],
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
digest: {
|
|
190
|
+
description: "Surface the daily DeepSQL digest from the terminal.",
|
|
191
|
+
usage: "deepsql digest [N|<subcommand>] [options]",
|
|
192
|
+
subcommands: [
|
|
193
|
+
["(no args)", "Show the most recent digest, full body"],
|
|
194
|
+
["<N>", "List the last N digests, compact"],
|
|
195
|
+
["list [--count N]", "Explicit list form (same as digest <N>)"],
|
|
196
|
+
["show <id>", "Show one digest by id"],
|
|
197
|
+
],
|
|
198
|
+
options: [
|
|
199
|
+
["--connection <name>", "Filter to one connection"],
|
|
200
|
+
["--json", "Raw JSON output"],
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
"brain-context": {
|
|
205
|
+
description: "Retrieve embedding-ranked tables/columns/FKs, training docs, and business rules for a question.",
|
|
206
|
+
usage: 'deepsql brain-context "<question>" --connection <name> [options]',
|
|
207
|
+
options: [
|
|
208
|
+
["--connection <name>", "Connection to retrieve from"],
|
|
209
|
+
["--top-k <n>", "Return ranked diagnostic snippets (else: rich training payload)"],
|
|
210
|
+
["--json", "Raw JSON output"],
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
"business-rules": {
|
|
215
|
+
description: "List active business rules and SQL guardrails for a connection.",
|
|
216
|
+
usage: "deepsql business-rules --connection <name> [options]",
|
|
217
|
+
options: [
|
|
218
|
+
["--connection <name>", "Connection to inspect"],
|
|
219
|
+
['--question "..."', "Scope rules to a specific question"],
|
|
220
|
+
["--json", "Raw JSON output"],
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
relationships: {
|
|
225
|
+
description: "Inferred and validated foreign-key relationships with confidence scores.",
|
|
226
|
+
usage: "deepsql relationships --connection <name> [--json]",
|
|
227
|
+
options: [
|
|
228
|
+
["--connection <name>", "Connection to inspect"],
|
|
229
|
+
["--json", "Raw JSON output"],
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
"anti-patterns": {
|
|
234
|
+
description: "Schema-level (table) or query-level anti-patterns detected by the brain.",
|
|
235
|
+
usage: "deepsql anti-patterns --connection <name> [options]",
|
|
236
|
+
options: [
|
|
237
|
+
["--connection <name>", "Connection to inspect"],
|
|
238
|
+
["--kind table|query", "Anti-pattern flavor (default: table)"],
|
|
239
|
+
["--limit <n>", "Limit results (query mode)"],
|
|
240
|
+
["--json", "Raw JSON output"],
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
users: {
|
|
245
|
+
description: "Manage workspace users (admin).",
|
|
246
|
+
usage: "deepsql users <subcommand> [options]",
|
|
247
|
+
subcommands: [
|
|
248
|
+
["list [--json]", "List all users"],
|
|
249
|
+
["get <ref>", "Show one user by email or id"],
|
|
250
|
+
["add [<email>] [options]", "Invite or create a user"],
|
|
251
|
+
["set-role <ref> <role>", "Change a user's role"],
|
|
252
|
+
["lock <ref>", "Lock a user account"],
|
|
253
|
+
["unlock <ref>", "Unlock a user account"],
|
|
254
|
+
["disable <ref>", "Disable a user account"],
|
|
255
|
+
["resend-invite <ref>", "Resend an invitation email"],
|
|
256
|
+
["reset-password <ref>", "Reset a user's password"],
|
|
257
|
+
["delete <ref> [--yes]", "Permanently delete a user"],
|
|
258
|
+
],
|
|
259
|
+
options: [
|
|
260
|
+
["--role <r>", "Role for `add` / `set-role`"],
|
|
261
|
+
["--name <n>", "Display name for `add`"],
|
|
262
|
+
["--password-stdin", "Read password from stdin"],
|
|
263
|
+
["--json", "Raw JSON output (list)"],
|
|
264
|
+
["--yes", "Assume yes for confirmations"],
|
|
265
|
+
],
|
|
266
|
+
notes: "All endpoints under /admin/users/** require ROLE_ADMIN.",
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
access: {
|
|
270
|
+
description: "Per-connection access grants and chat policies (admin).",
|
|
271
|
+
usage: "deepsql access <subcommand> [options]",
|
|
272
|
+
subcommands: [
|
|
273
|
+
["list --user <ref>", "Connections this user can see"],
|
|
274
|
+
["list --connection <name>", "Users who can see this connection"],
|
|
275
|
+
["grant --user <ref> --connection <name> --level <lvl>", "Grant access (read|write|admin)"],
|
|
276
|
+
["revoke --user <ref> --connection <name>", "Revoke access"],
|
|
277
|
+
["policy <user> <connection>", "Edit chat policy in $EDITOR"],
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
permissions: {
|
|
282
|
+
description: "Global role-based permission overrides (admin).",
|
|
283
|
+
usage: "deepsql permissions <subcommand> [options]",
|
|
284
|
+
subcommands: [
|
|
285
|
+
["list [--role <r>] [--json]", "List permissions, optionally filtered by role"],
|
|
286
|
+
["override --role <r> --permission <p> --grant|--revoke [--reason \"...\"]", "Override a role's permission"],
|
|
287
|
+
["reset --role <r> --permission <p>", "Remove an override (revert to default)"],
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
"slow-queries": {
|
|
292
|
+
description: "Read, trigger, and stream slow-query analyses (admin).",
|
|
293
|
+
usage: "deepsql slow-queries <subcommand> [options]",
|
|
294
|
+
subcommands: [
|
|
295
|
+
["latest --connection <name>", "Most recent analysis for the connection"],
|
|
296
|
+
["history --connection <name> [N]", "Last N analyses"],
|
|
297
|
+
["analyze --connection <name> [options]", "Trigger a new analysis"],
|
|
298
|
+
["optimize --connection <name> --query-id <id>", "Stream AI optimization steps live (SSE)"],
|
|
299
|
+
["delete (--history-id <id> | --connection <name>) [--yes]", "Clean up history"],
|
|
300
|
+
],
|
|
301
|
+
options: [
|
|
302
|
+
["--time-range LAST_24_HOURS|LAST_HOUR", "Window for analyze"],
|
|
303
|
+
["--threshold-ms <n>", "Min duration to consider"],
|
|
304
|
+
["--limit <n>", "Cap results"],
|
|
305
|
+
["--json", "Raw JSON output"],
|
|
306
|
+
],
|
|
307
|
+
notes: "`optimize` follows the SSE protocol from /slow-queries/optimize/stream — step events go to stderr, the final result to stdout. Honors SIGINT.",
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
setup: {
|
|
311
|
+
description: "Post-install wizard: SMTP/email, Slack (digests + bot), then mark setup complete.",
|
|
312
|
+
usage: "deepsql setup [options]",
|
|
313
|
+
options: [
|
|
314
|
+
["--skip-email", "Skip the SMTP/email step"],
|
|
315
|
+
["--skip-slack", "Skip the Slack step"],
|
|
316
|
+
["--skip-complete", "Don't mark setup complete at the end"],
|
|
317
|
+
],
|
|
318
|
+
notes: "Org and LLM config are set at install time and are NOT touched by this wizard.",
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// ─── Color helpers ──────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
function colorEnabled(stream, rawArgv) {
|
|
325
|
+
if (process.env.NO_COLOR) return false;
|
|
326
|
+
if (rawArgv && (rawArgv.includes("--no-color") || rawArgv.includes("--no-colors"))) return false;
|
|
327
|
+
if (process.env.FORCE_COLOR === "1") return true;
|
|
328
|
+
return !!(stream && stream.isTTY);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function paint(useColor) {
|
|
332
|
+
if (!useColor) {
|
|
333
|
+
const id = (s) => s;
|
|
334
|
+
return { brand: id, accent: id, bold: id, dim: id, reset: "" };
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
brand: (s) => `\x1b[1;38;5;75m${s}\x1b[0m`, // bold sky-blue
|
|
338
|
+
accent: (s) => `\x1b[38;5;75m${s}\x1b[0m`, // sky-blue
|
|
339
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
340
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
341
|
+
reset: "\x1b[0m",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function pad(str, width) {
|
|
346
|
+
return str + " ".repeat(Math.max(0, width - str.length));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function renderTable(rows, c, { nameMin = 16, gap = 2 } = {}) {
|
|
350
|
+
const nameWidth = Math.max(nameMin, ...rows.map((r) => r[0].length));
|
|
351
|
+
return rows
|
|
352
|
+
.map(([name, desc]) => ` ${c.accent(pad(name, nameWidth))}${" ".repeat(gap)}${desc}`)
|
|
353
|
+
.join("\n");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── Renderers ──────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function renderRootHelp(useColor) {
|
|
359
|
+
const c = paint(useColor);
|
|
360
|
+
const pkg = require("../package.json");
|
|
361
|
+
|
|
362
|
+
const header =
|
|
363
|
+
`${c.brand("🐬 DeepSQL")} ${c.accent(pkg.version)} ` +
|
|
364
|
+
c.dim("— ask the database what's wrong. It already knows.");
|
|
365
|
+
|
|
366
|
+
const usage = `${c.bold("Usage:")} deepsql [options] [command]`;
|
|
367
|
+
|
|
368
|
+
const optionsBlock =
|
|
369
|
+
`${c.bold("Options:")}\n` + renderTable(GLOBAL_OPTIONS, c, { nameMin: 22 });
|
|
370
|
+
|
|
371
|
+
const commandRows = COMMAND_LIST.map(([name, hasSub, desc]) => [
|
|
372
|
+
hasSub ? `${name} *` : name,
|
|
373
|
+
desc,
|
|
374
|
+
]);
|
|
375
|
+
const commandsBlock =
|
|
376
|
+
`${c.bold("Commands:")}\n` +
|
|
377
|
+
` ${c.dim("Hint: commands suffixed with * have subcommands. Run <command> --help for details.")}\n` +
|
|
378
|
+
renderTable(commandRows, c, { nameMin: 16 });
|
|
379
|
+
|
|
380
|
+
return [header, "", usage, "", optionsBlock, "", commandsBlock, ""].join("\n");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderCommandHelp(name, useColor) {
|
|
384
|
+
const c = paint(useColor);
|
|
385
|
+
const help = COMMAND_HELP[name];
|
|
386
|
+
if (!help) {
|
|
387
|
+
return `${c.bold("deepsql " + name)}\n No detailed help available — run \`deepsql --help\` for the command list.\n`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const lines = [];
|
|
391
|
+
lines.push(`${c.brand("deepsql " + name)} ${c.dim("— " + help.description)}`);
|
|
392
|
+
lines.push("");
|
|
393
|
+
lines.push(`${c.bold("Usage:")} ${help.usage}`);
|
|
394
|
+
if (help.subcommands && help.subcommands.length > 0) {
|
|
395
|
+
lines.push("");
|
|
396
|
+
lines.push(`${c.bold("Subcommands:")}`);
|
|
397
|
+
lines.push(renderTable(help.subcommands, c, { nameMin: 24 }));
|
|
398
|
+
}
|
|
399
|
+
if (help.options && help.options.length > 0) {
|
|
400
|
+
lines.push("");
|
|
401
|
+
lines.push(`${c.bold("Options:")}`);
|
|
402
|
+
lines.push(renderTable(help.options, c, { nameMin: 24 }));
|
|
403
|
+
}
|
|
404
|
+
if (help.notes) {
|
|
405
|
+
lines.push("");
|
|
406
|
+
lines.push(c.dim(help.notes));
|
|
407
|
+
}
|
|
408
|
+
lines.push("");
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ─── Argv parser ────────────────────────────────────────────────────────────
|
|
161
413
|
|
|
162
414
|
function parseArgs(argv) {
|
|
163
415
|
const result = { positional: [], flags: {} };
|
|
@@ -264,15 +516,22 @@ function buildOpts(parsed) {
|
|
|
264
516
|
deleteAfter: !!f.deleteAfter,
|
|
265
517
|
cloud: !!f.cloud,
|
|
266
518
|
allowPlaintextSecrets: !!f.allowPlaintextSecrets,
|
|
519
|
+
// Display
|
|
520
|
+
noColor: !!f.noColor,
|
|
267
521
|
};
|
|
268
522
|
}
|
|
269
523
|
|
|
524
|
+
function isHelpFlag(arg) {
|
|
525
|
+
return arg === "--help" || arg === "-h";
|
|
526
|
+
}
|
|
527
|
+
|
|
270
528
|
async function main(rawArgv = process.argv.slice(2), io = {}) {
|
|
271
529
|
const stderr = io.stderr || process.stderr;
|
|
272
530
|
const stdout = io.stdout || process.stdout;
|
|
531
|
+
const useColor = colorEnabled(stdout, rawArgv);
|
|
273
532
|
|
|
274
|
-
if (rawArgv.length === 0 || rawArgv[0]
|
|
275
|
-
stdout.write(`${
|
|
533
|
+
if (rawArgv.length === 0 || isHelpFlag(rawArgv[0])) {
|
|
534
|
+
stdout.write(`${renderRootHelp(useColor)}\n`);
|
|
276
535
|
return 0;
|
|
277
536
|
}
|
|
278
537
|
if (rawArgv[0] === "--version" || rawArgv[0] === "-v") {
|
|
@@ -284,11 +543,18 @@ async function main(rawArgv = process.argv.slice(2), io = {}) {
|
|
|
284
543
|
const command = rawArgv[0];
|
|
285
544
|
const loader = COMMANDS[command];
|
|
286
545
|
if (!loader) {
|
|
287
|
-
stderr.write(`Unknown command: ${command}\n\n${
|
|
546
|
+
stderr.write(`Unknown command: ${command}\n\n${renderRootHelp(colorEnabled(stderr, rawArgv))}\n`);
|
|
288
547
|
return 2;
|
|
289
548
|
}
|
|
290
549
|
|
|
291
|
-
|
|
550
|
+
// Per-command help: `deepsql <command> --help` / `-h` (anywhere in args).
|
|
551
|
+
const restArgs = rawArgv.slice(1);
|
|
552
|
+
if (restArgs.some(isHelpFlag)) {
|
|
553
|
+
stdout.write(`${renderCommandHelp(command, useColor)}\n`);
|
|
554
|
+
return 0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const parsed = parseArgs(restArgs);
|
|
292
558
|
const opts = buildOpts(parsed);
|
|
293
559
|
|
|
294
560
|
try {
|
|
@@ -304,4 +570,4 @@ async function main(rawArgv = process.argv.slice(2), io = {}) {
|
|
|
304
570
|
}
|
|
305
571
|
}
|
|
306
572
|
|
|
307
|
-
module.exports = { main, parseArgs, buildOpts };
|
|
573
|
+
module.exports = { main, parseArgs, buildOpts, renderRootHelp, renderCommandHelp, COMMAND_HELP };
|
package/src/cli.test.js
CHANGED
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
const test = require("node:test");
|
|
4
4
|
const assert = require("node:assert/strict");
|
|
5
5
|
|
|
6
|
-
const { parseArgs, buildOpts } = require("./cli");
|
|
6
|
+
const { parseArgs, buildOpts, main, renderRootHelp, renderCommandHelp, COMMAND_HELP } = require("./cli");
|
|
7
|
+
|
|
8
|
+
function captureStreams() {
|
|
9
|
+
let out = "";
|
|
10
|
+
let err = "";
|
|
11
|
+
return {
|
|
12
|
+
stdout: { write: (s) => { out += s; }, isTTY: false },
|
|
13
|
+
stderr: { write: (s) => { err += s; }, isTTY: false },
|
|
14
|
+
out: () => out,
|
|
15
|
+
err: () => err,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
test("parseArgs collects positional args", () => {
|
|
9
20
|
const { positional, flags } = parseArgs(["how", "many", "rows"]);
|
|
@@ -36,3 +47,72 @@ test("buildOpts maps known flags", () => {
|
|
|
36
47
|
assert.equal(opts.limit, "50");
|
|
37
48
|
assert.deepEqual(opts.positional, ["SELECT 1"]);
|
|
38
49
|
});
|
|
50
|
+
|
|
51
|
+
test("renderRootHelp lists every command and marks subcommand-bearing ones with *", () => {
|
|
52
|
+
const text = renderRootHelp(false);
|
|
53
|
+
assert.match(text, /🐬 DeepSQL/);
|
|
54
|
+
assert.match(text, /Hint: commands suffixed with \* have subcommands/);
|
|
55
|
+
// Leaf command — no star.
|
|
56
|
+
assert.match(text, /\n\s+login\s+ /);
|
|
57
|
+
// Parent commands — must carry a star.
|
|
58
|
+
for (const name of ["config", "connections", "digest", "users", "access", "permissions", "slow-queries"]) {
|
|
59
|
+
assert.match(text, new RegExp(`\\n\\s+${name} \\*`), `expected "${name} *" in root help`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("renderCommandHelp emits subcommands and options for parent commands", () => {
|
|
64
|
+
const text = renderCommandHelp("connections", false);
|
|
65
|
+
assert.match(text, /Usage: deepsql connections/);
|
|
66
|
+
assert.match(text, /Subcommands:/);
|
|
67
|
+
assert.match(text, /Options:/);
|
|
68
|
+
assert.match(text, /list \[--json\]/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("every command in the catalog has a COMMAND_HELP entry", () => {
|
|
72
|
+
// Sourced from the dispatcher map so the lists stay in lockstep.
|
|
73
|
+
const { main: _main } = require("./cli");
|
|
74
|
+
void _main;
|
|
75
|
+
const expected = [
|
|
76
|
+
"login","logout","whoami","config","mcp","connections","query","explain","schema",
|
|
77
|
+
"digest","brain-context","business-rules","relationships","anti-patterns",
|
|
78
|
+
"users","access","permissions","slow-queries","setup",
|
|
79
|
+
];
|
|
80
|
+
for (const name of expected) {
|
|
81
|
+
assert.ok(COMMAND_HELP[name], `missing COMMAND_HELP for ${name}`);
|
|
82
|
+
assert.ok(COMMAND_HELP[name].usage, `missing usage for ${name}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("main prints root help on no args and on --help", async () => {
|
|
87
|
+
for (const argv of [[], ["--help"], ["-h"]]) {
|
|
88
|
+
const io = captureStreams();
|
|
89
|
+
const code = await main(argv, io);
|
|
90
|
+
assert.equal(code, 0);
|
|
91
|
+
assert.match(io.out(), /Usage: deepsql \[options\] \[command\]/);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("main prints per-command help when --help follows the command", async () => {
|
|
96
|
+
const io = captureStreams();
|
|
97
|
+
const code = await main(["connections", "--help"], io);
|
|
98
|
+
assert.equal(code, 0);
|
|
99
|
+
assert.match(io.out(), /Usage: deepsql connections/);
|
|
100
|
+
assert.match(io.out(), /Subcommands:/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("main rejects unknown commands and shows the root help", async () => {
|
|
104
|
+
const io = captureStreams();
|
|
105
|
+
const code = await main(["nope-not-real"], io);
|
|
106
|
+
assert.equal(code, 2);
|
|
107
|
+
assert.match(io.err(), /Unknown command: nope-not-real/);
|
|
108
|
+
assert.match(io.err(), /Usage: deepsql/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("--no-color suppresses ANSI escapes in help output", async () => {
|
|
112
|
+
const io = captureStreams();
|
|
113
|
+
// Force a TTY so the only thing turning color off is --no-color itself.
|
|
114
|
+
io.stdout.isTTY = true;
|
|
115
|
+
await main(["--help", "--no-color"], io);
|
|
116
|
+
// No ESC bytes anywhere.
|
|
117
|
+
assert.equal(/\x1b\[/.test(io.out()), false, "help output should be plain when --no-color is set");
|
|
118
|
+
});
|
package/CLAUDE.md
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
# DeepSQL — guidance for AI agents (Claude Code / Cursor / Codex / etc.)
|
|
2
|
-
|
|
3
|
-
> Read this file before invoking DeepSQL tools. It exists because there are
|
|
4
|
-
> **two surfaces** (MCP tools + CLI commands) that look similar and agents
|
|
5
|
-
> tend to pick the wrong one or use the right one inefficiently.
|
|
6
|
-
|
|
7
|
-
DeepSQL is an autonomous database performance assistant. It exposes itself
|
|
8
|
-
to AI agents in two ways:
|
|
9
|
-
|
|
10
|
-
| Surface | Where it lives | When you use it |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| **MCP tools** (programmatic) | The stdio server you connected via `deepsql mcp` | Default. You're an MCP client and these are first-class tool calls. |
|
|
13
|
-
| **CLI** (`deepsql` binary) | The user's `$PATH`, invoked via Bash | Only when an MCP tool can't do it (admin ops, auth, multi-step user flows) **or** the user explicitly asked you to "run `deepsql ...`". |
|
|
14
|
-
|
|
15
|
-
If you have both available, **prefer MCP tools.** They're structured, typed,
|
|
16
|
-
faster, and don't depend on the user's shell environment.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Decision tree — "I want to..."
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
1. Find which databases I can query
|
|
24
|
-
→ list_connections (returns id, name, type for each)
|
|
25
|
-
|
|
26
|
-
2. Understand a database's structure
|
|
27
|
-
→ get_brain_context (RAG retrieval — best when you have a
|
|
28
|
-
natural-language question; returns ranked
|
|
29
|
-
tables/columns/FKs/docs/business rules)
|
|
30
|
-
→ get_schema (full deterministic schema dump — when you
|
|
31
|
-
need every table; expensive on large DBs)
|
|
32
|
-
→ get_database_objects (tables/views/functions/procedures only)
|
|
33
|
-
|
|
34
|
-
3. Answer a business question about data
|
|
35
|
-
→ get_brain_context first (retrieves what tables hold what, FK edges,
|
|
36
|
-
business rules; gives you grounded context)
|
|
37
|
-
→ then construct SQL yourself, then:
|
|
38
|
-
→ explain_readonly_sql (validate the plan)
|
|
39
|
-
→ execute_readonly_sql (run it)
|
|
40
|
-
|
|
41
|
-
4. Find inferred relationships between tables
|
|
42
|
-
→ get_relationships (returns FK candidates with confidence scores)
|
|
43
|
-
|
|
44
|
-
5. Read business rules / data-access policies
|
|
45
|
-
→ list_business_rules (active rules + guardrails for a connection;
|
|
46
|
-
pass `question` to scope to relevant ones)
|
|
47
|
-
|
|
48
|
-
6. Find anti-patterns
|
|
49
|
-
→ get_anti_patterns kind=table (schema/structural anti-patterns)
|
|
50
|
-
→ get_anti_patterns kind=query (slow/expensive query patterns)
|
|
51
|
-
|
|
52
|
-
7. Investigate slow queries
|
|
53
|
-
→ analyze_slow_queries (recent slow queries with fingerprints + ms)
|
|
54
|
-
|
|
55
|
-
8. Run SQL to inspect data
|
|
56
|
-
→ execute_readonly_sql (read-only — backend rejects mutations)
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
**Rule of thumb for question-answering:** start with `get_brain_context`,
|
|
60
|
-
not `execute_readonly_sql`. The brain context tells you which tables matter,
|
|
61
|
-
their FKs, what the columns mean, and what business rules apply. Skipping
|
|
62
|
-
straight to SQL is how you write queries against the wrong tables.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## MCP tool reference (10 tools)
|
|
67
|
-
|
|
68
|
-
Every tool requires a `connectionId` (string UUID) **except** `list_connections`.
|
|
69
|
-
Always call `list_connections` first if you don't already know the ID.
|
|
70
|
-
|
|
71
|
-
### Discovery
|
|
72
|
-
|
|
73
|
-
#### `list_connections`
|
|
74
|
-
- **Args:** none
|
|
75
|
-
- **Returns:** array of `{ id, connectionName, databaseType, ... }`
|
|
76
|
-
- **Use when:** the user mentions a DB by name and you need its ID, or you
|
|
77
|
-
don't know which DBs are available.
|
|
78
|
-
|
|
79
|
-
#### `get_schema(connectionId)`
|
|
80
|
-
- **Returns:** the cached schema metadata for the whole DB.
|
|
81
|
-
- **Use when:** you need an exhaustive listing. **Avoid** when the DB is
|
|
82
|
-
large (hundreds of tables) — `get_brain_context` ranks the relevant
|
|
83
|
-
subset much faster.
|
|
84
|
-
|
|
85
|
-
#### `get_database_objects(connectionId)`
|
|
86
|
-
- **Returns:** tables, views, functions, procedures.
|
|
87
|
-
- **Use when:** the user asks "what views/functions exist?" — narrower
|
|
88
|
-
than `get_schema`.
|
|
89
|
-
|
|
90
|
-
### RAG / brain retrieval (preferred for question-answering)
|
|
91
|
-
|
|
92
|
-
#### `get_brain_context(connectionId, question, topK?)`
|
|
93
|
-
- **Args:**
|
|
94
|
-
- `question` — natural-language question used for retrieval ranking
|
|
95
|
-
- `topK` (optional, 1–100) — when provided, returns ranked diagnostic
|
|
96
|
-
snippets (good for "show me the top 5 most relevant tables"). When
|
|
97
|
-
omitted, returns the rich training-context payload (tables + columns
|
|
98
|
-
+ FKs + business rules + docs assembled for prompt-grounding).
|
|
99
|
-
- **Use when:** the user asks any analytical question. This is the cheapest
|
|
100
|
-
way to ground yourself before generating SQL.
|
|
101
|
-
- **Output:** typically includes `trainingContext` (text block ready to feed
|
|
102
|
-
into your own context window) plus structured ranked results.
|
|
103
|
-
|
|
104
|
-
#### `list_business_rules(connectionId, question?)`
|
|
105
|
-
- **Returns:** `activeRules` + `applicableGuardrails` + `guardrailContext`.
|
|
106
|
-
- **Use when:** before generating SQL that touches sensitive entities. If
|
|
107
|
-
the rules say "PII columns are blocked," respect that in your output.
|
|
108
|
-
Pass `question` to filter to rules applicable to the user's intent.
|
|
109
|
-
|
|
110
|
-
#### `get_relationships(connectionId)`
|
|
111
|
-
- **Returns:** array of `{ sourceTable, sourceColumn, targetTable, targetColumn, confidence, inferenceMethod, validationStatus }`.
|
|
112
|
-
- **Use when:** writing JOINs and the actual FK constraint isn't declared
|
|
113
|
-
in the schema. Anything `confidence >= 0.8` is safe; lower confidence
|
|
114
|
-
means inferred from naming patterns or data — verify with the user.
|
|
115
|
-
|
|
116
|
-
#### `get_anti_patterns(connectionId, kind?, limit?)`
|
|
117
|
-
- **`kind="table"` (default):** schema-level anti-patterns (missing
|
|
118
|
-
indexes, wide tables, etc.).
|
|
119
|
-
- **`kind="query":** query-level patterns; pass `limit` (1–500).
|
|
120
|
-
- **Use when:** the user asks "what's wrong with this DB?" or you've
|
|
121
|
-
generated a query and want to sanity-check it.
|
|
122
|
-
|
|
123
|
-
### Operations
|
|
124
|
-
|
|
125
|
-
#### `analyze_slow_queries(connectionId, thresholdMs?, limit?)`
|
|
126
|
-
- **Args:** `thresholdMs` defaults 100, `limit` defaults 10.
|
|
127
|
-
- **Returns:** recent slow queries from `pg_stat_statements` with
|
|
128
|
-
fingerprints, durations, example statements.
|
|
129
|
-
- **Use when:** the user asks "what's slow?" or you're triaging a
|
|
130
|
-
performance incident.
|
|
131
|
-
|
|
132
|
-
### Execution
|
|
133
|
-
|
|
134
|
-
#### `execute_readonly_sql(connectionId, query, limit?, timeoutSeconds?)`
|
|
135
|
-
- **Read-only enforced at four layers:** client SQL parser, backend SQL
|
|
136
|
-
parser, per-connection ACL on the calling user's token, and the DB
|
|
137
|
-
role itself usually only has SELECT/EXPLAIN. Mutations (INSERT, UPDATE,
|
|
138
|
-
DELETE, DDL, etc.) are rejected — **don't try to work around this**.
|
|
139
|
-
- **Multi-statement SQL is rejected** in phase 1. Send one statement.
|
|
140
|
-
- **Defaults:** 100-row `limit`, backend default `timeoutSeconds`.
|
|
141
|
-
- **Use when:** you've grounded yourself with `get_brain_context` and need
|
|
142
|
-
to fetch concrete numbers.
|
|
143
|
-
|
|
144
|
-
#### `explain_readonly_sql(connectionId, query)`
|
|
145
|
-
- **Don't include `EXPLAIN` in the query string** — the tool wraps it.
|
|
146
|
-
`ANALYZE` is also rejected (read-only).
|
|
147
|
-
- **Use when:** you want to validate a plan before running it, or you're
|
|
148
|
-
diagnosing why a query is slow.
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
## CLI commands (run via Bash, only when MCP isn't enough)
|
|
153
|
-
|
|
154
|
-
The CLI exposes the same data plane plus admin operations the MCP server
|
|
155
|
-
deliberately doesn't expose. **Only run CLI commands when the user explicitly
|
|
156
|
-
asks you to**, or when an MCP tool can't do what's needed (admin, auth,
|
|
157
|
-
multi-step flows).
|
|
158
|
-
|
|
159
|
-
### Quick reference
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
# Auth (the user typically did this once; don't re-run unless asked)
|
|
163
|
-
deepsql login --url https://<host>
|
|
164
|
-
deepsql whoami
|
|
165
|
-
deepsql logout
|
|
166
|
-
|
|
167
|
-
# Connections — the human's "active DB" pin (CLI-only; MCP tools don't read this)
|
|
168
|
-
deepsql connections list # marks active with *
|
|
169
|
-
deepsql connections use <name> # pin
|
|
170
|
-
deepsql connections current # show pinned
|
|
171
|
-
deepsql connections unset
|
|
172
|
-
|
|
173
|
-
# Read-only data ops (mirror MCP tools — same backend, same guardrails)
|
|
174
|
-
deepsql query "SELECT ..." --connection <name>
|
|
175
|
-
deepsql explain "SELECT ..." --connection <name>
|
|
176
|
-
deepsql schema [tables|objects] --connection <name>
|
|
177
|
-
|
|
178
|
-
# Brain / RAG (mirror the MCP brain tools)
|
|
179
|
-
deepsql brain-context "<question>" --connection <name> [--top-k N]
|
|
180
|
-
deepsql business-rules --connection <name> [--question "..."]
|
|
181
|
-
deepsql relationships --connection <name>
|
|
182
|
-
deepsql anti-patterns --connection <name> [--kind table|query] [--limit N]
|
|
183
|
-
|
|
184
|
-
# Slack daily digest
|
|
185
|
-
deepsql digest [N] --connection <name>
|
|
186
|
-
|
|
187
|
-
# Slow-query operations
|
|
188
|
-
deepsql slow-queries latest --connection <name>
|
|
189
|
-
deepsql slow-queries history --connection <name> [N]
|
|
190
|
-
deepsql slow-queries analyze --connection <name>
|
|
191
|
-
deepsql slow-queries optimize --connection <name> --query-id <id> # SSE stream
|
|
192
|
-
|
|
193
|
-
# Admin (require ADMIN role)
|
|
194
|
-
deepsql users list | get | add | set-role | lock | unlock | disable | delete
|
|
195
|
-
deepsql access list | grant | revoke | policy
|
|
196
|
-
deepsql permissions list | override | reset
|
|
197
|
-
deepsql setup [--skip-email] [--skip-slack] # post-install wizard
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### When CLI is the right call (vs MCP)
|
|
201
|
-
|
|
202
|
-
- The user said "run `deepsql ...`" or "use the CLI."
|
|
203
|
-
- The operation is admin (`users`, `access`, `permissions`, `setup`) — these
|
|
204
|
-
aren't exposed via MCP intentionally.
|
|
205
|
-
- The user wants Slack digest content (`digest`).
|
|
206
|
-
- You're in a script context where structured stdin/stdout is preferable.
|
|
207
|
-
|
|
208
|
-
### When CLI is the **wrong** call
|
|
209
|
-
|
|
210
|
-
- For everything in the decision tree above. The MCP equivalents are
|
|
211
|
-
faster and don't depend on the user's `$PATH`, env, or saved auth.
|
|
212
|
-
- For executing SQL the user is paying you to write — use
|
|
213
|
-
`execute_readonly_sql`, not `Bash("deepsql query ...")`.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## Common mistakes — and how to avoid them
|
|
218
|
-
|
|
219
|
-
### ❌ Generating SQL without retrieving brain context first
|
|
220
|
-
The user asks: *"How many active customers do we have?"*
|
|
221
|
-
|
|
222
|
-
**Wrong:** call `execute_readonly_sql("SELECT COUNT(*) FROM customers WHERE active = true")` —
|
|
223
|
-
guesses at the table name (`customers` vs `dim_customer` vs `users`),
|
|
224
|
-
guesses at the column (`active` vs `is_active` vs `status='ACTIVE'`),
|
|
225
|
-
ignores any business rule that defines what "active" means.
|
|
226
|
-
|
|
227
|
-
**Right:**
|
|
228
|
-
1. `get_brain_context(connectionId, "how many active customers")` — returns the
|
|
229
|
-
right table (`dim_customer`) and the column convention.
|
|
230
|
-
2. `list_business_rules(connectionId, "active customers")` — returns the rule
|
|
231
|
-
if "active" has a workspace-specific definition.
|
|
232
|
-
3. Generate SQL using the names + rules from #1 and #2.
|
|
233
|
-
4. `explain_readonly_sql(...)` — sanity check.
|
|
234
|
-
5. `execute_readonly_sql(...)` — run it.
|
|
235
|
-
|
|
236
|
-
### ❌ Calling `get_schema` on every analysis question
|
|
237
|
-
`get_schema` returns the entire DB. On a 200-table OLAP warehouse that's a
|
|
238
|
-
huge response and most of it is irrelevant to the question. **Use
|
|
239
|
-
`get_brain_context` for question-scoped retrieval.** Reserve `get_schema`
|
|
240
|
-
for exhaustive listing tasks.
|
|
241
|
-
|
|
242
|
-
### ❌ Trying to mutate data
|
|
243
|
-
Every execution path is read-only. INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/
|
|
244
|
-
TRUNCATE are all rejected at the SQL parser layer. If the user asks for a
|
|
245
|
-
mutation, **stop and tell them DeepSQL is read-only**, then offer to draft
|
|
246
|
-
the SQL for them to run themselves.
|
|
247
|
-
|
|
248
|
-
### ❌ Forgetting the connectionId
|
|
249
|
-
Every tool except `list_connections` requires it. If the user mentions a DB
|
|
250
|
-
by name (e.g., "look at prod-replica"), call `list_connections` first to
|
|
251
|
-
resolve the name → UUID. Don't guess.
|
|
252
|
-
|
|
253
|
-
### ❌ Re-fetching context on every turn
|
|
254
|
-
Schema and brain context don't change minute-to-minute. If you already
|
|
255
|
-
called `get_brain_context` for a related question this conversation, reuse
|
|
256
|
-
the result. Don't re-call unless the question has shifted topics.
|
|
257
|
-
|
|
258
|
-
### ❌ Mixing CLI invocations and MCP tool calls in the same session
|
|
259
|
-
Pick one. If you have MCP available, stay in MCP. If you only have Bash,
|
|
260
|
-
use the CLI. Mixing forces the user to debug two surfaces.
|
|
261
|
-
|
|
262
|
-
### ❌ Calling `analyze_slow_queries` and immediately querying the slow-query log table directly
|
|
263
|
-
The MCP tool already does the right query against `pg_stat_statements` (or
|
|
264
|
-
the equivalent for MySQL) with the right thresholds. Don't reinvent it.
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## Output handling tips
|
|
269
|
-
|
|
270
|
-
- **`get_brain_context` returns a `trainingContext` text block.** It's
|
|
271
|
-
designed to drop into your prompt as-is. Don't summarize it before
|
|
272
|
-
generating SQL — let the structured names flow through.
|
|
273
|
-
- **`execute_readonly_sql` returns `{ result: { columns, rows, rowCount, totalRowCount, isLimited, ... }, success, queryType }`.**
|
|
274
|
-
`rows` is array-of-arrays (column-positional), not array-of-objects. The
|
|
275
|
-
CLI's `query` command renders this; if you're consuming the structured
|
|
276
|
-
response yourself, zip `columns` and `rows[i]` to get an object.
|
|
277
|
-
- **`explain_readonly_sql` returns the plan as JSON.** Postgres-style
|
|
278
|
-
textual EXPLAIN is in `plan` if available; structured form may be
|
|
279
|
-
alongside.
|
|
280
|
-
- **`analyze_slow_queries` returns slow queries with fingerprints, not raw
|
|
281
|
-
SQL.** Fingerprints are normalized (`?` for literals). Use the
|
|
282
|
-
`queryId` to feed back into `optimize` flows.
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
## Multi-database situations
|
|
287
|
-
|
|
288
|
-
DeepSQL doesn't support cross-connection JOINs at the SQL layer. If the user
|
|
289
|
-
asks a question that spans DBs:
|
|
290
|
-
|
|
291
|
-
1. Call `list_connections` to enumerate.
|
|
292
|
-
2. For each relevant DB, call `get_brain_context` and/or `execute_readonly_sql`.
|
|
293
|
-
3. Combine the results in your reasoning, not in SQL.
|
|
294
|
-
|
|
295
|
-
The CLI's "active connection" pin (`deepsql connections use`) is **not** read
|
|
296
|
-
by MCP tools — it only saves typing for human CLI users. As an MCP client,
|
|
297
|
-
always pass `connectionId` explicitly per call.
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
## Authentication & security model
|
|
302
|
-
|
|
303
|
-
You don't need to manage auth — the MCP server was launched with a saved
|
|
304
|
-
token from `~/.config/deepsql/auth.json`. Every tool call carries that
|
|
305
|
-
token. The token is bound to a specific user identity:
|
|
306
|
-
|
|
307
|
-
- The user's role and per-connection ACLs are enforced **server-side**.
|
|
308
|
-
If you call a tool and get an authorization error, surface it to the
|
|
309
|
-
user — don't retry with different parameters.
|
|
310
|
-
- The user may have **chat-access policies** (plain-English rules
|
|
311
|
-
attached to a connection). The brain context already reflects them; if
|
|
312
|
-
a query you generate triggers a policy violation, the backend rejects
|
|
313
|
-
it. Trust the rejection and ask the user how to proceed.
|
|
314
|
-
- **Read-only is enforced at four independent layers** (client parser,
|
|
315
|
-
backend parser, per-connection ACL, DB role). Don't try to bypass any of
|
|
316
|
-
them — each rejection is a real signal that the operation isn't safe.
|
|
317
|
-
|
|
318
|
-
---
|
|
319
|
-
|
|
320
|
-
## When in doubt
|
|
321
|
-
|
|
322
|
-
1. Call `list_connections` first if you don't have a connectionId.
|
|
323
|
-
2. Call `get_brain_context` second if you have a question.
|
|
324
|
-
3. Generate your SQL using the names and rules from those calls.
|
|
325
|
-
4. Call `explain_readonly_sql` if performance matters.
|
|
326
|
-
5. Call `execute_readonly_sql` last.
|
|
327
|
-
|
|
328
|
-
That five-step flow handles 80% of legitimate analytical workloads. Anything
|
|
329
|
-
that doesn't fit this pattern probably warrants asking the user a
|
|
330
|
-
clarifying question instead of guessing.
|