@deepsql/mcp 0.11.0 → 0.13.4
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 +83 -31
- package/CLAUDE.md +382 -0
- package/README.md +109 -25
- package/claude_desktop_config.customer.example.json +3 -11
- package/codex_config.customer.example.toml +12 -8
- package/deepsql-phase1-lib.js +149 -27
- package/deepsql-phase1-server.js +1 -1
- package/package.json +3 -2
- package/skills/SKILL_BODY.md +125 -0
- package/src/api/client.js +35 -1
- package/src/cli.js +65 -20
- package/src/cli.test.js +1 -1
- package/src/commands/analyze.js +165 -0
- package/src/commands/analyze.test.js +180 -0
- package/src/commands/explain.js +18 -34
- package/src/commands/mcp.js +592 -8
- package/src/commands/mcp.test.js +529 -0
- package/src/commands/query.js +95 -13
- package/src/commands/query.test.js +214 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const Module = require("node:module");
|
|
6
|
+
|
|
7
|
+
const { parseArgs, buildOpts } = require("../cli");
|
|
8
|
+
|
|
9
|
+
function opts(argv) {
|
|
10
|
+
return buildOpts(parseArgs(argv));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── argv plumbing ─────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
test("query --write opt-in lands in opts.write", () => {
|
|
16
|
+
// Boolean flags eat the next token if they aren't given a value;
|
|
17
|
+
// we always put SQL first in real use so that doesn't happen.
|
|
18
|
+
const o = opts(["UPDATE t SET x=1 WHERE id=1", "--connection", "c1", "--write"]);
|
|
19
|
+
assert.equal(o.write, true);
|
|
20
|
+
assert.equal(o.connection, "c1");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("query --caller-agent lands in opts.callerAgent", () => {
|
|
24
|
+
const o = opts(["--caller-agent", "claude-code", "SELECT 1"]);
|
|
25
|
+
assert.equal(o.callerAgent, "claude-code");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ─── dispatch with fake api/client ─────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function loadWithStubs({ responses = [], onRequest = () => {}, confirmAnswer = false }) {
|
|
31
|
+
for (const k of [
|
|
32
|
+
require.resolve("../api/client"),
|
|
33
|
+
require.resolve("./_session"),
|
|
34
|
+
require.resolve("./_connections"),
|
|
35
|
+
require.resolve("../ui/prompts"),
|
|
36
|
+
require.resolve("./query"),
|
|
37
|
+
]) {
|
|
38
|
+
delete require.cache[k];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const apiKey = require.resolve("../api/client");
|
|
42
|
+
let callIndex = 0;
|
|
43
|
+
require.cache[apiKey] = {
|
|
44
|
+
id: apiKey, filename: apiKey, loaded: true,
|
|
45
|
+
exports: {
|
|
46
|
+
ApiError: class ApiError extends Error {},
|
|
47
|
+
async request(_base, path, body) {
|
|
48
|
+
onRequest(path, body);
|
|
49
|
+
const r = responses[callIndex] ?? { success: true, result: { columns: [], rows: [] } };
|
|
50
|
+
callIndex++;
|
|
51
|
+
return r;
|
|
52
|
+
},
|
|
53
|
+
setClientContext() {},
|
|
54
|
+
getClientContext() { return null; },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const sessKey = require.resolve("./_session");
|
|
59
|
+
require.cache[sessKey] = {
|
|
60
|
+
id: sessKey, filename: sessKey, loaded: true,
|
|
61
|
+
exports: { resolveSession: () => ({ baseUrl: "http://test", token: "t" }) },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const connKey = require.resolve("./_connections");
|
|
65
|
+
require.cache[connKey] = {
|
|
66
|
+
id: connKey, filename: connKey, loaded: true,
|
|
67
|
+
exports: {
|
|
68
|
+
resolveConnectionId: async () => "00000000-0000-0000-0000-000000000001",
|
|
69
|
+
listConnections: async () => [],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const promptsKey = require.resolve("../ui/prompts");
|
|
74
|
+
require.cache[promptsKey] = {
|
|
75
|
+
id: promptsKey, filename: promptsKey, loaded: true,
|
|
76
|
+
exports: {
|
|
77
|
+
confirm: async () => confirmAnswer,
|
|
78
|
+
input: async () => "",
|
|
79
|
+
password: async () => "",
|
|
80
|
+
select: async () => "",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return require("./query");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function captureStdout() {
|
|
88
|
+
let out = "";
|
|
89
|
+
let err = "";
|
|
90
|
+
return {
|
|
91
|
+
stream: { write: (s) => { out += s; } },
|
|
92
|
+
errStream: { write: (s) => { err += s; } },
|
|
93
|
+
out: () => out,
|
|
94
|
+
err: () => err,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("query hits canonical /connections/{id}/query endpoint, not /mcp/query-readonly", async () => {
|
|
99
|
+
const seen = [];
|
|
100
|
+
const query = loadWithStubs({
|
|
101
|
+
onRequest: (path) => seen.push(path),
|
|
102
|
+
responses: [{ success: true, result: { columns: ["x"], rows: [[1]], rowCount: 1 } }],
|
|
103
|
+
});
|
|
104
|
+
const stdout = captureStdout();
|
|
105
|
+
await query.run(opts(["SELECT 1"]), { stdout: stdout.stream });
|
|
106
|
+
assert.equal(seen.length, 1);
|
|
107
|
+
assert.match(seen[0], /\/connections\/00000000-0000-0000-0000-000000000001\/query$/);
|
|
108
|
+
assert.equal(/\/mcp\//.test(seen[0]), false, "must not fall back to deprecated /mcp/ endpoint");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("query --write sets mutationConfirmed=true upfront (no confirm prompt)", async () => {
|
|
112
|
+
const seen = [];
|
|
113
|
+
const query = loadWithStubs({
|
|
114
|
+
onRequest: (path, body) => seen.push(body && body.json),
|
|
115
|
+
responses: [{ success: true, result: { rowCount: 1 } }],
|
|
116
|
+
});
|
|
117
|
+
const stdout = captureStdout();
|
|
118
|
+
await query.run(opts(["UPDATE t SET x=1 WHERE id=1", "--write"]), { stdout: stdout.stream });
|
|
119
|
+
assert.equal(seen.length, 1);
|
|
120
|
+
assert.equal(seen[0].mutationConfirmed, true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("query handles two-step requiresConfirmation: prompts, then re-sends with confirmMutation=true", async () => {
|
|
124
|
+
const seen = [];
|
|
125
|
+
const query = loadWithStubs({
|
|
126
|
+
onRequest: (_path, body) => seen.push(body && body.json),
|
|
127
|
+
responses: [
|
|
128
|
+
// First call: server returns requiresConfirmation
|
|
129
|
+
{
|
|
130
|
+
success: false,
|
|
131
|
+
requiresConfirmation: true,
|
|
132
|
+
message: "This statement will modify the database.",
|
|
133
|
+
queryType: "UPDATE",
|
|
134
|
+
warnings: ["A WHERE clause was detected, but verify the target rows."],
|
|
135
|
+
},
|
|
136
|
+
// Second call after the user confirmed: success
|
|
137
|
+
{ success: true, result: { rowCount: 1 } },
|
|
138
|
+
],
|
|
139
|
+
confirmAnswer: true, // simulate user typing 'y'
|
|
140
|
+
});
|
|
141
|
+
const io = captureStdout();
|
|
142
|
+
await query.run(opts(["UPDATE t SET x=1 WHERE id=1"]), { stdout: io.stream, stderr: io.errStream });
|
|
143
|
+
|
|
144
|
+
assert.equal(seen.length, 2);
|
|
145
|
+
assert.equal(seen[0].mutationConfirmed, false);
|
|
146
|
+
assert.equal(seen[1].mutationConfirmed, true);
|
|
147
|
+
assert.match(io.err(), /This statement will modify the database/);
|
|
148
|
+
assert.match(io.err(), /A WHERE clause was detected/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("query honors a 'no' answer at the confirmation prompt — never sends the second call", async () => {
|
|
152
|
+
const seen = [];
|
|
153
|
+
const query = loadWithStubs({
|
|
154
|
+
onRequest: (_path, body) => seen.push(body && body.json),
|
|
155
|
+
responses: [
|
|
156
|
+
{ success: false, requiresConfirmation: true, message: "ok?", warnings: [] },
|
|
157
|
+
],
|
|
158
|
+
confirmAnswer: false,
|
|
159
|
+
});
|
|
160
|
+
const io = captureStdout();
|
|
161
|
+
await query.run(opts(["UPDATE t SET x=1 WHERE id=1"]), { stdout: io.stream, stderr: io.errStream });
|
|
162
|
+
assert.equal(seen.length, 1, "must not retry when user declined");
|
|
163
|
+
assert.match(io.err(), /Aborted/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("query prints policy block messages with the error code and exits non-zero", async () => {
|
|
167
|
+
const query = loadWithStubs({
|
|
168
|
+
responses: [{
|
|
169
|
+
success: false,
|
|
170
|
+
message: "Only admins can execute DDL or DML from the SQL Editor.",
|
|
171
|
+
errorCode: "EDITOR_MUTATION_FORBIDDEN",
|
|
172
|
+
}],
|
|
173
|
+
});
|
|
174
|
+
const io = captureStdout();
|
|
175
|
+
// process.exitCode is shared state; reset around the test
|
|
176
|
+
const prev = process.exitCode;
|
|
177
|
+
process.exitCode = 0;
|
|
178
|
+
await query.run(opts(["UPDATE t SET x=1"]), { stdout: io.stream, stderr: io.errStream });
|
|
179
|
+
assert.equal(process.exitCode, 1);
|
|
180
|
+
assert.match(io.err(), /Only admins can execute DDL or DML/);
|
|
181
|
+
assert.match(io.err(), /EDITOR_MUTATION_FORBIDDEN/);
|
|
182
|
+
process.exitCode = prev;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("query --json prints the raw response body, including requiresConfirmation flag", async () => {
|
|
186
|
+
// With --json, we should NOT prompt; the caller wants the raw shape.
|
|
187
|
+
// Today's implementation will still prompt — that's a follow-up. For now
|
|
188
|
+
// assert the eventual JSON includes the confirmation marker so a script
|
|
189
|
+
// can react.
|
|
190
|
+
const query = loadWithStubs({
|
|
191
|
+
responses: [{ success: true, result: { columns: ["x"], rows: [[1]], rowCount: 1 } }],
|
|
192
|
+
});
|
|
193
|
+
const io = captureStdout();
|
|
194
|
+
// Put the SQL first so `--json` (a boolean flag) doesn't try to swallow it
|
|
195
|
+
// as its value. This is also how a human types it.
|
|
196
|
+
await query.run(opts(["SELECT 1", "--json"]), { stdout: io.stream, stderr: io.errStream });
|
|
197
|
+
const parsed = JSON.parse(io.out());
|
|
198
|
+
assert.equal(parsed.success, true);
|
|
199
|
+
assert.equal(parsed.result.rowCount, 1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test.after(() => {
|
|
203
|
+
for (const k of [
|
|
204
|
+
require.resolve("../api/client"),
|
|
205
|
+
require.resolve("./_session"),
|
|
206
|
+
require.resolve("./_connections"),
|
|
207
|
+
require.resolve("../ui/prompts"),
|
|
208
|
+
require.resolve("./query"),
|
|
209
|
+
]) {
|
|
210
|
+
delete require.cache[k];
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
void Module;
|