@deepsql/mcp 0.8.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.
@@ -63,6 +63,28 @@ test("auth file is written with mode 0600", { skip: process.platform === "win32"
63
63
  });
64
64
  });
65
65
 
66
+ test("setDefaultConnection round-trips and clearing it removes the field", () => {
67
+ withTempStore((store) => {
68
+ store.setProfile("http://x", { token: "t", username: "a" });
69
+ assert.equal(store.getDefaultConnection("http://x"), null);
70
+
71
+ store.setDefaultConnection("http://x", "prod-replica");
72
+ assert.equal(store.getDefaultConnection("http://x"), "prod-replica");
73
+
74
+ store.setDefaultConnection("http://x", null);
75
+ assert.equal(store.getDefaultConnection("http://x"), null);
76
+ });
77
+ });
78
+
79
+ test("setDefaultConnection refuses to write when the profile doesn't exist", () => {
80
+ withTempStore((store) => {
81
+ assert.throws(
82
+ () => store.setDefaultConnection("http://no-such-profile", "x"),
83
+ /No profile saved/,
84
+ );
85
+ });
86
+ });
87
+
66
88
  test("rejects loose perms unless DEEPSQL_INSECURE_AUTH=1", { skip: process.platform === "win32" }, () => {
67
89
  withTempStore((store, dir) => {
68
90
  store.setProfile("http://localhost:8080", { token: "t", username: "a" });
package/src/cli.js CHANGED
@@ -60,7 +60,31 @@ Commands:
60
60
  config set-default <url> Set the default profile.
61
61
  config path Print the auth file path.
62
62
  mcp Run the stdio MCP server using the saved token.
63
- connections list [--json] List database connections.
63
+ connections list [--json] List database connections (active default
64
+ is marked with `*`).
65
+ connections use <name> Pin <name> as the active default; commands
66
+ drop --connection from then on.
67
+ connections current Print the active default (exit 1 if none).
68
+ connections unset Clear the active default for this profile.
69
+ connections schema [--json] Print the JSON Schema for the connection
70
+ config (the input format for \`add\`).
71
+ connections add [--from-file <p>] [--from-stdin] [--upsert] [--no-test]
72
+ [--wait] [--delete-after] [--cloud]
73
+ [--allow-plaintext-secrets]
74
+ Create a connection. Default is interactive
75
+ prompts; use --from-file for AI-agent flows.
76
+ connections update <name> --from-file <p>
77
+ PATCH-style update; omitted secrets are
78
+ preserved.
79
+ connections remove <name> [--yes]
80
+ Delete a connection (DELETE /connections).
81
+ connections test [<name> | --from-file <p>]
82
+ Validate a connection without saving.
83
+ Prints the privilege report.
84
+ connections show <name> [--json]
85
+ Show a connection's config (secrets masked).
86
+ connections init <name> [--force] [--wait]
87
+ Trigger brain re-initialization.
64
88
  query "<sql>" --connection <name> [--limit <n>] [--timeout-seconds <n>] [--file <path>] [--json]
65
89
  Run a read-only SQL statement. Enforced
66
90
  read-only at the backend (parser-level) and
@@ -127,10 +151,12 @@ Admin commands (require ADMIN role on the calling token):
127
151
  and are NOT touched by this wizard.
128
152
 
129
153
  Global options:
130
- --url <url> Override the DeepSQL base URL.
131
- --token <tok> Override the auth token (also: DEEPSQL_AUTH_TOKEN).
132
- -h, --help Show help.
133
- -v, --version Show version.
154
+ --url <url> Override the DeepSQL base URL.
155
+ --token <tok> Override the auth token (also: DEEPSQL_AUTH_TOKEN).
156
+ --connection <name> Override the active connection (also: DEEPSQL_CONNECTION,
157
+ or pin one with \`deepsql connections use <name>\`).
158
+ -h, --help Show help.
159
+ -v, --version Show version.
134
160
  `;
135
161
 
136
162
  function parseArgs(argv) {
@@ -229,6 +255,15 @@ function buildOpts(parsed) {
229
255
  skipComplete: !!f.skipComplete,
230
256
  // Confirmations
231
257
  yes: !!f.yes || !!f.y,
258
+ // Connection management
259
+ fromFile: f.fromFile || null,
260
+ fromStdin: !!f.fromStdin,
261
+ upsert: !!f.upsert,
262
+ noTest: !!f.noTest,
263
+ wait: !!f.wait,
264
+ deleteAfter: !!f.deleteAfter,
265
+ cloud: !!f.cloud,
266
+ allowPlaintextSecrets: !!f.allowPlaintextSecrets,
232
267
  };
233
268
  }
234
269
 
@@ -29,11 +29,34 @@ async function listConnections(session) {
29
29
  return cachedList;
30
30
  }
31
31
 
32
+ /**
33
+ * Resolution chain for the connection a command should hit:
34
+ *
35
+ * 1. explicit `input` argument (i.e. opts.connection from --connection flag)
36
+ * 2. DEEPSQL_CONNECTION env var
37
+ * 3. session.defaultConnection (set via `deepsql connections use <name>`)
38
+ *
39
+ * If none of those produce a value, throw a friendly message that points the
40
+ * user at all three escape hatches.
41
+ */
32
42
  async function resolveConnectionId(session, input) {
33
- if (!input || typeof input !== "string") {
34
- throw new Error("Pass a connection name or id with --connection.");
43
+ let raw = input;
44
+ let source = "--connection";
45
+ if (raw == null || raw === "") {
46
+ raw = process.env.DEEPSQL_CONNECTION || null;
47
+ source = "DEEPSQL_CONNECTION";
48
+ }
49
+ if (raw == null || raw === "") {
50
+ raw = session && session.defaultConnection ? session.defaultConnection : null;
51
+ source = "saved default";
52
+ }
53
+ if (!raw || typeof raw !== "string") {
54
+ throw new Error(
55
+ "No connection specified. Pass --connection <name>, set DEEPSQL_CONNECTION, " +
56
+ "or run `deepsql connections use <name>` to pin a default.",
57
+ );
35
58
  }
36
- const trimmed = input.trim();
59
+ const trimmed = raw.trim();
37
60
  if (UUID_RE.test(trimmed)) return trimmed;
38
61
 
39
62
  const connections = await listConnections(session);
@@ -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
+ });
@@ -13,7 +13,6 @@ const { resolveSession } = require("./_session");
13
13
  const { resolveConnectionId } = require("./_connections");
14
14
 
15
15
  async function run(opts, { stdout = process.stdout } = {}) {
16
- if (!opts.connection) throw new Error("--connection <name> is required.");
17
16
 
18
17
  const session = resolveSession(opts);
19
18
  const connectionId = await resolveConnectionId(session, opts.connection);
@@ -23,7 +23,6 @@ async function run(opts, { stdout = process.stdout } = {}) {
23
23
  'Pass a question: `deepsql brain-context "which tables hold customer orders?" --connection <name>`.',
24
24
  );
25
25
  }
26
- if (!opts.connection) throw new Error("--connection <name> is required.");
27
26
 
28
27
  const session = resolveSession(opts);
29
28
  const connectionId = await resolveConnectionId(session, opts.connection);
@@ -49,16 +48,36 @@ async function run(opts, { stdout = process.stdout } = {}) {
49
48
  return;
50
49
  }
51
50
 
52
- // Default: print the most useful field directly so it can be piped into
53
- // a coding agent. /context returns trainingContext (text); /retrieve
54
- // returns ranked results fall back to JSON for the latter.
55
- if (response && typeof response === "object" && response.trainingContext) {
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) {
56
66
  if (response.skipped) {
57
- stdout.write(`# (skipped: ${response.skipReason || "n/a"})\n`);
67
+ stdout.write(`# (retrieval skipped: ${response.skipReason || "n/a"})\n\n`);
68
+ }
69
+ if (response.trainingContext) {
70
+ stdout.write(`${response.trainingContext}\n`);
58
71
  }
59
- stdout.write(`${response.trainingContext}\n`);
60
72
  if (response.companyKnowledgeContext) {
61
- stdout.write(`\n# Company knowledge\n${response.companyKnowledgeContext}\n`);
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
+ );
62
81
  }
63
82
  return;
64
83
  }
@@ -12,7 +12,6 @@ const { resolveSession } = require("./_session");
12
12
  const { resolveConnectionId } = require("./_connections");
13
13
 
14
14
  async function run(opts, { stdout = process.stdout } = {}) {
15
- if (!opts.connection) throw new Error("--connection <name> is required.");
16
15
 
17
16
  const session = resolveSession(opts);
18
17
  const connectionId = await resolveConnectionId(session, opts.connection);