@bytebase/dbhub 0.21.1 → 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 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
@@ -119,8 +129,8 @@ See [Testing](.claude/skills/testing/SKILL.md) and [Debug](https://dbhub.ai/conf
119
129
 
120
130
  <a href="https://www.star-history.com/?repos=bytebase%2Fdbhub&type=date&legend=top-left">
121
131
  <picture>
122
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=bytebase/dbhub&type=date&theme=dark&legend=top-left" />
123
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=bytebase/dbhub&type=date&legend=top-left" />
124
- <img alt="Star History Chart" src="https://api.star-history.com/image?repos=bytebase/dbhub&type=date&legend=top-left" />
132
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=bytebase/dbhub&type=date&theme=dark&legend=top-left" />
133
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=bytebase/dbhub&type=date&legend=top-left" />
134
+ <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=bytebase/dbhub&type=date&legend=top-left" />
125
135
  </picture>
126
136
  </a>
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isDriverNotInstalled
3
- } from "./chunk-GWPUVYLO.js";
3
+ } from "./chunk-WVVMH6FJ.js";
4
4
  import {
5
5
  ConnectorRegistry,
6
6
  SafeURL,
@@ -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.set("sslmode", "require");
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,
@@ -11,6 +11,13 @@ function isDriverNotInstalled(err, driver) {
11
11
  const missingSpecifier = match[1];
12
12
  return missingSpecifier === driver || missingSpecifier.startsWith(`${driver}/`);
13
13
  }
14
+ function isModuleNotFound(err) {
15
+ if (!(err instanceof Error) || !("code" in err)) {
16
+ return false;
17
+ }
18
+ const code = err.code;
19
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
20
+ }
14
21
  async function loadConnectors(connectorModules) {
15
22
  await Promise.all(
16
23
  connectorModules.map(async ({ load, name, driver }) => {
@@ -21,6 +28,13 @@ async function loadConnectors(connectorModules) {
21
28
  console.error(
22
29
  `Skipping ${name} connector: driver package "${driver}" not installed.`
23
30
  );
31
+ } else if (isModuleNotFound(err)) {
32
+ const msg = err instanceof Error ? err.message : String(err);
33
+ const match = msg.match(MISSING_MODULE_RE);
34
+ const missing = match ? match[1] : "unknown";
35
+ console.error(
36
+ `Skipping ${name} connector: required dependency "${missing}" not installed.`
37
+ );
24
38
  } else {
25
39
  throw err;
26
40
  }
package/dist/index.js CHANGED
@@ -8,14 +8,15 @@ 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-6JFMPX62.js";
16
+ } from "./chunk-MEBWJMLY.js";
16
17
  import {
17
18
  loadConnectors
18
- } from "./chunk-GWPUVYLO.js";
19
+ } from "./chunk-WVVMH6FJ.js";
19
20
  import {
20
21
  quoteQualifiedIdentifier
21
22
  } from "./chunk-JFWX35TB.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
- tablesToSearch = await connector.getTables(schemaName);
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 ? `Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
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 description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
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-I2JQWFDX.js");
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,6 +1592,13 @@ 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;
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
+ });
1601
+ }
1469
1602
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
1470
1603
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1471
1604
  res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
@@ -1512,16 +1645,28 @@ See documentation for more details on configuring database connections.
1512
1645
  res.sendFile(path.join(frontendPath, "index.html"));
1513
1646
  });
1514
1647
  }
1515
- app.listen(port, "0.0.0.0", () => {
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}`);
1516
1661
  if (process.env.NODE_ENV === "development") {
1517
1662
  console.error("Development mode detected!");
1518
1663
  console.error(" Workbench dev server (with HMR): http://localhost:5173");
1519
- console.error(" Backend API: http://localhost:8080");
1664
+ console.error(` Backend API: http://localhost:${boundPort}`);
1520
1665
  console.error("");
1521
1666
  } else {
1522
- console.error(`Workbench at http://localhost:${port}/`);
1667
+ console.error(`Workbench at http://${userHost}:${boundPort}/`);
1523
1668
  }
1524
- console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
1669
+ console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
1525
1670
  });
1526
1671
  } else {
1527
1672
  const server = createServer();
@@ -1549,11 +1694,11 @@ See documentation for more details on configuring database connections.
1549
1694
 
1550
1695
  // src/index.ts
1551
1696
  var connectorModules = [
1552
- { load: () => import("./postgres-4PCNQDGV.js"), name: "PostgreSQL", driver: "pg" },
1553
- { load: () => import("./sqlserver-JBR6X37Z.js"), name: "SQL Server", driver: "mssql" },
1554
- { load: () => import("./sqlite-FSCLCRIH.js"), name: "SQLite", driver: "better-sqlite3" },
1555
- { load: () => import("./mysql-I35IQ2GH.js"), name: "MySQL", driver: "mysql2" },
1556
- { load: () => import("./mariadb-VGTS4WXE.js"), name: "MariaDB", driver: "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" }
1557
1702
  ];
1558
1703
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
1559
1704
  console.error("Fatal error:", error);
@@ -2,6 +2,9 @@ import {
2
2
  extractAffectedRows,
3
3
  parseQueryResults
4
4
  } from "./chunk-RTB262PR.js";
5
+ import {
6
+ quoteIdentifier
7
+ } from "./chunk-JFWX35TB.js";
5
8
  import {
6
9
  SQLRowLimiter
7
10
  } from "./chunk-BRXZ5ZQB.js";
@@ -43,6 +46,10 @@ Expected: ${expectedFormat}`
43
46
  },
44
47
  ...queryTimeoutSeconds !== void 0 && {
45
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
46
53
  }
47
54
  };
48
55
  url.forEachSearchParam((value, key) => {
@@ -135,9 +142,10 @@ var MariaDBConnector = class _MariaDBConnector {
135
142
  const queryParams = schema ? [schema] : [];
136
143
  const rows = await this.pool.query(
137
144
  `
138
- SELECT TABLE_NAME
139
- FROM INFORMATION_SCHEMA.TABLES
145
+ SELECT TABLE_NAME
146
+ FROM INFORMATION_SCHEMA.TABLES
140
147
  ${schemaClause}
148
+ AND TABLE_TYPE = 'BASE TABLE'
141
149
  ORDER BY TABLE_NAME
142
150
  `,
143
151
  queryParams
@@ -148,6 +156,29 @@ var MariaDBConnector = class _MariaDBConnector {
148
156
  throw error;
149
157
  }
150
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
+ }
151
182
  async tableExists(tableName, schema) {
152
183
  if (!this.pool) {
153
184
  throw new Error("Not connected to database");
@@ -352,10 +383,12 @@ var MariaDBConnector = class _MariaDBConnector {
352
383
  let definition = procedure.ROUTINE_DEFINITION;
353
384
  try {
354
385
  const schemaValue = schema || await this.getCurrentSchema();
386
+ const quotedSchema = quoteIdentifier(schemaValue, "mariadb");
387
+ const quotedProcName = quoteIdentifier(procedureName, "mariadb");
355
388
  if (procedure.procedure_type === "procedure") {
356
389
  try {
357
390
  const defRows = await this.pool.query(`
358
- SHOW CREATE PROCEDURE ${schemaValue}.${procedureName}
391
+ SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
359
392
  `);
360
393
  if (defRows && defRows.length > 0) {
361
394
  definition = defRows[0]["Create Procedure"];
@@ -366,7 +399,7 @@ var MariaDBConnector = class _MariaDBConnector {
366
399
  } else {
367
400
  try {
368
401
  const defRows = await this.pool.query(`
369
- SHOW CREATE FUNCTION ${schemaValue}.${procedureName}
402
+ SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
370
403
  `);
371
404
  if (defRows && defRows.length > 0) {
372
405
  definition = defRows[0]["Create Function"];
@@ -2,6 +2,9 @@ import {
2
2
  extractAffectedRows,
3
3
  parseQueryResults
4
4
  } from "./chunk-RTB262PR.js";
5
+ import {
6
+ quoteIdentifier
7
+ } from "./chunk-JFWX35TB.js";
5
8
  import {
6
9
  SQLRowLimiter
7
10
  } from "./chunk-BRXZ5ZQB.js";
@@ -17,6 +20,7 @@ import mysql from "mysql2/promise";
17
20
  var MySQLDSNParser = class {
18
21
  async parse(dsn, config) {
19
22
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
23
+ const timezone = config?.timezone;
20
24
  if (!this.isValidDSN(dsn)) {
21
25
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
22
26
  const expectedFormat = this.getSampleDSN();
@@ -54,6 +58,9 @@ Expected: ${expectedFormat}`
54
58
  if (connectionTimeoutSeconds !== void 0) {
55
59
  config2.connectTimeout = connectionTimeoutSeconds * 1e3;
56
60
  }
61
+ if (timezone !== void 0) {
62
+ config2.timezone = timezone;
63
+ }
57
64
  if (url.password && url.password.includes("X-Amz-Credential")) {
58
65
  config2.authPlugins = {
59
66
  mysql_clear_password: () => () => {
@@ -141,9 +148,10 @@ var MySQLConnector = class _MySQLConnector {
141
148
  const queryParams = schema ? [schema] : [];
142
149
  const [rows] = await this.pool.query(
143
150
  `
144
- SELECT TABLE_NAME
145
- FROM INFORMATION_SCHEMA.TABLES
151
+ SELECT TABLE_NAME
152
+ FROM INFORMATION_SCHEMA.TABLES
146
153
  ${schemaClause}
154
+ AND TABLE_TYPE = 'BASE TABLE'
147
155
  ORDER BY TABLE_NAME
148
156
  `,
149
157
  queryParams
@@ -154,6 +162,29 @@ var MySQLConnector = class _MySQLConnector {
154
162
  throw error;
155
163
  }
156
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
+ }
157
188
  async tableExists(tableName, schema) {
158
189
  if (!this.pool) {
159
190
  throw new Error("Not connected to database");
@@ -358,10 +389,12 @@ var MySQLConnector = class _MySQLConnector {
358
389
  let definition = procedure.ROUTINE_DEFINITION;
359
390
  try {
360
391
  const schemaValue = schema || await this.getCurrentSchema();
392
+ const quotedSchema = quoteIdentifier(schemaValue, "mysql");
393
+ const quotedProcName = quoteIdentifier(procedureName, "mysql");
361
394
  if (procedure.procedure_type === "procedure") {
362
395
  try {
363
396
  const [defRows] = await this.pool.query(`
364
- SHOW CREATE PROCEDURE ${schemaValue}.${procedureName}
397
+ SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
365
398
  `);
366
399
  if (defRows && defRows.length > 0) {
367
400
  definition = defRows[0]["Create Procedure"];
@@ -372,7 +405,7 @@ var MySQLConnector = class _MySQLConnector {
372
405
  } else {
373
406
  try {
374
407
  const [defRows] = await this.pool.query(`
375
- SHOW CREATE FUNCTION ${schemaValue}.${procedureName}
408
+ SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
376
409
  `);
377
410
  if (defRows && defRows.length > 0) {
378
411
  definition = defRows[0]["Create Function"];
@@ -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
- if (value === "disable") {
43
- poolConfig.ssl = false;
44
- } else if (value === "require") {
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: ${error instanceof Error ? error.message : String(error)}`
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
  )
@@ -2,8 +2,8 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-6JFMPX62.js";
6
- import "./chunk-GWPUVYLO.js";
5
+ } from "./chunk-MEBWJMLY.js";
6
+ import "./chunk-WVVMH6FJ.js";
7
7
  import "./chunk-C7WEAPX4.js";
8
8
  export {
9
9
  ToolRegistry,
@@ -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
- const dbOptions = {};
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.readonly = true;
146
+ dbOptions.readOnly = true;
88
147
  }
89
- this.db = new Database(this.dbPath, dbOptions);
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
- if (!this.db.inTransaction) {
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.db.prepare(
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
- ).all();
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.db.prepare(
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
- ).get(tableName);
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.db.prepare(
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
- ).all(tableName);
244
+ `,
245
+ tableName
246
+ );
173
247
  const quotedTableName = quoteIdentifier(tableName, "sqlite");
174
- const indexListRows = this.db.prepare(`PRAGMA index_list(${quotedTableName})`).all();
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.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
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.db.prepare(`PRAGMA index_info(${quotedIndexName})`).all();
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.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
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.db.prepare(processedStatement).all(...parameters);
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.db.prepare(processedStatement).all();
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.db.prepare(processedStatement).run(...parameters);
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.db.prepare(processedStatement).run();
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.db.prepare(statement).run();
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.db.prepare(statement).all();
384
+ const result = this.prepare(statement).all();
307
385
  allRows.push(...result);
308
386
  }
309
387
  return { rows: allRows, rowCount: totalChanges + allRows.length };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isDriverNotInstalled
3
- } from "./chunk-GWPUVYLO.js";
3
+ } from "./chunk-WVVMH6FJ.js";
4
4
  import {
5
5
  SQLRowLimiter
6
6
  } from "./chunk-BRXZ5ZQB.js";
@@ -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.21.1",
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": ">=20"
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",