@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.
- package/AGENT-SETUP.md +289 -0
- package/CLAUDE.md +330 -0
- package/deepsql-phase1-lib.js +230 -36
- package/deepsql-phase1-server.js +1 -1
- package/package.json +3 -1
- package/src/api/client.js +1 -1
- package/src/auth/store.js +32 -1
- package/src/auth/store.test.js +22 -0
- package/src/cli.js +81 -13
- 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 +76 -0
- package/src/commands/brain-context.js +87 -0
- package/src/commands/business-rules.js +58 -0
- package/src/commands/connections.js +559 -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 +55 -0
- 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/commands/ask.js +0 -44
|
@@ -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
|
+
});
|
|
@@ -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 };
|