@deepsql/mcp 0.6.0 → 0.10.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.
@@ -66,9 +66,26 @@ test("throws a useful error listing available names when no match", async () =>
66
66
  });
67
67
  });
68
68
 
69
- test("requires non-empty input", async () => {
70
- await withMockedRequest([], async ({ resolveConnectionId }) => {
71
- await assert.rejects(() => resolveConnectionId(session, ""), /connection name/);
72
- await assert.rejects(() => resolveConnectionId(session, null), /connection name/);
69
+ test("falls back through env and saved default before erroring", async () => {
70
+ const connections = [{ id: "id-default", connectionName: "saved" }];
71
+ await withMockedRequest(connections, async ({ resolveConnectionId }) => {
72
+ // No flag, no env, no saved default → error mentioning all three escape hatches.
73
+ delete process.env.DEEPSQL_CONNECTION;
74
+ await assert.rejects(
75
+ () => resolveConnectionId({ baseUrl: "http://x", token: "t" }, ""),
76
+ /No connection specified.*--connection.*DEEPSQL_CONNECTION.*connections use/s,
77
+ );
78
+
79
+ // DEEPSQL_CONNECTION is consulted next.
80
+ process.env.DEEPSQL_CONNECTION = "saved";
81
+ try {
82
+ assert.equal(await resolveConnectionId({ baseUrl: "http://x", token: "t" }, ""), "id-default");
83
+ } finally {
84
+ delete process.env.DEEPSQL_CONNECTION;
85
+ }
86
+
87
+ // Otherwise, the session's defaultConnection is the final fallback.
88
+ const session = { baseUrl: "http://x", token: "t", defaultConnection: "saved" };
89
+ assert.equal(await resolveConnectionId(session, null), "id-default");
73
90
  });
74
91
  });
@@ -31,7 +31,10 @@ function resolveSession(opts = {}) {
31
31
  if (!token) {
32
32
  throw new Error(`No auth token for ${baseUrl}. Run \`deepsql login --url ${baseUrl}\`.`);
33
33
  }
34
- return { baseUrl, token, profile };
34
+ // Surface the saved active connection so the connection resolver can fall
35
+ // back to it when --connection isn't passed. Set via `deepsql connections use`.
36
+ const defaultConnection = profile?.defaultConnection || null;
37
+ return { baseUrl, token, profile, defaultConnection };
35
38
  }
36
39
 
37
40
  function stripApiSuffix(url) {
@@ -94,7 +94,6 @@ async function cmdList(opts, { stdout = process.stdout } = {}) {
94
94
 
95
95
  async function cmdGrant(opts, { stdout = process.stdout } = {}) {
96
96
  if (!opts.user) throw new Error("--user <ref> is required.");
97
- if (!opts.connection) throw new Error("--connection <name> is required.");
98
97
  const level = (opts.level || "READ").toUpperCase();
99
98
  if (!["READ", "WRITE", "ADMIN"].includes(level)) {
100
99
  throw new Error(`Invalid --level "${level}". Pick read, write, or admin.`);
@@ -122,7 +121,6 @@ async function cmdGrant(opts, { stdout = process.stdout } = {}) {
122
121
 
123
122
  async function cmdRevoke(opts, { stdout = process.stdout } = {}) {
124
123
  if (!opts.user) throw new Error("--user <ref> is required.");
125
- if (!opts.connection) throw new Error("--connection <name> is required.");
126
124
 
127
125
  const session = resolveSession(opts);
128
126
  const user = await resolveUser(session, opts.user);
@@ -133,3 +133,40 @@ test("--password=secret keeps the string value (still truthy)", () => {
133
133
  const o = opts(["--password=secret"]);
134
134
  assert.equal(o.password, "secret");
135
135
  });
136
+
137
+ // connections add/test flags ----------------------------------------------
138
+
139
+ test("connections add --from-file with --upsert/--no-test/--wait/--delete-after", () => {
140
+ const o = opts([
141
+ "add",
142
+ "--from-file",
143
+ "/tmp/conn.json",
144
+ "--upsert",
145
+ "--no-test",
146
+ "--wait",
147
+ "--delete-after",
148
+ ]);
149
+ assert.equal(o.fromFile, "/tmp/conn.json");
150
+ assert.equal(o.upsert, true);
151
+ assert.equal(o.noTest, true);
152
+ assert.equal(o.wait, true);
153
+ assert.equal(o.deleteAfter, true);
154
+ });
155
+
156
+ test("connections add --from-stdin + --allow-plaintext-secrets", () => {
157
+ const o = opts(["add", "--from-stdin", "--allow-plaintext-secrets"]);
158
+ assert.equal(o.fromStdin, true);
159
+ assert.equal(o.allowPlaintextSecrets, true);
160
+ });
161
+
162
+ test("connections add --cloud triggers cloud-prompt path", () => {
163
+ const o = opts(["add", "--cloud"]);
164
+ assert.equal(o.cloud, true);
165
+ });
166
+
167
+ test("connections init <name> --force --wait", () => {
168
+ const o = opts(["init", "prod", "--force", "--wait"]);
169
+ assert.deepEqual(o.positional, ["init", "prod"]);
170
+ assert.equal(o.force, true);
171
+ assert.equal(o.wait, true);
172
+ });
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql anti-patterns --connection <name> [--kind table|query] [--limit N] [--json]`
5
+ *
6
+ * Returns DeepSQL-detected anti-patterns. Two flavors:
7
+ * - kind=table (default) → GET /brain/table-anti-patterns/{cid}
8
+ * - kind=query → GET /brain/query-anti-patterns/{cid}?limit=
9
+ */
10
+
11
+ const { request } = require("../api/client");
12
+ const { resolveSession } = require("./_session");
13
+ const { resolveConnectionId } = require("./_connections");
14
+
15
+ async function run(opts, { stdout = process.stdout } = {}) {
16
+
17
+ const session = resolveSession(opts);
18
+ const connectionId = await resolveConnectionId(session, opts.connection);
19
+
20
+ const kind = opts.kind === "query" ? "query" : "table";
21
+ const path =
22
+ kind === "query"
23
+ ? `/brain/query-anti-patterns/${encodeURIComponent(connectionId)}`
24
+ : `/brain/table-anti-patterns/${encodeURIComponent(connectionId)}`;
25
+
26
+ const query = {};
27
+ if (kind === "query" && opts.limit != null) query.limit = opts.limit;
28
+
29
+ const response = await request(session.baseUrl, path, {
30
+ token: session.token,
31
+ query,
32
+ });
33
+
34
+ if (opts.json) {
35
+ stdout.write(`${JSON.stringify(response, null, 2)}\n`);
36
+ return;
37
+ }
38
+
39
+ if (kind === "table") {
40
+ const tableMap =
41
+ response && typeof response === "object" && !Array.isArray(response) ? response : {};
42
+ const tables = Object.keys(tableMap);
43
+ if (tables.length === 0) {
44
+ stdout.write("No table-level anti-patterns detected.\n");
45
+ return;
46
+ }
47
+ const noun = tables.length === 1 ? "table" : "tables";
48
+ stdout.write(`${tables.length} ${noun} with anti-patterns:\n`);
49
+ for (const t of tables) {
50
+ const entry = tableMap[t] || {};
51
+ const patterns = entry.patterns || entry.antiPatterns || entry || [];
52
+ const n = Array.isArray(patterns) ? patterns.length : 0;
53
+ stdout.write(` • ${t}: ${n} ${n === 1 ? "pattern" : "patterns"}\n`);
54
+ }
55
+ return;
56
+ }
57
+
58
+ const list = Array.isArray(response) ? response : response?.patterns || [];
59
+ if (list.length === 0) {
60
+ stdout.write("No query anti-patterns detected.\n");
61
+ return;
62
+ }
63
+ const sev = list.reduce((acc, p) => {
64
+ const s = p.severity || "UNKNOWN";
65
+ acc[s] = (acc[s] || 0) + 1;
66
+ return acc;
67
+ }, {});
68
+ const sevStr = Object.entries(sev).map(([k, v]) => `${k}=${v}`).join(", ");
69
+ const noun = list.length === 1 ? "anti-pattern" : "anti-patterns";
70
+ stdout.write(`${list.length} query ${noun}${sevStr ? ` (${sevStr})` : ""}:\n`);
71
+ for (const p of list.slice(0, 20)) {
72
+ stdout.write(` • [${p.severity || "?"}] ${p.patternType || p.name || "pattern"}: ${p.description || ""}\n`);
73
+ }
74
+ }
75
+
76
+ module.exports = { run };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql brain-context "<question>" --connection <name> [--top-k N] [--json]`
5
+ *
6
+ * Returns DeepSQL's retrieval context for a question — relevant tables,
7
+ * columns, FK relationships, training docs, business rules, and embedding-
8
+ * ranked snippets — without invoking the chat agent. Coding agents (Claude
9
+ * Code, Cursor, Codex) feed this into their own LLM to generate SQL or prose.
10
+ *
11
+ * - Without --top-k → POST /training/context/{cid} (rich payload)
12
+ * - With --top-k → GET /training/retrieve/{cid}?q=&topK= (ranked snippets)
13
+ */
14
+
15
+ const { request } = require("../api/client");
16
+ const { resolveSession } = require("./_session");
17
+ const { resolveConnectionId } = require("./_connections");
18
+
19
+ async function run(opts, { stdout = process.stdout } = {}) {
20
+ const question = opts.positional.join(" ").trim();
21
+ if (!question) {
22
+ throw new Error(
23
+ 'Pass a question: `deepsql brain-context "which tables hold customer orders?" --connection <name>`.',
24
+ );
25
+ }
26
+
27
+ const session = resolveSession(opts);
28
+ const connectionId = await resolveConnectionId(session, opts.connection);
29
+
30
+ const topK = opts.topK == null ? null : Number.parseInt(opts.topK, 10);
31
+ let response;
32
+ if (topK != null && Number.isFinite(topK)) {
33
+ response = await request(
34
+ session.baseUrl,
35
+ `/training/retrieve/${encodeURIComponent(connectionId)}`,
36
+ { token: session.token, query: { q: question, topK } },
37
+ );
38
+ } else {
39
+ response = await request(
40
+ session.baseUrl,
41
+ `/training/context/${encodeURIComponent(connectionId)}`,
42
+ { method: "POST", token: session.token, json: { question } },
43
+ );
44
+ }
45
+
46
+ if (opts.json) {
47
+ stdout.write(`${JSON.stringify(response, null, 2)}\n`);
48
+ return;
49
+ }
50
+
51
+ // Default: print the most useful fields directly so output can be piped
52
+ // into a coding agent. /context returns one or more of:
53
+ // - trainingContext (the rich, prompt-ready RAG block)
54
+ // - companyKnowledgeContext (workspace hints — populated even when the
55
+ // pipeline detects a "simple_schema_question" and skips RAG)
56
+ // - ragTableNames, retrievalIntent, resultCount, etc. (diagnostic)
57
+ // /retrieve (--top-k) returns ranked snippets — fall back to JSON for that.
58
+ const isContextPayload =
59
+ response &&
60
+ typeof response === "object" &&
61
+ ("trainingContext" in response ||
62
+ "companyKnowledgeContext" in response ||
63
+ "skipped" in response);
64
+
65
+ if (isContextPayload) {
66
+ if (response.skipped) {
67
+ stdout.write(`# (retrieval skipped: ${response.skipReason || "n/a"})\n\n`);
68
+ }
69
+ if (response.trainingContext) {
70
+ stdout.write(`${response.trainingContext}\n`);
71
+ }
72
+ if (response.companyKnowledgeContext) {
73
+ stdout.write(
74
+ `${response.trainingContext ? "\n" : ""}${response.companyKnowledgeContext}\n`,
75
+ );
76
+ }
77
+ if (!response.trainingContext && !response.companyKnowledgeContext) {
78
+ stdout.write(
79
+ "(no retrieval results — pass --top-k <n> to fetch ranked snippets, or `--json` for the full payload)\n",
80
+ );
81
+ }
82
+ return;
83
+ }
84
+ stdout.write(`${JSON.stringify(response, null, 2)}\n`);
85
+ }
86
+
87
+ module.exports = { run };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql business-rules --connection <name> [--question "..."] [--json]`
5
+ *
6
+ * Lists active business rules and SQL guardrails for a connection. Wraps
7
+ * GET /business-rules/connection/{connectionId}?question=...
8
+ */
9
+
10
+ const { request } = require("../api/client");
11
+ const { resolveSession } = require("./_session");
12
+ const { resolveConnectionId } = require("./_connections");
13
+
14
+ async function run(opts, { stdout = process.stdout } = {}) {
15
+
16
+ const session = resolveSession(opts);
17
+ const connectionId = await resolveConnectionId(session, opts.connection);
18
+
19
+ const query = {};
20
+ if (opts.question) query.question = opts.question;
21
+ const response = await request(
22
+ session.baseUrl,
23
+ `/business-rules/connection/${encodeURIComponent(connectionId)}`,
24
+ { token: session.token, query },
25
+ );
26
+
27
+ if (opts.json) {
28
+ stdout.write(`${JSON.stringify(response, null, 2)}\n`);
29
+ return;
30
+ }
31
+
32
+ const active = response?.activeRules || [];
33
+ const guards = response?.applicableGuardrails || [];
34
+
35
+ if (active.length === 0 && guards.length === 0) {
36
+ stdout.write("No business rules or guardrails configured for this connection.\n");
37
+ return;
38
+ }
39
+
40
+ stdout.write(
41
+ `${plural(active.length, "active business rule", "active business rules")}, ` +
42
+ `${plural(guards.length, "applicable guardrail", "applicable guardrails")}.\n`,
43
+ );
44
+ for (const r of active) {
45
+ const name = r.name || r.ruleName || `rule#${r.id}`;
46
+ const desc = r.description || r.ruleText || "";
47
+ stdout.write(` • ${name}${desc ? `: ${desc}` : ""}\n`);
48
+ }
49
+ if (guards.length) {
50
+ stdout.write(`\nGuardrail context:\n${response?.guardrailContext || "(none)"}\n`);
51
+ }
52
+ }
53
+
54
+ function plural(n, singular, many) {
55
+ return `${n} ${n === 1 ? singular : many}`;
56
+ }
57
+
58
+ module.exports = { run };