@bytebase/dbhub 0.22.5 → 0.23.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/README.md +4 -0
- package/dist/{chunk-BRXZ5ZQB.js → chunk-BXXHWEQS.js} +1 -1
- package/dist/{chunk-IOKERPF2.js → chunk-COEDRFSK.js} +120 -36
- package/dist/{chunk-C7WEAPX4.js → chunk-JEZZN2YZ.js} +15 -1
- package/dist/index.js +29 -9
- package/dist/{mariadb-ERB4D3RP.js → mariadb-SPFBL46R.js} +22 -3
- package/dist/{mysql-WG6UFALX.js → mysql-MZWCPISB.js} +20 -2
- package/dist/{postgres-EKVOBAH6.js → postgres-BKBCGDZS.js} +26 -4
- package/dist/{registry-L3ZIOUF2.js → registry-ZME64VB2.js} +2 -2
- package/dist/{sqlite-2GBIF3MQ.js → sqlite-CQ4LCBB6.js} +18 -4
- package/dist/{sqlserver-5TFVBCMA.js → sqlserver-65AGCEJX.js} +96 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -80,6 +80,10 @@ docker run --rm --init \
|
|
|
80
80
|
npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
**MCP Bundle (one-click install):**
|
|
84
|
+
|
|
85
|
+
Download `dbhub-<version>.mcpb` from the [latest release](https://github.com/bytebase/dbhub/releases/latest) and install it in any [MCPB-compatible client](https://github.com/modelcontextprotocol/mcpb) — Claude Desktop (double-click, or drag into Settings → Extensions), Claude Code, or MCP for Windows — then enter your database connection string. The bundle runs locally over stdio, is **read-only by design** (writes are rejected and the database session is set to read-only at the engine level), and needs no remote endpoint or OAuth setup — ideal for giving non-technical teammates curated, read-only database access. Pair it with a least-privilege, read-only database account. See the [MCP Bundle guide](https://dbhub.ai/mcpb) for details and for packaging your own bundle.
|
|
86
|
+
|
|
83
87
|
**Demo Mode:**
|
|
84
88
|
|
|
85
89
|
```bash
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getDefaultPortForType,
|
|
9
9
|
parseConnectionInfoFromDSN,
|
|
10
10
|
stripCommentsAndStrings
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
12
12
|
|
|
13
13
|
// src/tools/builtin-tools.ts
|
|
14
14
|
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
@@ -68,7 +68,8 @@ function findDefaultSSHKey() {
|
|
|
68
68
|
}
|
|
69
69
|
return void 0;
|
|
70
70
|
}
|
|
71
|
-
function parseSSHConfig(hostAlias, configPath) {
|
|
71
|
+
function parseSSHConfig(hostAlias, configPath, options = {}) {
|
|
72
|
+
const { requireUser = true } = options;
|
|
72
73
|
const sshConfigPath = resolveSymlink(configPath);
|
|
73
74
|
if (!isFile(sshConfigPath)) {
|
|
74
75
|
return null;
|
|
@@ -77,7 +78,7 @@ function parseSSHConfig(hostAlias, configPath) {
|
|
|
77
78
|
const configContent = readFileSync(sshConfigPath, "utf8");
|
|
78
79
|
const config = SSHConfig.parse(configContent);
|
|
79
80
|
const hostConfig = config.compute(hostAlias);
|
|
80
|
-
if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
|
|
81
|
+
if (!hostConfig || !hostConfig.HostName && !hostConfig.User && !hostConfig.Port && !hostConfig.IdentityFile && !hostConfig.ProxyJump) {
|
|
81
82
|
return null;
|
|
82
83
|
}
|
|
83
84
|
const sshConfig = {};
|
|
@@ -111,10 +112,10 @@ function parseSSHConfig(hostAlias, configPath) {
|
|
|
111
112
|
if (hostConfig.ProxyCommand) {
|
|
112
113
|
console.error("Warning: ProxyCommand in SSH config is not supported by DBHub. Use ProxyJump instead.");
|
|
113
114
|
}
|
|
114
|
-
if (!sshConfig.host || !sshConfig.username) {
|
|
115
|
+
if (!sshConfig.host || requireUser && !sshConfig.username) {
|
|
115
116
|
return null;
|
|
116
117
|
}
|
|
117
|
-
return sshConfig;
|
|
118
|
+
return requireUser ? sshConfig : sshConfig;
|
|
118
119
|
} catch (error) {
|
|
119
120
|
console.error(`Error parsing SSH config: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
121
|
return null;
|
|
@@ -193,6 +194,45 @@ function parseJumpHosts(proxyJump) {
|
|
|
193
194
|
}
|
|
194
195
|
return proxyJump.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map(parseJumpHost);
|
|
195
196
|
}
|
|
197
|
+
function resolveJumpHosts(proxyJump, configPath, visited = /* @__PURE__ */ new Set()) {
|
|
198
|
+
if (!proxyJump || proxyJump.trim() === "" || proxyJump.toLowerCase() === "none") {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const resolved = [];
|
|
202
|
+
for (const token of proxyJump.split(",").map((s) => s.trim()).filter((s) => s.length > 0)) {
|
|
203
|
+
const hop = parseJumpHost(token);
|
|
204
|
+
if (!looksLikeSSHAlias(hop.host)) {
|
|
205
|
+
resolved.push(hop);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (visited.has(hop.host)) {
|
|
209
|
+
throw new Error(`Cycle detected in SSH ProxyJump chain at alias "${hop.host}"`);
|
|
210
|
+
}
|
|
211
|
+
const aliasConfig = parseSSHConfig(hop.host, configPath, { requireUser: false });
|
|
212
|
+
if (!aliasConfig) {
|
|
213
|
+
resolved.push(hop);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (aliasConfig.proxyJump) {
|
|
217
|
+
resolved.push(...resolveJumpHosts(aliasConfig.proxyJump, configPath, new Set(visited).add(hop.host)));
|
|
218
|
+
}
|
|
219
|
+
resolved.push({
|
|
220
|
+
host: aliasConfig.host,
|
|
221
|
+
// An explicit `:port` on the token wins; otherwise use the alias's Port (default 22).
|
|
222
|
+
port: tokenHasExplicitPort(token) ? hop.port : aliasConfig.port ?? 22,
|
|
223
|
+
// An explicit `user@` on the token wins; otherwise the alias's User.
|
|
224
|
+
username: hop.username ?? aliasConfig.username,
|
|
225
|
+
privateKey: aliasConfig.privateKey,
|
|
226
|
+
passphrase: aliasConfig.passphrase
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
function tokenHasExplicitPort(token) {
|
|
232
|
+
const atIndex = token.indexOf("@");
|
|
233
|
+
const hostPart = atIndex !== -1 ? token.slice(atIndex + 1) : token;
|
|
234
|
+
return hostPart.startsWith("[") ? /\]:\d+$/.test(hostPart) : /:\d+$/.test(hostPart);
|
|
235
|
+
}
|
|
196
236
|
|
|
197
237
|
// src/utils/ssh-tunnel.ts
|
|
198
238
|
var SSHTunnel = class {
|
|
@@ -215,29 +255,8 @@ var SSHTunnel = class {
|
|
|
215
255
|
}
|
|
216
256
|
this.isConnected = true;
|
|
217
257
|
try {
|
|
218
|
-
const jumpHosts = config.proxyJump ? parseJumpHosts(config.proxyJump) : [];
|
|
219
|
-
|
|
220
|
-
if (config.privateKey) {
|
|
221
|
-
try {
|
|
222
|
-
const resolvedKeyPath = resolveSymlink(config.privateKey);
|
|
223
|
-
privateKeyBuffer = readFileSync2(resolvedKeyPath);
|
|
224
|
-
} catch {
|
|
225
|
-
try {
|
|
226
|
-
const decoded = Buffer.from(config.privateKey, "base64");
|
|
227
|
-
const text = decoded.toString("utf8");
|
|
228
|
-
if (text.includes("PRIVATE KEY")) {
|
|
229
|
-
privateKeyBuffer = decoded;
|
|
230
|
-
} else {
|
|
231
|
-
throw new Error(`SSH key is neither a valid file path nor a base64-encoded private key`);
|
|
232
|
-
}
|
|
233
|
-
} catch (decodeError) {
|
|
234
|
-
if (decodeError instanceof Error && decodeError.message.includes("neither a valid file path")) {
|
|
235
|
-
throw decodeError;
|
|
236
|
-
}
|
|
237
|
-
throw new Error(`SSH key is neither a valid file path nor a base64-encoded private key`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
258
|
+
const jumpHosts = config.resolvedJumpHosts ?? (config.proxyJump ? parseJumpHosts(config.proxyJump) : []);
|
|
259
|
+
const privateKeyBuffer = config.privateKey ? this.loadPrivateKey(config.privateKey) : void 0;
|
|
241
260
|
if (!config.password && !privateKeyBuffer) {
|
|
242
261
|
throw new Error("Either password or privateKey must be provided for SSH authentication");
|
|
243
262
|
}
|
|
@@ -248,6 +267,30 @@ var SSHTunnel = class {
|
|
|
248
267
|
throw error;
|
|
249
268
|
}
|
|
250
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Load an SSH private key, supporting both a file path (with symlink resolution)
|
|
272
|
+
* and base64-encoded key content.
|
|
273
|
+
*/
|
|
274
|
+
loadPrivateKey(key) {
|
|
275
|
+
try {
|
|
276
|
+
const resolvedKeyPath = resolveSymlink(key);
|
|
277
|
+
return readFileSync2(resolvedKeyPath);
|
|
278
|
+
} catch {
|
|
279
|
+
try {
|
|
280
|
+
const decoded = Buffer.from(key, "base64");
|
|
281
|
+
const text = decoded.toString("utf8");
|
|
282
|
+
if (text.includes("PRIVATE KEY")) {
|
|
283
|
+
return decoded;
|
|
284
|
+
}
|
|
285
|
+
throw new Error("SSH key is neither a valid file path nor a base64-encoded private key");
|
|
286
|
+
} catch (decodeError) {
|
|
287
|
+
if (decodeError instanceof Error && decodeError.message.includes("neither a valid file path")) {
|
|
288
|
+
throw decodeError;
|
|
289
|
+
}
|
|
290
|
+
throw new Error("SSH key is neither a valid file path nor a base64-encoded private key");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
251
294
|
/**
|
|
252
295
|
* Establish a chain of SSH connections through jump hosts.
|
|
253
296
|
* @returns The final SSH client connected to the target host
|
|
@@ -257,6 +300,9 @@ var SSHTunnel = class {
|
|
|
257
300
|
for (let i = 0; i < jumpHosts.length; i++) {
|
|
258
301
|
const jumpHost = jumpHosts[i];
|
|
259
302
|
const nextHost = i + 1 < jumpHosts.length ? jumpHosts[i + 1] : { host: targetConfig.host, port: targetConfig.port || 22 };
|
|
303
|
+
const hopPrivateKey = jumpHost.privateKey ? this.loadPrivateKey(jumpHost.privateKey) : privateKey;
|
|
304
|
+
const hopPassword = targetConfig.password;
|
|
305
|
+
const hopPassphrase = jumpHost.passphrase ?? targetConfig.passphrase;
|
|
260
306
|
let client = null;
|
|
261
307
|
let forwardStream;
|
|
262
308
|
try {
|
|
@@ -266,9 +312,9 @@ var SSHTunnel = class {
|
|
|
266
312
|
port: jumpHost.port,
|
|
267
313
|
username: jumpHost.username || targetConfig.username
|
|
268
314
|
},
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
315
|
+
hopPassword,
|
|
316
|
+
hopPrivateKey,
|
|
317
|
+
hopPassphrase,
|
|
272
318
|
previousStream,
|
|
273
319
|
`jump host ${i + 1}`,
|
|
274
320
|
targetConfig.keepaliveInterval,
|
|
@@ -1367,6 +1413,30 @@ function validateSourceConfig(source, configPath) {
|
|
|
1367
1413
|
);
|
|
1368
1414
|
}
|
|
1369
1415
|
}
|
|
1416
|
+
if (source.charset !== void 0) {
|
|
1417
|
+
if (source.type !== "mysql" && source.type !== "mariadb") {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`Configuration file ${configPath}: source '${source.id}' has 'charset' but it is only supported for MySQL and MariaDB sources.`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof source.charset !== "string" || source.charset.trim() === "") {
|
|
1423
|
+
throw new Error(
|
|
1424
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid charset '${source.charset}'. Must be a non-empty string naming a character set (e.g. "utf8mb4").`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (source.collation !== void 0) {
|
|
1429
|
+
if (source.type !== "mysql" && source.type !== "mariadb") {
|
|
1430
|
+
throw new Error(
|
|
1431
|
+
`Configuration file ${configPath}: source '${source.id}' has 'collation' but it is only supported for MySQL and MariaDB sources.`
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
if (typeof source.collation !== "string" || source.collation.trim() === "") {
|
|
1435
|
+
throw new Error(
|
|
1436
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid collation '${source.collation}'. Must be a non-empty string naming a collation (e.g. "utf8mb4_0900_ai_ci").`
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1370
1440
|
if (source.readonly !== void 0) {
|
|
1371
1441
|
throw new Error(
|
|
1372
1442
|
`Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
|
|
@@ -1735,13 +1805,23 @@ var ConnectorManager = class {
|
|
|
1735
1805
|
console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
|
|
1736
1806
|
let actualDSN = dsn;
|
|
1737
1807
|
if (source.ssh_host) {
|
|
1808
|
+
const sshConfigPath = getDefaultSSHConfigPath();
|
|
1738
1809
|
let resolvedSSHConfig = null;
|
|
1739
1810
|
if (looksLikeSSHAlias(source.ssh_host)) {
|
|
1740
|
-
const sshConfigPath = getDefaultSSHConfigPath();
|
|
1741
1811
|
console.error(` Resolving SSH config for host '${source.ssh_host}' from: ${sshConfigPath}`);
|
|
1742
1812
|
resolvedSSHConfig = parseSSHConfig(source.ssh_host, sshConfigPath);
|
|
1743
1813
|
}
|
|
1744
1814
|
const username = source.ssh_user || resolvedSSHConfig?.username;
|
|
1815
|
+
const proxyJump = source.ssh_proxy_jump || resolvedSSHConfig?.proxyJump;
|
|
1816
|
+
let resolvedJumpHosts;
|
|
1817
|
+
try {
|
|
1818
|
+
resolvedJumpHosts = proxyJump ? resolveJumpHosts(proxyJump, sshConfigPath) : void 0;
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
if (error && typeof error === "object") {
|
|
1821
|
+
error[TUNNEL_ERROR_MARKER] = true;
|
|
1822
|
+
}
|
|
1823
|
+
throw error;
|
|
1824
|
+
}
|
|
1745
1825
|
const sshConfig = {
|
|
1746
1826
|
host: resolvedSSHConfig?.host || source.ssh_host,
|
|
1747
1827
|
port: source.ssh_port || resolvedSSHConfig?.port || 22,
|
|
@@ -1749,7 +1829,8 @@ var ConnectorManager = class {
|
|
|
1749
1829
|
password: source.ssh_password,
|
|
1750
1830
|
privateKey: source.ssh_key || resolvedSSHConfig?.privateKey,
|
|
1751
1831
|
passphrase: source.ssh_passphrase,
|
|
1752
|
-
proxyJump
|
|
1832
|
+
proxyJump,
|
|
1833
|
+
resolvedJumpHosts,
|
|
1753
1834
|
keepaliveInterval: source.ssh_keepalive_interval,
|
|
1754
1835
|
keepaliveCountMax: source.ssh_keepalive_count_max
|
|
1755
1836
|
};
|
|
@@ -1802,15 +1883,18 @@ var ConnectorManager = class {
|
|
|
1802
1883
|
if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
|
|
1803
1884
|
config.queryTimeoutSeconds = source.query_timeout;
|
|
1804
1885
|
}
|
|
1805
|
-
if (source.readonly !== void 0) {
|
|
1806
|
-
config.readonly = source.readonly;
|
|
1807
|
-
}
|
|
1808
1886
|
if (source.search_path) {
|
|
1809
1887
|
config.searchPath = source.search_path;
|
|
1810
1888
|
}
|
|
1811
1889
|
if (source.timezone) {
|
|
1812
1890
|
config.timezone = source.timezone;
|
|
1813
1891
|
}
|
|
1892
|
+
if (source.charset) {
|
|
1893
|
+
config.charset = source.charset;
|
|
1894
|
+
}
|
|
1895
|
+
if (source.collation) {
|
|
1896
|
+
config.collation = source.collation;
|
|
1897
|
+
}
|
|
1814
1898
|
await connector.connect(actualDSN, source.init_script, config);
|
|
1815
1899
|
this.connectors.set(sourceId, connector);
|
|
1816
1900
|
if (!this.sourceIds.includes(sourceId)) {
|
|
@@ -267,6 +267,20 @@ function scanSingleLineComment(sql, i) {
|
|
|
267
267
|
}
|
|
268
268
|
return { type: TokenType.Comment, end: j };
|
|
269
269
|
}
|
|
270
|
+
function scanSingleLineCommentMySQL(sql, i) {
|
|
271
|
+
if (sql[i] !== "-" || sql[i + 1] !== "-") {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const next = sql[i + 2];
|
|
275
|
+
if (next !== void 0 && next.charCodeAt(0) > 32 && next.charCodeAt(0) !== 127) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
let j = i;
|
|
279
|
+
while (j < sql.length && sql[j] !== "\n") {
|
|
280
|
+
j++;
|
|
281
|
+
}
|
|
282
|
+
return { type: TokenType.Comment, end: j };
|
|
283
|
+
}
|
|
270
284
|
function scanMultiLineComment(sql, i) {
|
|
271
285
|
if (sql[i] !== "/" || sql[i + 1] !== "*") {
|
|
272
286
|
return null;
|
|
@@ -405,7 +419,7 @@ function scanTokenPostgres(sql, i) {
|
|
|
405
419
|
return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
|
|
406
420
|
}
|
|
407
421
|
function scanTokenMySQL(sql, i) {
|
|
408
|
-
return
|
|
422
|
+
return scanSingleLineCommentMySQL(sql, i) ?? scanMultiLineCommentMySQL(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
|
|
409
423
|
}
|
|
410
424
|
function scanTokenSQLite(sql, i) {
|
|
411
425
|
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
resolveSourceConfigs,
|
|
16
16
|
resolveTomlConfigPath,
|
|
17
17
|
resolveTransport
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-COEDRFSK.js";
|
|
19
19
|
import {
|
|
20
20
|
loadConnectors
|
|
21
21
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
parseConnectionInfoFromDSN,
|
|
30
30
|
splitSQLStatements,
|
|
31
31
|
stripCommentsAndStrings
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
33
33
|
|
|
34
34
|
// src/server.ts
|
|
35
35
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -126,14 +126,25 @@ var mutatingPatternWithReplace = new RegExp(
|
|
|
126
126
|
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
|
|
127
127
|
"i"
|
|
128
128
|
);
|
|
129
|
+
var mutatingPatternSqlServer = new RegExp(
|
|
130
|
+
`\\b(?:${[...mutatingKeywords, "execute", "exec", "sp_executesql", "xp_cmdshell"].join("|")})\\b`,
|
|
131
|
+
"i"
|
|
132
|
+
);
|
|
129
133
|
var mutatingPatterns = {
|
|
130
134
|
postgres: mutatingPattern,
|
|
131
135
|
mysql: mutatingPatternWithReplace,
|
|
132
136
|
mariadb: mutatingPatternWithReplace,
|
|
133
137
|
sqlite: mutatingPatternWithReplace,
|
|
134
|
-
sqlserver:
|
|
138
|
+
sqlserver: mutatingPatternSqlServer
|
|
135
139
|
};
|
|
136
140
|
var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
|
|
141
|
+
var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
|
|
142
|
+
"table_info",
|
|
143
|
+
"index_info",
|
|
144
|
+
"index_list",
|
|
145
|
+
"foreign_key_list"
|
|
146
|
+
]);
|
|
147
|
+
var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
|
|
137
148
|
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
|
|
138
149
|
function isReadOnlySQL(sql, connectorType) {
|
|
139
150
|
return checkReadOnly(
|
|
@@ -156,6 +167,15 @@ function checkReadOnly(cleanedSQL, connectorType) {
|
|
|
156
167
|
return false;
|
|
157
168
|
}
|
|
158
169
|
}
|
|
170
|
+
if (firstWord === "pragma" && connectorType === "sqlite") {
|
|
171
|
+
if (cleanedSQL.includes("=")) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
|
|
175
|
+
if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
159
179
|
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
|
|
160
180
|
return false;
|
|
161
181
|
}
|
|
@@ -1656,7 +1676,7 @@ See documentation for more details on configuring database connections.
|
|
|
1656
1676
|
const sources = sourceConfigsData.sources;
|
|
1657
1677
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1658
1678
|
await connectorManager.connectWithSources(sources);
|
|
1659
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1679
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZME64VB2.js");
|
|
1660
1680
|
initializeToolRegistry2({
|
|
1661
1681
|
sources: sourceConfigsData.sources,
|
|
1662
1682
|
tools: sourceConfigsData.tools
|
|
@@ -1810,11 +1830,11 @@ See documentation for more details on configuring database connections.
|
|
|
1810
1830
|
|
|
1811
1831
|
// src/index.ts
|
|
1812
1832
|
var connectorModules = [
|
|
1813
|
-
{ load: () => import("./postgres-
|
|
1814
|
-
{ load: () => import("./sqlserver-
|
|
1815
|
-
{ load: () => import("./sqlite-
|
|
1816
|
-
{ load: () => import("./mysql-
|
|
1817
|
-
{ load: () => import("./mariadb-
|
|
1833
|
+
{ load: () => import("./postgres-BKBCGDZS.js"), name: "PostgreSQL", driver: "pg" },
|
|
1834
|
+
{ load: () => import("./sqlserver-65AGCEJX.js"), name: "SQL Server", driver: "mssql" },
|
|
1835
|
+
{ load: () => import("./sqlite-CQ4LCBB6.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1836
|
+
{ load: () => import("./mysql-MZWCPISB.js"), name: "MySQL", driver: "mysql2" },
|
|
1837
|
+
{ load: () => import("./mariadb-SPFBL46R.js"), name: "MariaDB", driver: "mariadb" }
|
|
1818
1838
|
];
|
|
1819
1839
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1820
1840
|
console.error("Fatal error:", error);
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
} from "./chunk-JFWX35TB.js";
|
|
8
8
|
import {
|
|
9
9
|
SQLRowLimiter
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-BXXHWEQS.js";
|
|
11
11
|
import {
|
|
12
12
|
ConnectorRegistry,
|
|
13
13
|
SafeURL,
|
|
14
14
|
obfuscateDSNPassword,
|
|
15
15
|
splitSQLStatements
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
17
17
|
|
|
18
18
|
// src/connectors/mariadb/index.ts
|
|
19
19
|
import * as mariadb from "mariadb";
|
|
@@ -50,7 +50,14 @@ Expected: ${expectedFormat}`
|
|
|
50
50
|
// Controls how the driver interprets DATETIME values ("Z", "local", or "±HH:MM").
|
|
51
51
|
...config?.timezone !== void 0 && {
|
|
52
52
|
timezone: config.timezone
|
|
53
|
-
}
|
|
53
|
+
},
|
|
54
|
+
// Connection character set (e.g. "utf8mb4") / collation (e.g.
|
|
55
|
+
// "utf8mb4_general_ci"). The mariadb driver exposes both as distinct
|
|
56
|
+
// options, but a configured collation is authoritative (it implies its
|
|
57
|
+
// character set) and the driver ignores `collation` when `charset` is also
|
|
58
|
+
// passed. So forward the collation when present, otherwise the charset
|
|
59
|
+
// (which uses that character set's default collation).
|
|
60
|
+
...config?.collation !== void 0 ? { collation: config.collation } : config?.charset !== void 0 ? { charset: config.charset } : {}
|
|
54
61
|
};
|
|
55
62
|
url.forEachSearchParam((value, key) => {
|
|
56
63
|
if (key === "sslmode") {
|
|
@@ -466,6 +473,9 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
466
473
|
}
|
|
467
474
|
const conn = await this.pool.getConnection();
|
|
468
475
|
try {
|
|
476
|
+
if (options.readonly) {
|
|
477
|
+
await conn.query("START TRANSACTION READ ONLY");
|
|
478
|
+
}
|
|
469
479
|
let processedSQL = sql;
|
|
470
480
|
if (options.maxRows) {
|
|
471
481
|
const statements = splitSQLStatements(sql, "mariadb");
|
|
@@ -492,8 +502,17 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
492
502
|
}
|
|
493
503
|
const rows = parseQueryResults(results);
|
|
494
504
|
const rowCount = extractAffectedRows(results);
|
|
505
|
+
if (options.readonly) {
|
|
506
|
+
await conn.query("COMMIT");
|
|
507
|
+
}
|
|
495
508
|
return { rows, rowCount };
|
|
496
509
|
} catch (error) {
|
|
510
|
+
if (options.readonly) {
|
|
511
|
+
try {
|
|
512
|
+
await conn.query("ROLLBACK");
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
497
516
|
console.error("Error executing query:", error);
|
|
498
517
|
throw error;
|
|
499
518
|
} finally {
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
} from "./chunk-JFWX35TB.js";
|
|
8
8
|
import {
|
|
9
9
|
SQLRowLimiter
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-BXXHWEQS.js";
|
|
11
11
|
import {
|
|
12
12
|
ConnectorRegistry,
|
|
13
13
|
SafeURL,
|
|
14
14
|
obfuscateDSNPassword,
|
|
15
15
|
splitSQLStatements
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
17
17
|
|
|
18
18
|
// src/connectors/mysql/index.ts
|
|
19
19
|
import mysql from "mysql2/promise";
|
|
@@ -21,6 +21,8 @@ var MySQLDSNParser = class {
|
|
|
21
21
|
async parse(dsn, config) {
|
|
22
22
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
23
23
|
const timezone = config?.timezone;
|
|
24
|
+
const charset = config?.charset;
|
|
25
|
+
const collation = config?.collation;
|
|
24
26
|
if (!this.isValidDSN(dsn)) {
|
|
25
27
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
26
28
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -61,6 +63,10 @@ Expected: ${expectedFormat}`
|
|
|
61
63
|
if (timezone !== void 0) {
|
|
62
64
|
config2.timezone = timezone;
|
|
63
65
|
}
|
|
66
|
+
const charsetOrCollation = collation ?? charset;
|
|
67
|
+
if (charsetOrCollation !== void 0) {
|
|
68
|
+
config2.charset = charsetOrCollation;
|
|
69
|
+
}
|
|
64
70
|
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
65
71
|
config2.authPlugins = {
|
|
66
72
|
mysql_clear_password: () => () => {
|
|
@@ -472,6 +478,9 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
472
478
|
}
|
|
473
479
|
const conn = await this.pool.getConnection();
|
|
474
480
|
try {
|
|
481
|
+
if (options.readonly) {
|
|
482
|
+
await conn.query("START TRANSACTION READ ONLY");
|
|
483
|
+
}
|
|
475
484
|
let processedSQL = sql;
|
|
476
485
|
if (options.maxRows) {
|
|
477
486
|
const statements = splitSQLStatements(sql, "mysql");
|
|
@@ -499,8 +508,17 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
499
508
|
const [firstResult] = results;
|
|
500
509
|
const rows = parseQueryResults(firstResult);
|
|
501
510
|
const rowCount = extractAffectedRows(firstResult);
|
|
511
|
+
if (options.readonly) {
|
|
512
|
+
await conn.query("COMMIT");
|
|
513
|
+
}
|
|
502
514
|
return { rows, rowCount };
|
|
503
515
|
} catch (error) {
|
|
516
|
+
if (options.readonly) {
|
|
517
|
+
try {
|
|
518
|
+
await conn.query("ROLLBACK");
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
}
|
|
504
522
|
console.error("Error executing query:", error);
|
|
505
523
|
throw error;
|
|
506
524
|
} finally {
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-JFWX35TB.js";
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BXXHWEQS.js";
|
|
7
7
|
import {
|
|
8
8
|
ConnectorRegistry,
|
|
9
9
|
SafeURL,
|
|
10
10
|
obfuscateDSNPassword,
|
|
11
11
|
splitSQLStatements
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
13
13
|
|
|
14
14
|
// src/connectors/postgres/index.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -491,6 +491,25 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
491
491
|
const statements = splitSQLStatements(sql, "postgres");
|
|
492
492
|
if (statements.length === 1) {
|
|
493
493
|
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
494
|
+
if (options.readonly) {
|
|
495
|
+
await client.query("BEGIN READ ONLY");
|
|
496
|
+
try {
|
|
497
|
+
const result2 = parameters && parameters.length > 0 ? await client.query(processedStatement, parameters) : await client.query(processedStatement);
|
|
498
|
+
await client.query("COMMIT");
|
|
499
|
+
return { rows: result2.rows, rowCount: result2.rowCount ?? result2.rows.length };
|
|
500
|
+
} catch (error) {
|
|
501
|
+
try {
|
|
502
|
+
await client.query("ROLLBACK");
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
|
|
506
|
+
console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
|
|
507
|
+
if (parameters && parameters.length > 0) {
|
|
508
|
+
console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
509
|
+
}
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
494
513
|
let result;
|
|
495
514
|
if (parameters && parameters.length > 0) {
|
|
496
515
|
try {
|
|
@@ -511,7 +530,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
511
530
|
}
|
|
512
531
|
let allRows = [];
|
|
513
532
|
let totalRowCount = 0;
|
|
514
|
-
await client.query("BEGIN");
|
|
533
|
+
await client.query(options.readonly ? "BEGIN READ ONLY" : "BEGIN");
|
|
515
534
|
try {
|
|
516
535
|
for (let statement of statements) {
|
|
517
536
|
const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
@@ -525,7 +544,10 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
525
544
|
}
|
|
526
545
|
await client.query("COMMIT");
|
|
527
546
|
} catch (error) {
|
|
528
|
-
|
|
547
|
+
try {
|
|
548
|
+
await client.query("ROLLBACK");
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
529
551
|
throw error;
|
|
530
552
|
}
|
|
531
553
|
return { rows: allRows, rowCount: totalRowCount };
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
ToolRegistry,
|
|
3
3
|
getToolRegistry,
|
|
4
4
|
initializeToolRegistry
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-COEDRFSK.js";
|
|
6
6
|
import "./chunk-WVVMH6FJ.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-JEZZN2YZ.js";
|
|
8
8
|
export {
|
|
9
9
|
ToolRegistry,
|
|
10
10
|
getToolRegistry,
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-JFWX35TB.js";
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BXXHWEQS.js";
|
|
7
7
|
import {
|
|
8
8
|
ConnectorRegistry,
|
|
9
9
|
SafeURL,
|
|
10
10
|
obfuscateDSNPassword,
|
|
11
11
|
splitSQLStatements
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
13
13
|
|
|
14
14
|
// src/connectors/sqlite/suppress-experimental-warning.ts
|
|
15
15
|
var installed = false;
|
|
@@ -319,6 +319,9 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
319
319
|
if (!this.db) {
|
|
320
320
|
throw new Error("Not connected to SQLite database");
|
|
321
321
|
}
|
|
322
|
+
if (options.readonly) {
|
|
323
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
324
|
+
}
|
|
322
325
|
try {
|
|
323
326
|
const statements = splitSQLStatements(sql, "sqlite");
|
|
324
327
|
if (statements.length === 1) {
|
|
@@ -375,19 +378,30 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
375
378
|
}
|
|
376
379
|
let totalChanges = 0;
|
|
377
380
|
for (const statement of writeStatements) {
|
|
381
|
+
if (options.readonly) {
|
|
382
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
383
|
+
}
|
|
378
384
|
const result = this.prepare(statement).run();
|
|
379
385
|
totalChanges += Number(result.changes);
|
|
380
386
|
}
|
|
381
387
|
let allRows = [];
|
|
382
388
|
for (let statement of readStatements) {
|
|
389
|
+
if (options.readonly) {
|
|
390
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
391
|
+
}
|
|
383
392
|
statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
384
393
|
const result = this.prepare(statement).all();
|
|
385
394
|
allRows.push(...result);
|
|
386
395
|
}
|
|
387
396
|
return { rows: allRows, rowCount: totalChanges + allRows.length };
|
|
388
397
|
}
|
|
389
|
-
}
|
|
390
|
-
|
|
398
|
+
} finally {
|
|
399
|
+
if (options.readonly) {
|
|
400
|
+
try {
|
|
401
|
+
this.db.exec("PRAGMA query_only = OFF");
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
391
405
|
}
|
|
392
406
|
}
|
|
393
407
|
};
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "./chunk-WVVMH6FJ.js";
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BXXHWEQS.js";
|
|
7
7
|
import {
|
|
8
8
|
ConnectorRegistry,
|
|
9
9
|
SafeURL,
|
|
10
10
|
obfuscateDSNPassword,
|
|
11
11
|
stripCommentsAndStrings
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
13
13
|
|
|
14
14
|
// src/connectors/sqlserver/index.ts
|
|
15
15
|
import sql from "mssql";
|
|
@@ -469,6 +469,9 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
469
469
|
if (options.maxRows) {
|
|
470
470
|
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
471
471
|
}
|
|
472
|
+
if (options.readonly) {
|
|
473
|
+
return await this.executeReadOnly(processedSQL, parameters);
|
|
474
|
+
}
|
|
472
475
|
const request = this.connection.request();
|
|
473
476
|
const messages = [];
|
|
474
477
|
request.on(
|
|
@@ -525,6 +528,97 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
525
528
|
throw new Error(`Failed to execute query: ${error.message}`);
|
|
526
529
|
}
|
|
527
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Execute a query inside a transaction that always rolls back, preventing
|
|
533
|
+
* any modifications from persisting. SQL Server has no native READ ONLY
|
|
534
|
+
* transaction mode, so this is the defense-in-depth backstop behind the
|
|
535
|
+
* keyword classifier.
|
|
536
|
+
*
|
|
537
|
+
* Because the rollback guard is application-level (not engine-enforced),
|
|
538
|
+
* we reject dangerous keywords in the stripped SQL before opening the
|
|
539
|
+
* transaction:
|
|
540
|
+
* - COMMIT/ROLLBACK: would end the outer transaction, letting writes persist
|
|
541
|
+
* - EXEC/EXECUTE/sp_executesql/xp_cmdshell: dynamic SQL can carry hidden
|
|
542
|
+
* COMMIT/ROLLBACK inside string literals that stripCommentsAndStrings removes
|
|
543
|
+
*/
|
|
544
|
+
async executeReadOnly(processedSQL, parameters) {
|
|
545
|
+
const cleaned = stripCommentsAndStrings(processedSQL, "sqlserver").toLowerCase();
|
|
546
|
+
if (/\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
"Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (/\b(?:exec|execute|sp_executesql|xp_cmdshell)\b/.test(cleaned)) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
"Read-only mode: dynamic SQL execution (EXEC, EXECUTE, sp_executesql, xp_cmdshell) is not allowed"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
const transaction = new sql.Transaction(this.connection);
|
|
557
|
+
await transaction.begin();
|
|
558
|
+
const request = new sql.Request(transaction);
|
|
559
|
+
const messages = [];
|
|
560
|
+
request.on(
|
|
561
|
+
"info",
|
|
562
|
+
(info) => {
|
|
563
|
+
messages.push({
|
|
564
|
+
text: info.message,
|
|
565
|
+
severity: info.class !== void 0 ? String(info.class) : void 0,
|
|
566
|
+
code: info.number,
|
|
567
|
+
line: info.lineNumber
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
if (parameters && parameters.length > 0) {
|
|
572
|
+
parameters.forEach((param, index) => {
|
|
573
|
+
const paramName = `p${index + 1}`;
|
|
574
|
+
if (typeof param === "string") {
|
|
575
|
+
request.input(paramName, sql.VarChar, param);
|
|
576
|
+
} else if (typeof param === "number") {
|
|
577
|
+
if (Number.isInteger(param)) {
|
|
578
|
+
request.input(paramName, sql.Int, param);
|
|
579
|
+
} else {
|
|
580
|
+
request.input(paramName, sql.Float, param);
|
|
581
|
+
}
|
|
582
|
+
} else if (typeof param === "boolean") {
|
|
583
|
+
request.input(paramName, sql.Bit, param);
|
|
584
|
+
} else if (param === null || param === void 0) {
|
|
585
|
+
request.input(paramName, sql.VarChar, param);
|
|
586
|
+
} else if (Array.isArray(param)) {
|
|
587
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
588
|
+
} else {
|
|
589
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
let result;
|
|
594
|
+
let queryFailed = false;
|
|
595
|
+
try {
|
|
596
|
+
result = await request.query(processedSQL);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
queryFailed = true;
|
|
599
|
+
console.error(`[SQL Server executeReadOnly] ERROR: ${error.message}`);
|
|
600
|
+
console.error(`[SQL Server executeReadOnly] SQL: ${processedSQL}`);
|
|
601
|
+
if (parameters && parameters.length > 0) {
|
|
602
|
+
console.error(`[SQL Server executeReadOnly] Parameters: ${JSON.stringify(parameters)}`);
|
|
603
|
+
}
|
|
604
|
+
throw error;
|
|
605
|
+
} finally {
|
|
606
|
+
try {
|
|
607
|
+
await transaction.rollback();
|
|
608
|
+
} catch (rollbackError) {
|
|
609
|
+
if (!queryFailed) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Read-only rollback failed \u2014 data may have been modified: ${rollbackError.message}`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
rows: result.recordset || [],
|
|
618
|
+
rowCount: result.rowsAffected[0] || 0,
|
|
619
|
+
...messages.length > 0 ? { messages } : {}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
528
622
|
/**
|
|
529
623
|
* Return the estimated execution plan for a query using SHOWPLAN_XML.
|
|
530
624
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"mcpName": "io.github.bytebase/dbhub",
|
|
5
5
|
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"build": "pnpm run generate:api-types && tsup && cd frontend && pnpm run build",
|
|
22
22
|
"build:backend": "pnpm run generate:api-types && tsup",
|
|
23
23
|
"build:frontend": "cd frontend && pnpm run build",
|
|
24
|
+
"build:mcpb": "pnpm run build:backend && node scripts/build-mcpb.mjs",
|
|
25
|
+
"test:mcpb": "node scripts/smoke-test-mcpb.mjs",
|
|
24
26
|
"start": "node dist/index.js",
|
|
25
27
|
"dev": "concurrently --kill-others \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
|
|
26
28
|
"dev:backend": "NODE_ENV=development tsx src/index.ts --transport=http",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"pg": "^8.13.3"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
63
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
61
64
|
"@testcontainers/mariadb": "^11.0.3",
|
|
62
65
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
63
66
|
"@testcontainers/mysql": "^11.0.3",
|