@bytebase/dbhub 0.21.2 → 0.22.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/README.md +11 -1
- package/dist/{chunk-7D6RKVK2.js → chunk-MEBWJMLY.js} +105 -2
- package/dist/index.js +160 -29
- package/dist/{mariadb-4IBCPIEV.js → mariadb-DVQM4MMM.js} +30 -2
- package/dist/{mysql-M4INQQ46.js → mysql-NCKXNVZJ.js} +30 -2
- package/dist/{postgres-4PCNQDGV.js → postgres-EKVOBAH6.js} +83 -21
- package/dist/{registry-Y755MTUO.js → registry-FZPB5LRL.js} +1 -1
- package/dist/{sqlite-FSCLCRIH.js → sqlite-2GBIF3MQ.js} +111 -33
- package/dist/{sqlserver-YV3KVJ3B.js → sqlserver-5E7DSDVX.js} +36 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ docker run --rm --init \
|
|
|
74
74
|
--dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
**NPM:**
|
|
77
|
+
**NPM:** (requires Node.js >= 22.5.0)
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
@@ -86,6 +86,14 @@ npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:p
|
|
|
86
86
|
npx @bytebase/dbhub@latest --transport http --port 8080 --demo
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
**Restrict to loopback (recommended for production):**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> The HTTP transport defaults to `--host 0.0.0.0`, exposing DBHub on every network interface. For production, bind to `127.0.0.1` and front DBHub with a reverse proxy (nginx/Caddy) or firewall — DBHub does not authenticate HTTP clients.
|
|
96
|
+
|
|
89
97
|
See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
|
|
90
98
|
|
|
91
99
|
### Multi-Database Setup
|
|
@@ -96,6 +104,8 @@ See [Multi-Database Configuration](https://dbhub.ai/config/toml) for complete se
|
|
|
96
104
|
|
|
97
105
|
## Development
|
|
98
106
|
|
|
107
|
+
Requires Node.js >= 22.5.0 (DBHub uses the built-in `node:sqlite` module).
|
|
108
|
+
|
|
99
109
|
```bash
|
|
100
110
|
# Install dependencies
|
|
101
111
|
pnpm install
|
|
@@ -701,6 +701,38 @@ function resolvePort() {
|
|
|
701
701
|
}
|
|
702
702
|
return { port: 8080, source: "default" };
|
|
703
703
|
}
|
|
704
|
+
function requireFlagValue(flag, args, example) {
|
|
705
|
+
const fail = () => {
|
|
706
|
+
console.error(`ERROR: --${flag} requires a value (e.g., --${flag}=${example}).`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
};
|
|
709
|
+
const rawArgs = process.argv.slice(2);
|
|
710
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
711
|
+
const token = rawArgs[i];
|
|
712
|
+
if (token === `--${flag}`) {
|
|
713
|
+
const next = rawArgs[i + 1];
|
|
714
|
+
if (!next || next.startsWith("--")) fail();
|
|
715
|
+
} else if (token === `--${flag}=`) {
|
|
716
|
+
fail();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (args[flag] === void 0) return void 0;
|
|
720
|
+
const value = args[flag].trim();
|
|
721
|
+
if (!value) fail();
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
function resolveHost() {
|
|
725
|
+
const args = parseCommandLineArgs();
|
|
726
|
+
const cliHost = requireFlagValue("host", args, "127.0.0.1");
|
|
727
|
+
if (cliHost !== void 0) {
|
|
728
|
+
return { host: cliHost, source: "command line argument" };
|
|
729
|
+
}
|
|
730
|
+
const envHost = process.env.DBHUB_HOST?.trim();
|
|
731
|
+
if (envHost) {
|
|
732
|
+
return { host: envHost, source: "environment variable" };
|
|
733
|
+
}
|
|
734
|
+
return { host: "0.0.0.0", source: "default" };
|
|
735
|
+
}
|
|
704
736
|
function redactDSN(dsn) {
|
|
705
737
|
try {
|
|
706
738
|
const url = new URL(dsn);
|
|
@@ -1132,12 +1164,45 @@ function validateSourceConfig(source, configPath) {
|
|
|
1132
1164
|
`Configuration file ${configPath}: source '${source.id}' has sslmode but SQLite does not support SSL. Remove the sslmode field for SQLite sources.`
|
|
1133
1165
|
);
|
|
1134
1166
|
}
|
|
1135
|
-
const validSslModes = ["disable", "require"];
|
|
1167
|
+
const validSslModes = ["disable", "require", "verify-ca", "verify-full"];
|
|
1136
1168
|
if (!validSslModes.includes(source.sslmode)) {
|
|
1137
1169
|
throw new Error(
|
|
1138
1170
|
`Configuration file ${configPath}: source '${source.id}' has invalid sslmode '${source.sslmode}'. Valid values: ${validSslModes.join(", ")}`
|
|
1139
1171
|
);
|
|
1140
1172
|
}
|
|
1173
|
+
if ((source.sslmode === "verify-ca" || source.sslmode === "verify-full") && source.type !== "postgres") {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`Configuration file ${configPath}: source '${source.id}' has sslmode '${source.sslmode}' which is only supported for PostgreSQL. Valid values for ${source.type}: disable, require`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (source.sslrootcert !== void 0) {
|
|
1180
|
+
if (source.sslmode !== "verify-ca" && source.sslmode !== "verify-full") {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`Configuration file ${configPath}: source '${source.id}' has sslrootcert but sslmode is '${source.sslmode ?? "not set"}'. sslrootcert requires sslmode 'verify-ca' or 'verify-full'`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const expandedPath = expandHomeDir(source.sslrootcert);
|
|
1186
|
+
let stats;
|
|
1187
|
+
try {
|
|
1188
|
+
stats = fs2.statSync(expandedPath);
|
|
1189
|
+
} catch {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`Configuration file ${configPath}: source '${source.id}' sslrootcert file not found or not accessible: '${expandedPath}'`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (!stats.isFile()) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`Configuration file ${configPath}: source '${source.id}' sslrootcert path is not a regular file: '${expandedPath}'`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
try {
|
|
1200
|
+
fs2.accessSync(expandedPath, fs2.constants.R_OK);
|
|
1201
|
+
} catch {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Configuration file ${configPath}: source '${source.id}' sslrootcert file is not readable: '${expandedPath}'`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1141
1206
|
}
|
|
1142
1207
|
if (source.authentication !== void 0) {
|
|
1143
1208
|
if (source.type !== "sqlserver") {
|
|
@@ -1186,6 +1251,18 @@ function validateSourceConfig(source, configPath) {
|
|
|
1186
1251
|
);
|
|
1187
1252
|
}
|
|
1188
1253
|
}
|
|
1254
|
+
if (source.timezone !== void 0) {
|
|
1255
|
+
if (source.type !== "mysql" && source.type !== "mariadb") {
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`Configuration file ${configPath}: source '${source.id}' has 'timezone' but it is only supported for MySQL and MariaDB sources.`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (typeof source.timezone !== "string" || !/^(?:local|Z|[+-]\d\d:\d\d)$/.test(source.timezone)) {
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid timezone '${source.timezone}'. Must be "local", "Z" (UTC), or an offset like "+09:00".`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1189
1266
|
if (source.readonly !== void 0) {
|
|
1190
1267
|
throw new Error(
|
|
1191
1268
|
`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.`
|
|
@@ -1203,6 +1280,9 @@ function processSourceConfigs(sources, configPath) {
|
|
|
1203
1280
|
if (processed.ssh_key) {
|
|
1204
1281
|
processed.ssh_key = expandHomeDir(processed.ssh_key);
|
|
1205
1282
|
}
|
|
1283
|
+
if (processed.sslrootcert) {
|
|
1284
|
+
processed.sslrootcert = expandHomeDir(processed.sslrootcert);
|
|
1285
|
+
}
|
|
1206
1286
|
if (processed.type === "sqlite" && processed.database) {
|
|
1207
1287
|
processed.database = expandHomeDir(processed.database);
|
|
1208
1288
|
}
|
|
@@ -1228,6 +1308,18 @@ function processSourceConfigs(sources, configPath) {
|
|
|
1228
1308
|
processed.user = connectionInfo.user;
|
|
1229
1309
|
}
|
|
1230
1310
|
}
|
|
1311
|
+
try {
|
|
1312
|
+
const url = new SafeURL(processed.dsn);
|
|
1313
|
+
const dsnSslmode = url.getSearchParam("sslmode");
|
|
1314
|
+
if (!processed.sslmode && dsnSslmode) {
|
|
1315
|
+
processed.sslmode = dsnSslmode;
|
|
1316
|
+
}
|
|
1317
|
+
const dsnSslrootcert = url.getSearchParam("sslrootcert");
|
|
1318
|
+
if (!processed.sslrootcert && dsnSslrootcert) {
|
|
1319
|
+
processed.sslrootcert = dsnSslrootcert;
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1231
1323
|
}
|
|
1232
1324
|
return processed;
|
|
1233
1325
|
});
|
|
@@ -1310,6 +1402,10 @@ function buildDSNFromSource(source) {
|
|
|
1310
1402
|
if (source.sslmode && source.type !== "sqlite") {
|
|
1311
1403
|
queryParams.push(`sslmode=${source.sslmode}`);
|
|
1312
1404
|
}
|
|
1405
|
+
if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full")) {
|
|
1406
|
+
const expandedCertPath = expandHomeDir(source.sslrootcert);
|
|
1407
|
+
queryParams.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
|
|
1408
|
+
}
|
|
1313
1409
|
if (queryParams.length > 0) {
|
|
1314
1410
|
dsn += `?${queryParams.join("&")}`;
|
|
1315
1411
|
}
|
|
@@ -1511,6 +1607,9 @@ var ConnectorManager = class {
|
|
|
1511
1607
|
if (source.search_path) {
|
|
1512
1608
|
config.searchPath = source.search_path;
|
|
1513
1609
|
}
|
|
1610
|
+
if (source.timezone) {
|
|
1611
|
+
config.timezone = source.timezone;
|
|
1612
|
+
}
|
|
1514
1613
|
await connector.connect(actualDSN, source.init_script, config);
|
|
1515
1614
|
this.connectors.set(sourceId, connector);
|
|
1516
1615
|
if (!this.sourceIds.includes(sourceId)) {
|
|
@@ -1741,7 +1840,10 @@ var ConnectorManager = class {
|
|
|
1741
1840
|
region: source.aws_region
|
|
1742
1841
|
});
|
|
1743
1842
|
const queryParams = new Map(parsed.searchParams);
|
|
1744
|
-
queryParams.
|
|
1843
|
+
const currentSslMode = queryParams.get("sslmode");
|
|
1844
|
+
if (currentSslMode !== "verify-ca" && currentSslMode !== "verify-full") {
|
|
1845
|
+
queryParams.set("sslmode", "require");
|
|
1846
|
+
}
|
|
1745
1847
|
const protocol = parsed.protocol.endsWith(":") ? parsed.protocol.slice(0, -1) : parsed.protocol;
|
|
1746
1848
|
const encodedUser = encodeURIComponent(username);
|
|
1747
1849
|
const encodedToken = encodeURIComponent(token);
|
|
@@ -2082,6 +2184,7 @@ export {
|
|
|
2082
2184
|
isDemoMode,
|
|
2083
2185
|
resolveTransport,
|
|
2084
2186
|
resolvePort,
|
|
2187
|
+
resolveHost,
|
|
2085
2188
|
resolveSourceConfigs,
|
|
2086
2189
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
2087
2190
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
package/dist/index.js
CHANGED
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
isDemoMode,
|
|
9
9
|
loadTomlConfig,
|
|
10
10
|
mapArgumentsToArray,
|
|
11
|
+
resolveHost,
|
|
11
12
|
resolvePort,
|
|
12
13
|
resolveSourceConfigs,
|
|
13
14
|
resolveTomlConfigPath,
|
|
14
15
|
resolveTransport
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-MEBWJMLY.js";
|
|
16
17
|
import {
|
|
17
18
|
loadConnectors
|
|
18
19
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -33,6 +34,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
33
34
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
34
35
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
35
36
|
import express from "express";
|
|
37
|
+
import http from "http";
|
|
36
38
|
import path from "path";
|
|
37
39
|
import { readFileSync } from "fs";
|
|
38
40
|
import { fileURLToPath } from "url";
|
|
@@ -281,7 +283,8 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
281
283
|
const responseData = {
|
|
282
284
|
rows: result.rows,
|
|
283
285
|
count: result.rowCount,
|
|
284
|
-
source_id: effectiveSourceId
|
|
286
|
+
source_id: effectiveSourceId,
|
|
287
|
+
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
285
288
|
};
|
|
286
289
|
return createToolSuccessResponse(responseData);
|
|
287
290
|
} catch (error) {
|
|
@@ -307,7 +310,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
307
310
|
// src/tools/search-objects.ts
|
|
308
311
|
import { z as z2 } from "zod";
|
|
309
312
|
var searchDatabaseObjectsSchema = {
|
|
310
|
-
object_type: z2.enum(["schema", "table", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
313
|
+
object_type: z2.enum(["schema", "table", "view", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
311
314
|
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
|
|
312
315
|
schema: z2.string().optional().describe("Filter to schema"),
|
|
313
316
|
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
@@ -451,6 +454,77 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
451
454
|
}
|
|
452
455
|
return results;
|
|
453
456
|
}
|
|
457
|
+
async function searchViews(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
458
|
+
const regex = likePatternToRegex(pattern);
|
|
459
|
+
const results = [];
|
|
460
|
+
let schemasToSearch;
|
|
461
|
+
if (schemaFilter) {
|
|
462
|
+
schemasToSearch = [schemaFilter];
|
|
463
|
+
} else {
|
|
464
|
+
schemasToSearch = await connector.getSchemas();
|
|
465
|
+
}
|
|
466
|
+
for (const schemaName of schemasToSearch) {
|
|
467
|
+
if (results.length >= limit) break;
|
|
468
|
+
try {
|
|
469
|
+
const views = await connector.getViews(schemaName);
|
|
470
|
+
const matched = views.filter((view) => regex.test(view));
|
|
471
|
+
for (const viewName of matched) {
|
|
472
|
+
if (results.length >= limit) break;
|
|
473
|
+
if (detailLevel === "names") {
|
|
474
|
+
results.push({
|
|
475
|
+
name: viewName,
|
|
476
|
+
schema: schemaName
|
|
477
|
+
});
|
|
478
|
+
} else if (detailLevel === "summary") {
|
|
479
|
+
try {
|
|
480
|
+
const columns = await connector.getTableSchema(viewName, schemaName);
|
|
481
|
+
const comment = await getTableComment(connector, viewName, schemaName);
|
|
482
|
+
results.push({
|
|
483
|
+
name: viewName,
|
|
484
|
+
schema: schemaName,
|
|
485
|
+
column_count: columns.length,
|
|
486
|
+
...comment ? { comment } : {}
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
results.push({
|
|
490
|
+
name: viewName,
|
|
491
|
+
schema: schemaName,
|
|
492
|
+
column_count: null
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
try {
|
|
497
|
+
const columns = await connector.getTableSchema(viewName, schemaName);
|
|
498
|
+
const comment = await getTableComment(connector, viewName, schemaName);
|
|
499
|
+
results.push({
|
|
500
|
+
name: viewName,
|
|
501
|
+
schema: schemaName,
|
|
502
|
+
column_count: columns.length,
|
|
503
|
+
...comment ? { comment } : {},
|
|
504
|
+
columns: columns.map((col) => ({
|
|
505
|
+
name: col.column_name,
|
|
506
|
+
type: col.data_type,
|
|
507
|
+
nullable: col.is_nullable === "YES",
|
|
508
|
+
default: col.column_default,
|
|
509
|
+
...col.description ? { description: col.description } : {}
|
|
510
|
+
})),
|
|
511
|
+
indexes: []
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
results.push({
|
|
515
|
+
name: viewName,
|
|
516
|
+
schema: schemaName,
|
|
517
|
+
error: `Unable to fetch full details: ${error.message}`
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return results;
|
|
527
|
+
}
|
|
454
528
|
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
455
529
|
const regex = likePatternToRegex(pattern);
|
|
456
530
|
const results = [];
|
|
@@ -467,7 +541,11 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
467
541
|
if (tableFilter) {
|
|
468
542
|
tablesToSearch = [tableFilter];
|
|
469
543
|
} else {
|
|
470
|
-
|
|
544
|
+
const [tables, views] = await Promise.all([
|
|
545
|
+
connector.getTables(schemaName),
|
|
546
|
+
Promise.resolve(connector.getViews(schemaName)).catch(() => [])
|
|
547
|
+
]);
|
|
548
|
+
tablesToSearch = [...tables, ...views ?? []];
|
|
471
549
|
}
|
|
472
550
|
for (const tableName of tablesToSearch) {
|
|
473
551
|
if (results.length >= limit) break;
|
|
@@ -649,6 +727,9 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
649
727
|
case "table":
|
|
650
728
|
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
651
729
|
break;
|
|
730
|
+
case "view":
|
|
731
|
+
results = await searchViews(connector, pattern, schema, detail_level, limit);
|
|
732
|
+
break;
|
|
652
733
|
case "column":
|
|
653
734
|
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
654
735
|
break;
|
|
@@ -708,6 +789,11 @@ function normalizeSourceId(id) {
|
|
|
708
789
|
}
|
|
709
790
|
|
|
710
791
|
// src/utils/tool-metadata.ts
|
|
792
|
+
function buildSourceDescriptionPrefix(description) {
|
|
793
|
+
const trimmed = description?.trim() ?? "";
|
|
794
|
+
if (!trimmed) return "";
|
|
795
|
+
return /[.!?:]$/.test(trimmed) ? `${trimmed} ` : `${trimmed}. `;
|
|
796
|
+
}
|
|
711
797
|
function zodToParameters(schema) {
|
|
712
798
|
const parameters = [];
|
|
713
799
|
for (const [key, zodType] of Object.entries(schema)) {
|
|
@@ -747,9 +833,10 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
747
833
|
};
|
|
748
834
|
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
749
835
|
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
836
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
750
837
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
751
838
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
752
|
-
const description = isSingleSource ?
|
|
839
|
+
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
753
840
|
const isReadonly = executeOptions.readonly === true;
|
|
754
841
|
const annotations = {
|
|
755
842
|
title,
|
|
@@ -776,7 +863,8 @@ function getSearchObjectsMetadata(sourceId) {
|
|
|
776
863
|
const isSingleSource = sourceIds.length === 1;
|
|
777
864
|
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
778
865
|
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
779
|
-
const
|
|
866
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
867
|
+
const description = isSingleSource ? `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
|
|
780
868
|
return {
|
|
781
869
|
name: toolName,
|
|
782
870
|
description,
|
|
@@ -1369,6 +1457,43 @@ Detected change in ${configPath}, reloading configuration...`);
|
|
|
1369
1457
|
};
|
|
1370
1458
|
}
|
|
1371
1459
|
|
|
1460
|
+
// src/utils/cross-origin.ts
|
|
1461
|
+
var INVALID_HOST_CHARS = /[\s/\\@?#]/;
|
|
1462
|
+
function validateOrigin(originHeader, hostHeader) {
|
|
1463
|
+
if (originHeader === void 0) return { ok: true };
|
|
1464
|
+
const trimmedOrigin = originHeader.trim();
|
|
1465
|
+
if (!trimmedOrigin) {
|
|
1466
|
+
return { ok: false, status: 400, message: "Malformed Origin header" };
|
|
1467
|
+
}
|
|
1468
|
+
const trimmedHost = (hostHeader ?? "").trim();
|
|
1469
|
+
if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
|
|
1470
|
+
return { ok: false, status: 400, message: "Malformed Host header" };
|
|
1471
|
+
}
|
|
1472
|
+
let originHostname;
|
|
1473
|
+
try {
|
|
1474
|
+
originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
|
|
1475
|
+
} catch {
|
|
1476
|
+
return { ok: false, status: 400, message: "Malformed Origin header" };
|
|
1477
|
+
}
|
|
1478
|
+
let hostname;
|
|
1479
|
+
try {
|
|
1480
|
+
hostname = new URL(`http://${trimmedHost}`).hostname.toLowerCase();
|
|
1481
|
+
} catch {
|
|
1482
|
+
return { ok: false, status: 400, message: "Malformed Host header" };
|
|
1483
|
+
}
|
|
1484
|
+
if (!hostname) {
|
|
1485
|
+
return { ok: false, status: 400, message: "Malformed Host header" };
|
|
1486
|
+
}
|
|
1487
|
+
if (originHostname !== hostname) {
|
|
1488
|
+
return {
|
|
1489
|
+
ok: false,
|
|
1490
|
+
status: 403,
|
|
1491
|
+
message: "Origin does not match Host header"
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
return { ok: true };
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1372
1497
|
// src/server.ts
|
|
1373
1498
|
var __filename = fileURLToPath(import.meta.url);
|
|
1374
1499
|
var __dirname = path.dirname(__filename);
|
|
@@ -1421,7 +1546,7 @@ See documentation for more details on configuring database connections.
|
|
|
1421
1546
|
const sources = sourceConfigsData.sources;
|
|
1422
1547
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1423
1548
|
await connectorManager.connectWithSources(sources);
|
|
1424
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1549
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-FZPB5LRL.js");
|
|
1425
1550
|
initializeToolRegistry2({
|
|
1426
1551
|
sources: sourceConfigsData.sources,
|
|
1427
1552
|
tools: sourceConfigsData.tools
|
|
@@ -1441,6 +1566,7 @@ See documentation for more details on configuring database connections.
|
|
|
1441
1566
|
};
|
|
1442
1567
|
const transportData = resolveTransport();
|
|
1443
1568
|
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1569
|
+
const host = transportData.type === "http" ? resolveHost().host : null;
|
|
1444
1570
|
const activeModes = [];
|
|
1445
1571
|
const modeDescriptions = [];
|
|
1446
1572
|
const isDemo = isDemoMode();
|
|
@@ -1466,19 +1592,12 @@ See documentation for more details on configuring database connections.
|
|
|
1466
1592
|
app.use(express.json());
|
|
1467
1593
|
app.use((req, res, next) => {
|
|
1468
1594
|
const origin = req.headers.origin;
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
error: "Forbidden",
|
|
1476
|
-
message: "Origin does not match Host header (DNS rebinding protection)"
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
} catch {
|
|
1480
|
-
return res.status(400).json({ error: "Bad Request", message: "Malformed Origin header" });
|
|
1481
|
-
}
|
|
1595
|
+
const result = validateOrigin(origin, req.headers.host);
|
|
1596
|
+
if (!result.ok) {
|
|
1597
|
+
return res.status(result.status).json({
|
|
1598
|
+
error: result.status === 400 ? "Bad Request" : "Forbidden",
|
|
1599
|
+
message: result.message
|
|
1600
|
+
});
|
|
1482
1601
|
}
|
|
1483
1602
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1484
1603
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
@@ -1526,16 +1645,28 @@ See documentation for more details on configuring database connections.
|
|
|
1526
1645
|
res.sendFile(path.join(frontendPath, "index.html"));
|
|
1527
1646
|
});
|
|
1528
1647
|
}
|
|
1529
|
-
|
|
1648
|
+
const httpServer = http.createServer(app);
|
|
1649
|
+
httpServer.on("error", (err) => {
|
|
1650
|
+
const displayHost = host.includes(":") ? `[${host}]` : host;
|
|
1651
|
+
console.error(`Failed to bind HTTP server to ${displayHost}:${port}: ${err.message}`);
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
});
|
|
1654
|
+
httpServer.listen(port, host, () => {
|
|
1655
|
+
const address = httpServer.address();
|
|
1656
|
+
const boundHost = typeof address === "object" && address ? address.address : host;
|
|
1657
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
1658
|
+
const displayHost = boundHost.includes(":") ? `[${boundHost}]` : boundHost;
|
|
1659
|
+
const userHost = boundHost === "0.0.0.0" || boundHost === "::" ? "localhost" : displayHost;
|
|
1660
|
+
console.error(`HTTP server listening on ${displayHost}:${boundPort}`);
|
|
1530
1661
|
if (process.env.NODE_ENV === "development") {
|
|
1531
1662
|
console.error("Development mode detected!");
|
|
1532
1663
|
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
1533
|
-
console.error(
|
|
1664
|
+
console.error(` Backend API: http://localhost:${boundPort}`);
|
|
1534
1665
|
console.error("");
|
|
1535
1666
|
} else {
|
|
1536
|
-
console.error(`Workbench at http
|
|
1667
|
+
console.error(`Workbench at http://${userHost}:${boundPort}/`);
|
|
1537
1668
|
}
|
|
1538
|
-
console.error(`MCP server endpoint at http
|
|
1669
|
+
console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
|
|
1539
1670
|
});
|
|
1540
1671
|
} else {
|
|
1541
1672
|
const server = createServer();
|
|
@@ -1563,11 +1694,11 @@ See documentation for more details on configuring database connections.
|
|
|
1563
1694
|
|
|
1564
1695
|
// src/index.ts
|
|
1565
1696
|
var connectorModules = [
|
|
1566
|
-
{ load: () => import("./postgres-
|
|
1567
|
-
{ load: () => import("./sqlserver-
|
|
1568
|
-
{ load: () => import("./sqlite-
|
|
1569
|
-
{ load: () => import("./mysql-
|
|
1570
|
-
{ load: () => import("./mariadb-
|
|
1697
|
+
{ load: () => import("./postgres-EKVOBAH6.js"), name: "PostgreSQL", driver: "pg" },
|
|
1698
|
+
{ load: () => import("./sqlserver-5E7DSDVX.js"), name: "SQL Server", driver: "mssql" },
|
|
1699
|
+
{ load: () => import("./sqlite-2GBIF3MQ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1700
|
+
{ load: () => import("./mysql-NCKXNVZJ.js"), name: "MySQL", driver: "mysql2" },
|
|
1701
|
+
{ load: () => import("./mariadb-DVQM4MMM.js"), name: "MariaDB", driver: "mariadb" }
|
|
1571
1702
|
];
|
|
1572
1703
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1573
1704
|
console.error("Fatal error:", error);
|
|
@@ -46,6 +46,10 @@ Expected: ${expectedFormat}`
|
|
|
46
46
|
},
|
|
47
47
|
...queryTimeoutSeconds !== void 0 && {
|
|
48
48
|
queryTimeout: queryTimeoutSeconds * 1e3
|
|
49
|
+
},
|
|
50
|
+
// Controls how the driver interprets DATETIME values ("Z", "local", or "±HH:MM").
|
|
51
|
+
...config?.timezone !== void 0 && {
|
|
52
|
+
timezone: config.timezone
|
|
49
53
|
}
|
|
50
54
|
};
|
|
51
55
|
url.forEachSearchParam((value, key) => {
|
|
@@ -138,9 +142,10 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
138
142
|
const queryParams = schema ? [schema] : [];
|
|
139
143
|
const rows = await this.pool.query(
|
|
140
144
|
`
|
|
141
|
-
SELECT TABLE_NAME
|
|
142
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
145
|
+
SELECT TABLE_NAME
|
|
146
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
143
147
|
${schemaClause}
|
|
148
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
144
149
|
ORDER BY TABLE_NAME
|
|
145
150
|
`,
|
|
146
151
|
queryParams
|
|
@@ -151,6 +156,29 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
151
156
|
throw error;
|
|
152
157
|
}
|
|
153
158
|
}
|
|
159
|
+
async getViews(schema) {
|
|
160
|
+
if (!this.pool) {
|
|
161
|
+
throw new Error("Not connected to database");
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
165
|
+
const queryParams = schema ? [schema] : [];
|
|
166
|
+
const rows = await this.pool.query(
|
|
167
|
+
`
|
|
168
|
+
SELECT TABLE_NAME
|
|
169
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
170
|
+
${schemaClause}
|
|
171
|
+
AND TABLE_TYPE = 'VIEW'
|
|
172
|
+
ORDER BY TABLE_NAME
|
|
173
|
+
`,
|
|
174
|
+
queryParams
|
|
175
|
+
);
|
|
176
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Error getting views:", error);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
154
182
|
async tableExists(tableName, schema) {
|
|
155
183
|
if (!this.pool) {
|
|
156
184
|
throw new Error("Not connected to database");
|
|
@@ -20,6 +20,7 @@ import mysql from "mysql2/promise";
|
|
|
20
20
|
var MySQLDSNParser = class {
|
|
21
21
|
async parse(dsn, config) {
|
|
22
22
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
23
|
+
const timezone = config?.timezone;
|
|
23
24
|
if (!this.isValidDSN(dsn)) {
|
|
24
25
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
25
26
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -57,6 +58,9 @@ Expected: ${expectedFormat}`
|
|
|
57
58
|
if (connectionTimeoutSeconds !== void 0) {
|
|
58
59
|
config2.connectTimeout = connectionTimeoutSeconds * 1e3;
|
|
59
60
|
}
|
|
61
|
+
if (timezone !== void 0) {
|
|
62
|
+
config2.timezone = timezone;
|
|
63
|
+
}
|
|
60
64
|
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
61
65
|
config2.authPlugins = {
|
|
62
66
|
mysql_clear_password: () => () => {
|
|
@@ -144,9 +148,10 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
144
148
|
const queryParams = schema ? [schema] : [];
|
|
145
149
|
const [rows] = await this.pool.query(
|
|
146
150
|
`
|
|
147
|
-
SELECT TABLE_NAME
|
|
148
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
151
|
+
SELECT TABLE_NAME
|
|
152
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
149
153
|
${schemaClause}
|
|
154
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
150
155
|
ORDER BY TABLE_NAME
|
|
151
156
|
`,
|
|
152
157
|
queryParams
|
|
@@ -157,6 +162,29 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
157
162
|
throw error;
|
|
158
163
|
}
|
|
159
164
|
}
|
|
165
|
+
async getViews(schema) {
|
|
166
|
+
if (!this.pool) {
|
|
167
|
+
throw new Error("Not connected to database");
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
171
|
+
const queryParams = schema ? [schema] : [];
|
|
172
|
+
const [rows] = await this.pool.query(
|
|
173
|
+
`
|
|
174
|
+
SELECT TABLE_NAME
|
|
175
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
176
|
+
${schemaClause}
|
|
177
|
+
AND TABLE_TYPE = 'VIEW'
|
|
178
|
+
ORDER BY TABLE_NAME
|
|
179
|
+
`,
|
|
180
|
+
queryParams
|
|
181
|
+
);
|
|
182
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Error getting views:", error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
160
188
|
async tableExists(tableName, schema) {
|
|
161
189
|
if (!this.pool) {
|
|
162
190
|
throw new Error("Not connected to database");
|
|
@@ -12,7 +12,20 @@ import {
|
|
|
12
12
|
} from "./chunk-C7WEAPX4.js";
|
|
13
13
|
|
|
14
14
|
// src/connectors/postgres/index.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import path from "path";
|
|
15
18
|
import pg from "pg";
|
|
19
|
+
|
|
20
|
+
// src/connectors/postgres/failed-to-read-certificate.ts
|
|
21
|
+
var FailedToReadCertificate = class extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "FailedToReadCertificate";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/connectors/postgres/index.ts
|
|
16
29
|
var { Pool } = pg;
|
|
17
30
|
var PostgresDSNParser = class {
|
|
18
31
|
async parse(dsn, config) {
|
|
@@ -37,17 +50,38 @@ Expected: ${expectedFormat}`
|
|
|
37
50
|
user: url.username,
|
|
38
51
|
password: url.password
|
|
39
52
|
};
|
|
53
|
+
let sslmode;
|
|
54
|
+
let sslrootcert;
|
|
40
55
|
url.forEachSearchParam((value, key) => {
|
|
41
56
|
if (key === "sslmode") {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
poolConfig.ssl = { rejectUnauthorized: false };
|
|
46
|
-
} else {
|
|
47
|
-
poolConfig.ssl = true;
|
|
48
|
-
}
|
|
57
|
+
sslmode = value;
|
|
58
|
+
} else if (key === "sslrootcert") {
|
|
59
|
+
sslrootcert = value;
|
|
49
60
|
}
|
|
50
61
|
});
|
|
62
|
+
if (sslmode === "disable") {
|
|
63
|
+
poolConfig.ssl = false;
|
|
64
|
+
} else if (sslmode === "require") {
|
|
65
|
+
poolConfig.ssl = { rejectUnauthorized: false };
|
|
66
|
+
} else if (sslmode === "verify-ca" || sslmode === "verify-full") {
|
|
67
|
+
const sslConfig = { rejectUnauthorized: true };
|
|
68
|
+
if (sslmode === "verify-ca") {
|
|
69
|
+
sslConfig.checkServerIdentity = () => void 0;
|
|
70
|
+
}
|
|
71
|
+
if (sslrootcert) {
|
|
72
|
+
const certPath = sslrootcert.startsWith("~/") ? path.join(os.homedir(), sslrootcert.slice(2)) : sslrootcert;
|
|
73
|
+
try {
|
|
74
|
+
sslConfig.ca = await fs.promises.readFile(certPath, "utf-8");
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new FailedToReadCertificate(
|
|
77
|
+
`Failed to read SSL root certificate at '${certPath}': ${err instanceof Error ? err.message : String(err)}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
poolConfig.ssl = sslConfig;
|
|
82
|
+
} else if (sslmode !== void 0) {
|
|
83
|
+
poolConfig.ssl = true;
|
|
84
|
+
}
|
|
51
85
|
if (connectionTimeoutSeconds !== void 0) {
|
|
52
86
|
poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
|
|
53
87
|
}
|
|
@@ -56,8 +90,13 @@ Expected: ${expectedFormat}`
|
|
|
56
90
|
}
|
|
57
91
|
return poolConfig;
|
|
58
92
|
} catch (error) {
|
|
93
|
+
if (error instanceof FailedToReadCertificate) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const originalError = error instanceof Error ? error : new Error(String(error));
|
|
59
97
|
throw new Error(
|
|
60
|
-
`Failed to parse PostgreSQL DSN: ${
|
|
98
|
+
`Failed to parse PostgreSQL DSN: ${originalError.message}`,
|
|
99
|
+
{ cause: originalError }
|
|
61
100
|
);
|
|
62
101
|
}
|
|
63
102
|
}
|
|
@@ -145,9 +184,32 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
145
184
|
const schemaToUse = schema || this.defaultSchema;
|
|
146
185
|
const result = await client.query(
|
|
147
186
|
`
|
|
148
|
-
SELECT table_name
|
|
149
|
-
FROM information_schema.tables
|
|
187
|
+
SELECT table_name
|
|
188
|
+
FROM information_schema.tables
|
|
189
|
+
WHERE table_schema = $1
|
|
190
|
+
AND table_type = 'BASE TABLE'
|
|
191
|
+
ORDER BY table_name
|
|
192
|
+
`,
|
|
193
|
+
[schemaToUse]
|
|
194
|
+
);
|
|
195
|
+
return result.rows.map((row) => row.table_name);
|
|
196
|
+
} finally {
|
|
197
|
+
client.release();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async getViews(schema) {
|
|
201
|
+
if (!this.pool) {
|
|
202
|
+
throw new Error("Not connected to database");
|
|
203
|
+
}
|
|
204
|
+
const client = await this.pool.connect();
|
|
205
|
+
try {
|
|
206
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
207
|
+
const result = await client.query(
|
|
208
|
+
`
|
|
209
|
+
SELECT table_name
|
|
210
|
+
FROM information_schema.tables
|
|
150
211
|
WHERE table_schema = $1
|
|
212
|
+
AND table_type = 'VIEW'
|
|
151
213
|
ORDER BY table_name
|
|
152
214
|
`,
|
|
153
215
|
[schemaToUse]
|
|
@@ -167,8 +229,8 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
167
229
|
const result = await client.query(
|
|
168
230
|
`
|
|
169
231
|
SELECT EXISTS (
|
|
170
|
-
SELECT FROM information_schema.tables
|
|
171
|
-
WHERE table_schema = $1
|
|
232
|
+
SELECT FROM information_schema.tables
|
|
233
|
+
WHERE table_schema = $1
|
|
172
234
|
AND table_name = $2
|
|
173
235
|
)
|
|
174
236
|
`,
|
|
@@ -188,18 +250,18 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
188
250
|
const schemaToUse = schema || this.defaultSchema;
|
|
189
251
|
const result = await client.query(
|
|
190
252
|
`
|
|
191
|
-
SELECT
|
|
253
|
+
SELECT
|
|
192
254
|
i.relname as index_name,
|
|
193
255
|
array_agg(a.attname) as column_names,
|
|
194
256
|
ix.indisunique as is_unique,
|
|
195
257
|
ix.indisprimary as is_primary
|
|
196
|
-
FROM
|
|
258
|
+
FROM
|
|
197
259
|
pg_class t,
|
|
198
260
|
pg_class i,
|
|
199
261
|
pg_index ix,
|
|
200
262
|
pg_attribute a,
|
|
201
263
|
pg_namespace ns
|
|
202
|
-
WHERE
|
|
264
|
+
WHERE
|
|
203
265
|
t.oid = ix.indrelid
|
|
204
266
|
AND i.oid = ix.indexrelid
|
|
205
267
|
AND a.attrelid = t.oid
|
|
@@ -208,11 +270,11 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
208
270
|
AND t.relname = $1
|
|
209
271
|
AND ns.oid = t.relnamespace
|
|
210
272
|
AND ns.nspname = $2
|
|
211
|
-
GROUP BY
|
|
212
|
-
i.relname,
|
|
273
|
+
GROUP BY
|
|
274
|
+
i.relname,
|
|
213
275
|
ix.indisunique,
|
|
214
276
|
ix.indisprimary
|
|
215
|
-
ORDER BY
|
|
277
|
+
ORDER BY
|
|
216
278
|
i.relname
|
|
217
279
|
`,
|
|
218
280
|
[tableName, schemaToUse]
|
|
@@ -353,7 +415,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
353
415
|
const schemaToUse = schema || this.defaultSchema;
|
|
354
416
|
const result = await client.query(
|
|
355
417
|
`
|
|
356
|
-
SELECT
|
|
418
|
+
SELECT
|
|
357
419
|
routine_name as procedure_name,
|
|
358
420
|
routine_type,
|
|
359
421
|
CASE WHEN routine_type = 'PROCEDURE' THEN 'procedure' ELSE 'function' END as procedure_type,
|
|
@@ -362,8 +424,8 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
362
424
|
routine_definition as definition,
|
|
363
425
|
(
|
|
364
426
|
SELECT string_agg(
|
|
365
|
-
parameter_name || ' ' ||
|
|
366
|
-
parameter_mode || ' ' ||
|
|
427
|
+
parameter_name || ' ' ||
|
|
428
|
+
parameter_mode || ' ' ||
|
|
367
429
|
data_type,
|
|
368
430
|
', '
|
|
369
431
|
)
|
|
@@ -11,8 +11,24 @@ import {
|
|
|
11
11
|
splitSQLStatements
|
|
12
12
|
} from "./chunk-C7WEAPX4.js";
|
|
13
13
|
|
|
14
|
+
// src/connectors/sqlite/suppress-experimental-warning.ts
|
|
15
|
+
var installed = false;
|
|
16
|
+
function suppressSqliteExperimentalWarning() {
|
|
17
|
+
if (installed) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
installed = true;
|
|
21
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
22
|
+
process.emitWarning = (warning, ...args) => {
|
|
23
|
+
const message = typeof warning === "string" ? warning : warning?.message;
|
|
24
|
+
if (message && message.includes("SQLite is an experimental feature")) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
return originalEmitWarning(warning, ...args);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
// src/connectors/sqlite/index.ts
|
|
15
|
-
import Database from "better-sqlite3";
|
|
16
32
|
var SQLiteDSNParser = class {
|
|
17
33
|
async parse(dsn, config) {
|
|
18
34
|
if (!this.isValidDSN(dsn)) {
|
|
@@ -73,6 +89,42 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
73
89
|
clone() {
|
|
74
90
|
return new _SQLiteConnector();
|
|
75
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Prepare a user-facing query statement with BigInt reads enabled, so 64-bit
|
|
94
|
+
* integer values in the result rows keep full precision.
|
|
95
|
+
*
|
|
96
|
+
* better-sqlite3 exposed this connection-wide via `defaultSafeIntegers(true)`;
|
|
97
|
+
* `node:sqlite` only offers it per-statement via `setReadBigInts`. This is for
|
|
98
|
+
* `executeSQL` only — schema introspection reads small integer flags and uses
|
|
99
|
+
* `queryAll`/`queryOne` (plain numbers) so comparisons like `=== 1` work.
|
|
100
|
+
*/
|
|
101
|
+
prepare(sql) {
|
|
102
|
+
if (!this.db) {
|
|
103
|
+
throw new Error("Not connected to SQLite database");
|
|
104
|
+
}
|
|
105
|
+
const statement = this.db.prepare(sql);
|
|
106
|
+
statement.setReadBigInts(true);
|
|
107
|
+
return statement;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run an introspection query and return all rows. Integers come back as plain
|
|
111
|
+
* numbers (no `setReadBigInts`), which is what the schema-reading callers need
|
|
112
|
+
* for boolean flag comparisons. node:sqlite types `.all()` as a generic record
|
|
113
|
+
* array, so the result is cast to the caller's expected row shape.
|
|
114
|
+
*/
|
|
115
|
+
queryAll(sql, ...params) {
|
|
116
|
+
if (!this.db) {
|
|
117
|
+
throw new Error("Not connected to SQLite database");
|
|
118
|
+
}
|
|
119
|
+
return this.db.prepare(sql).all(...params);
|
|
120
|
+
}
|
|
121
|
+
/** Run an introspection query and return a single row (or undefined). */
|
|
122
|
+
queryOne(sql, ...params) {
|
|
123
|
+
if (!this.db) {
|
|
124
|
+
throw new Error("Not connected to SQLite database");
|
|
125
|
+
}
|
|
126
|
+
return this.db.prepare(sql).get(...params);
|
|
127
|
+
}
|
|
76
128
|
/**
|
|
77
129
|
* Connect to SQLite database
|
|
78
130
|
* Note: SQLite does not support connection timeouts as it's a local file-based database.
|
|
@@ -82,12 +134,18 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
82
134
|
const parsedConfig = await this.dsnParser.parse(dsn, config);
|
|
83
135
|
this.dbPath = parsedConfig.dbPath;
|
|
84
136
|
try {
|
|
85
|
-
|
|
137
|
+
suppressSqliteExperimentalWarning();
|
|
138
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
139
|
+
const dbOptions = {
|
|
140
|
+
// node:sqlite enables foreign key constraints by default, whereas
|
|
141
|
+
// better-sqlite3 (and SQLite's own default) leaves them off. Preserve
|
|
142
|
+
// the historical behavior; callers can opt in via `PRAGMA foreign_keys = ON`.
|
|
143
|
+
enableForeignKeyConstraints: false
|
|
144
|
+
};
|
|
86
145
|
if (config?.readonly && this.dbPath !== ":memory:") {
|
|
87
|
-
dbOptions.
|
|
146
|
+
dbOptions.readOnly = true;
|
|
88
147
|
}
|
|
89
|
-
this.db = new
|
|
90
|
-
this.db.defaultSafeIntegers(true);
|
|
148
|
+
this.db = new DatabaseSync(this.dbPath, dbOptions);
|
|
91
149
|
if (initScript) {
|
|
92
150
|
this.db.exec(initScript);
|
|
93
151
|
}
|
|
@@ -99,7 +157,8 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
99
157
|
async disconnect() {
|
|
100
158
|
if (this.db) {
|
|
101
159
|
try {
|
|
102
|
-
|
|
160
|
+
const inTransaction = this.db.isTransaction;
|
|
161
|
+
if (!inTransaction) {
|
|
103
162
|
this.db.close();
|
|
104
163
|
} else {
|
|
105
164
|
try {
|
|
@@ -127,13 +186,26 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
127
186
|
throw new Error("Not connected to SQLite database");
|
|
128
187
|
}
|
|
129
188
|
try {
|
|
130
|
-
const rows = this.
|
|
131
|
-
|
|
132
|
-
SELECT name FROM sqlite_master
|
|
189
|
+
const rows = this.queryAll(`
|
|
190
|
+
SELECT name FROM sqlite_master
|
|
133
191
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
134
192
|
ORDER BY name
|
|
135
|
-
`
|
|
136
|
-
|
|
193
|
+
`);
|
|
194
|
+
return rows.map((row) => row.name);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async getViews(schema) {
|
|
200
|
+
if (!this.db) {
|
|
201
|
+
throw new Error("Not connected to SQLite database");
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const rows = this.queryAll(`
|
|
205
|
+
SELECT name FROM sqlite_master
|
|
206
|
+
WHERE type='view' AND name NOT LIKE 'sqlite_%'
|
|
207
|
+
ORDER BY name
|
|
208
|
+
`);
|
|
137
209
|
return rows.map((row) => row.name);
|
|
138
210
|
} catch (error) {
|
|
139
211
|
throw error;
|
|
@@ -144,12 +216,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
144
216
|
throw new Error("Not connected to SQLite database");
|
|
145
217
|
}
|
|
146
218
|
try {
|
|
147
|
-
const row = this.
|
|
219
|
+
const row = this.queryOne(
|
|
148
220
|
`
|
|
149
|
-
SELECT name FROM sqlite_master
|
|
221
|
+
SELECT name FROM sqlite_master
|
|
150
222
|
WHERE type='table' AND name = ?
|
|
151
|
-
|
|
152
|
-
|
|
223
|
+
`,
|
|
224
|
+
tableName
|
|
225
|
+
);
|
|
153
226
|
return !!row;
|
|
154
227
|
} catch (error) {
|
|
155
228
|
throw error;
|
|
@@ -160,28 +233,33 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
160
233
|
throw new Error("Not connected to SQLite database");
|
|
161
234
|
}
|
|
162
235
|
try {
|
|
163
|
-
const indexInfoRows = this.
|
|
236
|
+
const indexInfoRows = this.queryAll(
|
|
164
237
|
`
|
|
165
|
-
SELECT
|
|
238
|
+
SELECT
|
|
166
239
|
name as index_name,
|
|
167
240
|
0 as is_unique
|
|
168
|
-
FROM sqlite_master
|
|
169
|
-
WHERE type = 'index'
|
|
241
|
+
FROM sqlite_master
|
|
242
|
+
WHERE type = 'index'
|
|
170
243
|
AND tbl_name = ?
|
|
171
|
-
|
|
172
|
-
|
|
244
|
+
`,
|
|
245
|
+
tableName
|
|
246
|
+
);
|
|
173
247
|
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
174
|
-
const indexListRows = this.
|
|
248
|
+
const indexListRows = this.queryAll(
|
|
249
|
+
`PRAGMA index_list(${quotedTableName})`
|
|
250
|
+
);
|
|
175
251
|
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
176
252
|
for (const indexListRow of indexListRows) {
|
|
177
253
|
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
178
254
|
}
|
|
179
|
-
const tableInfo = this.
|
|
255
|
+
const tableInfo = this.queryAll(`PRAGMA table_info(${quotedTableName})`);
|
|
180
256
|
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
181
257
|
const results = [];
|
|
182
258
|
for (const indexInfo of indexInfoRows) {
|
|
183
259
|
const quotedIndexName = quoteIdentifier(indexInfo.index_name, "sqlite");
|
|
184
|
-
const indexDetailRows = this.
|
|
260
|
+
const indexDetailRows = this.queryAll(
|
|
261
|
+
`PRAGMA index_info(${quotedIndexName})`
|
|
262
|
+
);
|
|
185
263
|
const columnNames = indexDetailRows.map((row) => row.name);
|
|
186
264
|
results.push({
|
|
187
265
|
index_name: indexInfo.index_name,
|
|
@@ -209,7 +287,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
209
287
|
}
|
|
210
288
|
try {
|
|
211
289
|
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
212
|
-
const rows = this.
|
|
290
|
+
const rows = this.queryAll(`PRAGMA table_info(${quotedTableName})`);
|
|
213
291
|
const columns = rows.map((row) => ({
|
|
214
292
|
column_name: row.name,
|
|
215
293
|
data_type: row.type,
|
|
@@ -253,7 +331,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
253
331
|
if (isReadStatement) {
|
|
254
332
|
if (parameters && parameters.length > 0) {
|
|
255
333
|
try {
|
|
256
|
-
const rows = this.
|
|
334
|
+
const rows = this.prepare(processedStatement).all(...parameters);
|
|
257
335
|
return { rows, rowCount: rows.length };
|
|
258
336
|
} catch (error) {
|
|
259
337
|
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
@@ -262,14 +340,14 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
262
340
|
throw error;
|
|
263
341
|
}
|
|
264
342
|
} else {
|
|
265
|
-
const rows = this.
|
|
343
|
+
const rows = this.prepare(processedStatement).all();
|
|
266
344
|
return { rows, rowCount: rows.length };
|
|
267
345
|
}
|
|
268
346
|
} else {
|
|
269
347
|
let result;
|
|
270
348
|
if (parameters && parameters.length > 0) {
|
|
271
349
|
try {
|
|
272
|
-
result = this.
|
|
350
|
+
result = this.prepare(processedStatement).run(...parameters);
|
|
273
351
|
} catch (error) {
|
|
274
352
|
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
275
353
|
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
@@ -277,9 +355,9 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
277
355
|
throw error;
|
|
278
356
|
}
|
|
279
357
|
} else {
|
|
280
|
-
result = this.
|
|
358
|
+
result = this.prepare(processedStatement).run();
|
|
281
359
|
}
|
|
282
|
-
return { rows: [], rowCount: result.changes };
|
|
360
|
+
return { rows: [], rowCount: Number(result.changes) };
|
|
283
361
|
}
|
|
284
362
|
} else {
|
|
285
363
|
if (parameters && parameters.length > 0) {
|
|
@@ -297,13 +375,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
297
375
|
}
|
|
298
376
|
let totalChanges = 0;
|
|
299
377
|
for (const statement of writeStatements) {
|
|
300
|
-
const result = this.
|
|
301
|
-
totalChanges += result.changes;
|
|
378
|
+
const result = this.prepare(statement).run();
|
|
379
|
+
totalChanges += Number(result.changes);
|
|
302
380
|
}
|
|
303
381
|
let allRows = [];
|
|
304
382
|
for (let statement of readStatements) {
|
|
305
383
|
statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
306
|
-
const result = this.
|
|
384
|
+
const result = this.prepare(statement).all();
|
|
307
385
|
allRows.push(...result);
|
|
308
386
|
}
|
|
309
387
|
return { rows: allRows, rowCount: totalChanges + allRows.length };
|
|
@@ -192,6 +192,7 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
192
192
|
SELECT TABLE_NAME
|
|
193
193
|
FROM INFORMATION_SCHEMA.TABLES
|
|
194
194
|
WHERE TABLE_SCHEMA = @schema
|
|
195
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
195
196
|
ORDER BY TABLE_NAME
|
|
196
197
|
`;
|
|
197
198
|
const result = await request.query(query);
|
|
@@ -200,6 +201,26 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
200
201
|
throw new Error(`Failed to get tables: ${error.message}`);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
204
|
+
async getViews(schema) {
|
|
205
|
+
if (!this.connection) {
|
|
206
|
+
throw new Error("Not connected to SQL Server database");
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const schemaToUse = schema || "dbo";
|
|
210
|
+
const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
|
|
211
|
+
const query = `
|
|
212
|
+
SELECT TABLE_NAME
|
|
213
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
214
|
+
WHERE TABLE_SCHEMA = @schema
|
|
215
|
+
AND TABLE_TYPE = 'VIEW'
|
|
216
|
+
ORDER BY TABLE_NAME
|
|
217
|
+
`;
|
|
218
|
+
const result = await request.query(query);
|
|
219
|
+
return result.recordset.map((row) => row.TABLE_NAME);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new Error(`Failed to get views: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
203
224
|
async tableExists(tableName, schema) {
|
|
204
225
|
if (!this.connection) {
|
|
205
226
|
throw new Error("Not connected to SQL Server database");
|
|
@@ -442,6 +463,19 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
442
463
|
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
443
464
|
}
|
|
444
465
|
const request = this.connection.request();
|
|
466
|
+
const messages = [];
|
|
467
|
+
request.on(
|
|
468
|
+
"info",
|
|
469
|
+
(info) => {
|
|
470
|
+
messages.push({
|
|
471
|
+
text: info.message,
|
|
472
|
+
// SQL Server reports severity as a numeric class; info messages are < 10.
|
|
473
|
+
severity: info.class !== void 0 ? String(info.class) : void 0,
|
|
474
|
+
code: info.number,
|
|
475
|
+
line: info.lineNumber
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
);
|
|
445
479
|
if (parameters && parameters.length > 0) {
|
|
446
480
|
parameters.forEach((param, index) => {
|
|
447
481
|
const paramName = `p${index + 1}`;
|
|
@@ -477,7 +511,8 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
477
511
|
}
|
|
478
512
|
return {
|
|
479
513
|
rows: result.recordset || [],
|
|
480
|
-
rowCount: result.rowsAffected[0] || 0
|
|
514
|
+
rowCount: result.rowsAffected[0] || 0,
|
|
515
|
+
...messages.length > 0 ? { messages } : {}
|
|
481
516
|
};
|
|
482
517
|
} catch (error) {
|
|
483
518
|
throw new Error(`Failed to execute query: ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
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": {
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"test:build": "node scripts/smoke-test-build.mjs"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=22.5.0"
|
|
38
38
|
},
|
|
39
|
+
"packageManager": "pnpm@10.17.1",
|
|
39
40
|
"keywords": [],
|
|
40
41
|
"author": "",
|
|
41
42
|
"license": "MIT",
|
|
@@ -51,7 +52,6 @@
|
|
|
51
52
|
"optionalDependencies": {
|
|
52
53
|
"@aws-sdk/rds-signer": "^3.1001.0",
|
|
53
54
|
"@azure/identity": "^4.8.0",
|
|
54
|
-
"better-sqlite3": "^11.9.0",
|
|
55
55
|
"mariadb": "^3.4.0",
|
|
56
56
|
"mssql": "^11.0.1",
|
|
57
57
|
"mysql2": "^3.13.0",
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
63
63
|
"@testcontainers/mysql": "^11.0.3",
|
|
64
64
|
"@testcontainers/postgresql": "^11.0.3",
|
|
65
|
-
"@types/better-sqlite3": "^7.6.12",
|
|
66
65
|
"@types/express": "^4.17.21",
|
|
67
66
|
"@types/mssql": "^9.1.7",
|
|
68
67
|
"@types/node": "^22.13.10",
|