@deepsql/mcp 0.8.0 → 0.10.1
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 +289 -0
- package/CLAUDE.md +330 -0
- package/package.json +3 -1
- package/src/auth/store.js +32 -1
- package/src/auth/store.test.js +22 -0
- package/src/cli.js +40 -5
- package/src/commands/_connections.js +26 -3
- package/src/commands/_connections.test.js +21 -4
- package/src/commands/_session.js +4 -1
- package/src/commands/access.js +0 -2
- package/src/commands/admin.test.js +37 -0
- package/src/commands/anti-patterns.js +0 -1
- package/src/commands/brain-context.js +27 -8
- package/src/commands/business-rules.js +0 -1
- package/src/commands/connections.js +579 -9
- package/src/commands/digest.js +3 -5
- package/src/commands/explain.js +0 -1
- package/src/commands/query.js +0 -1
- package/src/commands/relationships.js +0 -1
- package/src/commands/schema.js +0 -1
- package/src/commands/slow-queries.js +2 -3
- package/src/commands/whoami.js +11 -5
- package/src/connections/schema.js +213 -0
- package/src/connections/secrets.js +167 -0
- package/src/connections/secrets.test.js +151 -0
package/src/auth/store.test.js
CHANGED
|
@@ -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>
|
|
131
|
-
--token <tok>
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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 =
|
|
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("
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
});
|
package/src/commands/_session.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/src/commands/access.js
CHANGED
|
@@ -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
|
|
53
|
-
// a coding agent. /context returns
|
|
54
|
-
//
|
|
55
|
-
|
|
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(
|
|
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);
|