@bytebase/dbhub 0.11.6 → 0.11.8

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/dist/index.js CHANGED
@@ -184,6 +184,21 @@ function obfuscateDSNPassword(dsn) {
184
184
  return dsn;
185
185
  }
186
186
  }
187
+ function getDatabaseTypeFromDSN(dsn) {
188
+ if (!dsn) {
189
+ return void 0;
190
+ }
191
+ const protocol = dsn.split(":")[0];
192
+ const protocolToType = {
193
+ "postgres": "postgres",
194
+ "postgresql": "postgres",
195
+ "mysql": "mysql",
196
+ "mariadb": "mariadb",
197
+ "sqlserver": "sqlserver",
198
+ "sqlite": "sqlite"
199
+ };
200
+ return protocolToType[protocol];
201
+ }
187
202
 
188
203
  // src/utils/sql-row-limiter.ts
189
204
  var SQLRowLimiter = class {
@@ -279,7 +294,8 @@ var SQLRowLimiter = class {
279
294
  // src/connectors/postgres/index.ts
280
295
  var { Pool } = pg;
281
296
  var PostgresDSNParser = class {
282
- async parse(dsn) {
297
+ async parse(dsn, config) {
298
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
283
299
  if (!this.isValidDSN(dsn)) {
284
300
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
285
301
  const expectedFormat = this.getSampleDSN();
@@ -291,7 +307,7 @@ Expected: ${expectedFormat}`
291
307
  }
292
308
  try {
293
309
  const url = new SafeURL(dsn);
294
- const config = {
310
+ const config2 = {
295
311
  host: url.hostname,
296
312
  port: url.port ? parseInt(url.port) : 5432,
297
313
  database: url.pathname ? url.pathname.substring(1) : "",
@@ -302,15 +318,18 @@ Expected: ${expectedFormat}`
302
318
  url.forEachSearchParam((value, key) => {
303
319
  if (key === "sslmode") {
304
320
  if (value === "disable") {
305
- config.ssl = false;
321
+ config2.ssl = false;
306
322
  } else if (value === "require") {
307
- config.ssl = { rejectUnauthorized: false };
323
+ config2.ssl = { rejectUnauthorized: false };
308
324
  } else {
309
- config.ssl = true;
325
+ config2.ssl = true;
310
326
  }
311
327
  }
312
328
  });
313
- return config;
329
+ if (connectionTimeoutSeconds !== void 0) {
330
+ config2.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
331
+ }
332
+ return config2;
314
333
  } catch (error) {
315
334
  throw new Error(
316
335
  `Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
@@ -328,17 +347,20 @@ Expected: ${expectedFormat}`
328
347
  }
329
348
  }
330
349
  };
331
- var PostgresConnector = class {
350
+ var PostgresConnector = class _PostgresConnector {
332
351
  constructor() {
333
352
  this.id = "postgres";
334
353
  this.name = "PostgreSQL";
335
354
  this.dsnParser = new PostgresDSNParser();
336
355
  this.pool = null;
337
356
  }
338
- async connect(dsn) {
357
+ clone() {
358
+ return new _PostgresConnector();
359
+ }
360
+ async connect(dsn, initScript, config) {
339
361
  try {
340
- const config = await this.dsnParser.parse(dsn);
341
- this.pool = new Pool(config);
362
+ const poolConfig = await this.dsnParser.parse(dsn, config);
363
+ this.pool = new Pool(poolConfig);
342
364
  const client = await this.pool.connect();
343
365
  console.error("Successfully connected to PostgreSQL database");
344
366
  client.release();
@@ -625,7 +647,9 @@ ConnectorRegistry.register(postgresConnector);
625
647
  import sql from "mssql";
626
648
  import { DefaultAzureCredential } from "@azure/identity";
627
649
  var SQLServerDSNParser = class {
628
- async parse(dsn) {
650
+ async parse(dsn, config) {
651
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
652
+ const requestTimeoutSeconds = config?.requestTimeoutSeconds;
629
653
  if (!this.isValidDSN(dsn)) {
630
654
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
631
655
  const expectedFormat = this.getSampleDSN();
@@ -639,14 +663,12 @@ Expected: ${expectedFormat}`
639
663
  const url = new SafeURL(dsn);
640
664
  const options = {};
641
665
  url.forEachSearchParam((value, key) => {
642
- if (key === "connectTimeout") {
643
- options.connectTimeout = parseInt(value, 10);
644
- } else if (key === "requestTimeout") {
645
- options.requestTimeout = parseInt(value, 10);
646
- } else if (key === "authentication") {
666
+ if (key === "authentication") {
647
667
  options.authentication = value;
648
668
  } else if (key === "sslmode") {
649
669
  options.sslmode = value;
670
+ } else if (key === "instanceName") {
671
+ options.instanceName = value;
650
672
  }
651
673
  });
652
674
  if (options.sslmode) {
@@ -658,7 +680,7 @@ Expected: ${expectedFormat}`
658
680
  options.trustServerCertificate = true;
659
681
  }
660
682
  }
661
- const config = {
683
+ const config2 = {
662
684
  user: url.username,
663
685
  password: url.password,
664
686
  server: url.hostname,
@@ -670,15 +692,21 @@ Expected: ${expectedFormat}`
670
692
  encrypt: options.encrypt ?? false,
671
693
  // Default to unencrypted for development
672
694
  trustServerCertificate: options.trustServerCertificate ?? false,
673
- connectTimeout: options.connectTimeout ?? 15e3,
674
- requestTimeout: options.requestTimeout ?? 15e3
695
+ ...connectionTimeoutSeconds !== void 0 && {
696
+ connectTimeout: connectionTimeoutSeconds * 1e3
697
+ },
698
+ ...requestTimeoutSeconds !== void 0 && {
699
+ requestTimeout: requestTimeoutSeconds * 1e3
700
+ },
701
+ instanceName: options.instanceName
702
+ // Add named instance support
675
703
  }
676
704
  };
677
705
  if (options.authentication === "azure-active-directory-access-token") {
678
706
  try {
679
707
  const credential = new DefaultAzureCredential();
680
708
  const token = await credential.getToken("https://database.windows.net/");
681
- config.authentication = {
709
+ config2.authentication = {
682
710
  type: "azure-active-directory-access-token",
683
711
  options: {
684
712
  token: token.token
@@ -689,7 +717,7 @@ Expected: ${expectedFormat}`
689
717
  throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
690
718
  }
691
719
  }
692
- return config;
720
+ return config2;
693
721
  } catch (error) {
694
722
  throw new Error(
695
723
  `Failed to parse SQL Server DSN: ${error instanceof Error ? error.message : String(error)}`
@@ -697,7 +725,7 @@ Expected: ${expectedFormat}`
697
725
  }
698
726
  }
699
727
  getSampleDSN() {
700
- return "sqlserver://username:password@localhost:1433/database?sslmode=disable";
728
+ return "sqlserver://username:password@localhost:1433/database?sslmode=disable&instanceName=INSTANCE1";
701
729
  }
702
730
  isValidDSN(dsn) {
703
731
  try {
@@ -707,15 +735,18 @@ Expected: ${expectedFormat}`
707
735
  }
708
736
  }
709
737
  };
710
- var SQLServerConnector = class {
738
+ var SQLServerConnector = class _SQLServerConnector {
711
739
  constructor() {
712
740
  this.id = "sqlserver";
713
741
  this.name = "SQL Server";
714
742
  this.dsnParser = new SQLServerDSNParser();
715
743
  }
716
- async connect(dsn) {
744
+ clone() {
745
+ return new _SQLServerConnector();
746
+ }
747
+ async connect(dsn, initScript, config) {
717
748
  try {
718
- this.config = await this.dsnParser.parse(dsn);
749
+ this.config = await this.dsnParser.parse(dsn, config);
719
750
  if (!this.config.options) {
720
751
  this.config.options = {};
721
752
  }
@@ -979,7 +1010,7 @@ ConnectorRegistry.register(sqlServerConnector);
979
1010
  // src/connectors/sqlite/index.ts
980
1011
  import Database from "better-sqlite3";
981
1012
  var SQLiteDSNParser = class {
982
- async parse(dsn) {
1013
+ async parse(dsn, config) {
983
1014
  if (!this.isValidDSN(dsn)) {
984
1015
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
985
1016
  const expectedFormat = this.getSampleDSN();
@@ -997,6 +1028,8 @@ Expected: ${expectedFormat}`
997
1028
  } else {
998
1029
  if (url.pathname.startsWith("//")) {
999
1030
  dbPath = url.pathname.substring(2);
1031
+ } else if (url.pathname.match(/^\/[A-Za-z]:\//)) {
1032
+ dbPath = url.pathname.substring(1);
1000
1033
  } else {
1001
1034
  dbPath = url.pathname;
1002
1035
  }
@@ -1019,7 +1052,7 @@ Expected: ${expectedFormat}`
1019
1052
  }
1020
1053
  }
1021
1054
  };
1022
- var SQLiteConnector = class {
1055
+ var SQLiteConnector = class _SQLiteConnector {
1023
1056
  constructor() {
1024
1057
  this.id = "sqlite";
1025
1058
  this.name = "SQLite";
@@ -1028,9 +1061,17 @@ var SQLiteConnector = class {
1028
1061
  this.dbPath = ":memory:";
1029
1062
  }
1030
1063
  // Default to in-memory database
1031
- async connect(dsn, initScript) {
1032
- const config = await this.dsnParser.parse(dsn);
1033
- this.dbPath = config.dbPath;
1064
+ clone() {
1065
+ return new _SQLiteConnector();
1066
+ }
1067
+ /**
1068
+ * Connect to SQLite database
1069
+ * Note: SQLite does not support connection timeouts as it's a local file-based database.
1070
+ * The config parameter is accepted for interface compliance but ignored.
1071
+ */
1072
+ async connect(dsn, initScript, config) {
1073
+ const parsedConfig = await this.dsnParser.parse(dsn, config);
1074
+ this.dbPath = parsedConfig.dbPath;
1034
1075
  try {
1035
1076
  this.db = new Database(this.dbPath);
1036
1077
  console.error("Successfully connected to SQLite database");
@@ -1232,8 +1273,47 @@ ConnectorRegistry.register(sqliteConnector);
1232
1273
 
1233
1274
  // src/connectors/mysql/index.ts
1234
1275
  import mysql from "mysql2/promise";
1276
+
1277
+ // src/utils/multi-statement-result-parser.ts
1278
+ function isMetadataObject(element) {
1279
+ if (!element || typeof element !== "object" || Array.isArray(element)) {
1280
+ return false;
1281
+ }
1282
+ return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
1283
+ }
1284
+ function isMultiStatementResult(results) {
1285
+ if (!Array.isArray(results) || results.length === 0) {
1286
+ return false;
1287
+ }
1288
+ const firstElement = results[0];
1289
+ return isMetadataObject(firstElement) || Array.isArray(firstElement);
1290
+ }
1291
+ function extractRowsFromMultiStatement(results) {
1292
+ if (!Array.isArray(results)) {
1293
+ return [];
1294
+ }
1295
+ const allRows = [];
1296
+ for (const result of results) {
1297
+ if (Array.isArray(result)) {
1298
+ allRows.push(...result);
1299
+ }
1300
+ }
1301
+ return allRows;
1302
+ }
1303
+ function parseQueryResults(results) {
1304
+ if (!Array.isArray(results)) {
1305
+ return [];
1306
+ }
1307
+ if (isMultiStatementResult(results)) {
1308
+ return extractRowsFromMultiStatement(results);
1309
+ }
1310
+ return results;
1311
+ }
1312
+
1313
+ // src/connectors/mysql/index.ts
1235
1314
  var MySQLDSNParser = class {
1236
- async parse(dsn) {
1315
+ async parse(dsn, config) {
1316
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
1237
1317
  if (!this.isValidDSN(dsn)) {
1238
1318
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
1239
1319
  const expectedFormat = this.getSampleDSN();
@@ -1245,7 +1325,7 @@ Expected: ${expectedFormat}`
1245
1325
  }
1246
1326
  try {
1247
1327
  const url = new SafeURL(dsn);
1248
- const config = {
1328
+ const config2 = {
1249
1329
  host: url.hostname,
1250
1330
  port: url.port ? parseInt(url.port) : 3306,
1251
1331
  database: url.pathname ? url.pathname.substring(1) : "",
@@ -1258,15 +1338,18 @@ Expected: ${expectedFormat}`
1258
1338
  url.forEachSearchParam((value, key) => {
1259
1339
  if (key === "sslmode") {
1260
1340
  if (value === "disable") {
1261
- config.ssl = void 0;
1341
+ config2.ssl = void 0;
1262
1342
  } else if (value === "require") {
1263
- config.ssl = { rejectUnauthorized: false };
1343
+ config2.ssl = { rejectUnauthorized: false };
1264
1344
  } else {
1265
- config.ssl = {};
1345
+ config2.ssl = {};
1266
1346
  }
1267
1347
  }
1268
1348
  });
1269
- return config;
1349
+ if (connectionTimeoutSeconds !== void 0) {
1350
+ config2.connectTimeout = connectionTimeoutSeconds * 1e3;
1351
+ }
1352
+ return config2;
1270
1353
  } catch (error) {
1271
1354
  throw new Error(
1272
1355
  `Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
@@ -1284,17 +1367,20 @@ Expected: ${expectedFormat}`
1284
1367
  }
1285
1368
  }
1286
1369
  };
1287
- var MySQLConnector = class {
1370
+ var MySQLConnector = class _MySQLConnector {
1288
1371
  constructor() {
1289
1372
  this.id = "mysql";
1290
1373
  this.name = "MySQL";
1291
1374
  this.dsnParser = new MySQLDSNParser();
1292
1375
  this.pool = null;
1293
1376
  }
1294
- async connect(dsn) {
1377
+ clone() {
1378
+ return new _MySQLConnector();
1379
+ }
1380
+ async connect(dsn, initScript, config) {
1295
1381
  try {
1296
- const config = await this.dsnParser.parse(dsn);
1297
- this.pool = mysql.createPool(config);
1382
+ const connectionOptions = await this.dsnParser.parse(dsn, config);
1383
+ this.pool = mysql.createPool(connectionOptions);
1298
1384
  const [rows] = await this.pool.query("SELECT 1");
1299
1385
  console.error("Successfully connected to MySQL database");
1300
1386
  } catch (err) {
@@ -1582,6 +1668,7 @@ var MySQLConnector = class {
1582
1668
  if (!this.pool) {
1583
1669
  throw new Error("Not connected to database");
1584
1670
  }
1671
+ const conn = await this.pool.getConnection();
1585
1672
  try {
1586
1673
  let processedSQL = sql2;
1587
1674
  if (options.maxRows) {
@@ -1594,22 +1681,15 @@ var MySQLConnector = class {
1594
1681
  processedSQL += ";";
1595
1682
  }
1596
1683
  }
1597
- const results = await this.pool.query(processedSQL);
1684
+ const results = await conn.query(processedSQL);
1598
1685
  const [firstResult] = results;
1599
- if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0])) {
1600
- let allRows = [];
1601
- for (const result of firstResult) {
1602
- if (Array.isArray(result)) {
1603
- allRows.push(...result);
1604
- }
1605
- }
1606
- return { rows: allRows };
1607
- } else {
1608
- return { rows: Array.isArray(firstResult) ? firstResult : [] };
1609
- }
1686
+ const rows = parseQueryResults(firstResult);
1687
+ return { rows };
1610
1688
  } catch (error) {
1611
1689
  console.error("Error executing query:", error);
1612
1690
  throw error;
1691
+ } finally {
1692
+ conn.release();
1613
1693
  }
1614
1694
  }
1615
1695
  };
@@ -1619,7 +1699,8 @@ ConnectorRegistry.register(mysqlConnector);
1619
1699
  // src/connectors/mariadb/index.ts
1620
1700
  import mariadb from "mariadb";
1621
1701
  var MariadbDSNParser = class {
1622
- async parse(dsn) {
1702
+ async parse(dsn, config) {
1703
+ const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
1623
1704
  if (!this.isValidDSN(dsn)) {
1624
1705
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
1625
1706
  const expectedFormat = this.getSampleDSN();
@@ -1631,7 +1712,7 @@ Expected: ${expectedFormat}`
1631
1712
  }
1632
1713
  try {
1633
1714
  const url = new SafeURL(dsn);
1634
- const config = {
1715
+ const config2 = {
1635
1716
  host: url.hostname,
1636
1717
  port: url.port ? parseInt(url.port) : 3306,
1637
1718
  database: url.pathname ? url.pathname.substring(1) : "",
@@ -1640,21 +1721,22 @@ Expected: ${expectedFormat}`
1640
1721
  password: url.password,
1641
1722
  multipleStatements: true,
1642
1723
  // Enable native multi-statement support
1643
- connectTimeout: 5e3
1644
- // 5 second timeout for connections
1724
+ ...connectionTimeoutSeconds !== void 0 && {
1725
+ connectTimeout: connectionTimeoutSeconds * 1e3
1726
+ }
1645
1727
  };
1646
1728
  url.forEachSearchParam((value, key) => {
1647
1729
  if (key === "sslmode") {
1648
1730
  if (value === "disable") {
1649
- config.ssl = void 0;
1731
+ config2.ssl = void 0;
1650
1732
  } else if (value === "require") {
1651
- config.ssl = { rejectUnauthorized: false };
1733
+ config2.ssl = { rejectUnauthorized: false };
1652
1734
  } else {
1653
- config.ssl = {};
1735
+ config2.ssl = {};
1654
1736
  }
1655
1737
  }
1656
1738
  });
1657
- return config;
1739
+ return config2;
1658
1740
  } catch (error) {
1659
1741
  throw new Error(
1660
1742
  `Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
@@ -1672,17 +1754,20 @@ Expected: ${expectedFormat}`
1672
1754
  }
1673
1755
  }
1674
1756
  };
1675
- var MariaDBConnector = class {
1757
+ var MariaDBConnector = class _MariaDBConnector {
1676
1758
  constructor() {
1677
1759
  this.id = "mariadb";
1678
1760
  this.name = "MariaDB";
1679
1761
  this.dsnParser = new MariadbDSNParser();
1680
1762
  this.pool = null;
1681
1763
  }
1682
- async connect(dsn) {
1764
+ clone() {
1765
+ return new _MariaDBConnector();
1766
+ }
1767
+ async connect(dsn, initScript, config) {
1683
1768
  try {
1684
- const config = await this.dsnParser.parse(dsn);
1685
- this.pool = mariadb.createPool(config);
1769
+ const connectionConfig = await this.dsnParser.parse(dsn, config);
1770
+ this.pool = mariadb.createPool(connectionConfig);
1686
1771
  console.error("Testing connection to MariaDB...");
1687
1772
  await this.pool.query("SELECT 1");
1688
1773
  console.error("Successfully connected to MariaDB database");
@@ -1971,6 +2056,7 @@ var MariaDBConnector = class {
1971
2056
  if (!this.pool) {
1972
2057
  throw new Error("Not connected to database");
1973
2058
  }
2059
+ const conn = await this.pool.getConnection();
1974
2060
  try {
1975
2061
  let processedSQL = sql2;
1976
2062
  if (options.maxRows) {
@@ -1983,25 +2069,14 @@ var MariaDBConnector = class {
1983
2069
  processedSQL += ";";
1984
2070
  }
1985
2071
  }
1986
- const results = await this.pool.query(processedSQL);
1987
- if (Array.isArray(results)) {
1988
- if (results.length > 0 && Array.isArray(results[0]) && results[0].length > 0) {
1989
- let allRows = [];
1990
- for (const result of results) {
1991
- if (Array.isArray(result)) {
1992
- allRows.push(...result);
1993
- }
1994
- }
1995
- return { rows: allRows };
1996
- } else {
1997
- return { rows: results };
1998
- }
1999
- } else {
2000
- return { rows: [] };
2001
- }
2072
+ const results = await conn.query(processedSQL);
2073
+ const rows = parseQueryResults(results);
2074
+ return { rows };
2002
2075
  } catch (error) {
2003
2076
  console.error("Error executing query:", error);
2004
2077
  throw error;
2078
+ } finally {
2079
+ conn.release();
2005
2080
  }
2006
2081
  }
2007
2082
  };
@@ -2015,7 +2090,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2015
2090
  import express from "express";
2016
2091
  import path3 from "path";
2017
2092
  import { readFileSync as readFileSync3 } from "fs";
2018
- import { fileURLToPath as fileURLToPath3 } from "url";
2093
+ import { fileURLToPath as fileURLToPath2 } from "url";
2019
2094
 
2020
2095
  // src/utils/ssh-tunnel.ts
2021
2096
  import { Client } from "ssh2";
@@ -2159,6 +2234,12 @@ var SSHTunnel = class {
2159
2234
  }
2160
2235
  };
2161
2236
 
2237
+ // src/config/toml-loader.ts
2238
+ import fs2 from "fs";
2239
+ import path2 from "path";
2240
+ import { homedir as homedir3 } from "os";
2241
+ import toml from "@iarna/toml";
2242
+
2162
2243
  // src/config/env.ts
2163
2244
  import dotenv from "dotenv";
2164
2245
  import path from "path";
@@ -2544,96 +2625,398 @@ function resolveSSHConfig() {
2544
2625
  source: sources.join(", ")
2545
2626
  };
2546
2627
  }
2628
+ async function resolveSourceConfigs() {
2629
+ const tomlConfig = loadTomlConfig();
2630
+ if (tomlConfig) {
2631
+ return tomlConfig;
2632
+ }
2633
+ const dsnResult = resolveDSN();
2634
+ if (dsnResult) {
2635
+ let dsnUrl;
2636
+ try {
2637
+ dsnUrl = new URL(dsnResult.dsn);
2638
+ } catch (error) {
2639
+ throw new Error(
2640
+ `Invalid DSN format: ${dsnResult.dsn}. Expected format: protocol://[user[:password]@]host[:port]/database`
2641
+ );
2642
+ }
2643
+ const protocol = dsnUrl.protocol.replace(":", "");
2644
+ let dbType;
2645
+ if (protocol === "postgresql" || protocol === "postgres") {
2646
+ dbType = "postgres";
2647
+ } else if (protocol === "mysql") {
2648
+ dbType = "mysql";
2649
+ } else if (protocol === "mariadb") {
2650
+ dbType = "mariadb";
2651
+ } else if (protocol === "sqlserver") {
2652
+ dbType = "sqlserver";
2653
+ } else if (protocol === "sqlite") {
2654
+ dbType = "sqlite";
2655
+ } else {
2656
+ throw new Error(`Unsupported database type in DSN: ${protocol}`);
2657
+ }
2658
+ const source = {
2659
+ id: "default",
2660
+ type: dbType,
2661
+ dsn: dsnResult.dsn
2662
+ };
2663
+ const sshResult = resolveSSHConfig();
2664
+ if (sshResult) {
2665
+ source.ssh_host = sshResult.config.host;
2666
+ source.ssh_port = sshResult.config.port;
2667
+ source.ssh_user = sshResult.config.username;
2668
+ source.ssh_password = sshResult.config.password;
2669
+ source.ssh_key = sshResult.config.privateKey;
2670
+ source.ssh_passphrase = sshResult.config.passphrase;
2671
+ }
2672
+ source.readonly = isReadOnlyMode();
2673
+ const maxRowsResult = resolveMaxRows();
2674
+ if (maxRowsResult) {
2675
+ source.max_rows = maxRowsResult.maxRows;
2676
+ }
2677
+ if (dsnResult.isDemo) {
2678
+ const { getSqliteInMemorySetupSql } = await import("./demo-loader-EOVFD32T.js");
2679
+ source.init_script = getSqliteInMemorySetupSql();
2680
+ }
2681
+ return {
2682
+ sources: [source],
2683
+ source: dsnResult.isDemo ? "demo mode" : dsnResult.source
2684
+ };
2685
+ }
2686
+ return null;
2687
+ }
2688
+
2689
+ // src/config/toml-loader.ts
2690
+ function loadTomlConfig() {
2691
+ const configPath = resolveTomlConfigPath();
2692
+ if (!configPath) {
2693
+ return null;
2694
+ }
2695
+ try {
2696
+ const fileContent = fs2.readFileSync(configPath, "utf-8");
2697
+ const parsedToml = toml.parse(fileContent);
2698
+ validateTomlConfig(parsedToml, configPath);
2699
+ const sources = processSourceConfigs(parsedToml.sources, configPath);
2700
+ return {
2701
+ sources,
2702
+ source: path2.basename(configPath)
2703
+ };
2704
+ } catch (error) {
2705
+ if (error instanceof Error) {
2706
+ throw new Error(
2707
+ `Failed to load TOML configuration from ${configPath}: ${error.message}`
2708
+ );
2709
+ }
2710
+ throw error;
2711
+ }
2712
+ }
2713
+ function resolveTomlConfigPath() {
2714
+ const args = parseCommandLineArgs();
2715
+ if (args.config) {
2716
+ const configPath = expandHomeDir(args.config);
2717
+ if (!fs2.existsSync(configPath)) {
2718
+ throw new Error(
2719
+ `Configuration file specified by --config flag not found: ${configPath}`
2720
+ );
2721
+ }
2722
+ return configPath;
2723
+ }
2724
+ const defaultConfigPath = path2.join(process.cwd(), "dbhub.toml");
2725
+ if (fs2.existsSync(defaultConfigPath)) {
2726
+ return defaultConfigPath;
2727
+ }
2728
+ return null;
2729
+ }
2730
+ function validateTomlConfig(config, configPath) {
2731
+ if (!config.sources) {
2732
+ throw new Error(
2733
+ `Configuration file ${configPath} must contain a [[sources]] array. Example:
2734
+
2735
+ [[sources]]
2736
+ id = "my_db"
2737
+ dsn = "postgres://..."`
2738
+ );
2739
+ }
2740
+ if (!Array.isArray(config.sources)) {
2741
+ throw new Error(
2742
+ `Configuration file ${configPath}: 'sources' must be an array. Use [[sources]] syntax for array of tables in TOML.`
2743
+ );
2744
+ }
2745
+ if (config.sources.length === 0) {
2746
+ throw new Error(
2747
+ `Configuration file ${configPath}: sources array cannot be empty. Please define at least one source with [[sources]].`
2748
+ );
2749
+ }
2750
+ const ids = /* @__PURE__ */ new Set();
2751
+ const duplicates = [];
2752
+ for (const source of config.sources) {
2753
+ if (!source.id) {
2754
+ throw new Error(
2755
+ `Configuration file ${configPath}: each source must have an 'id' field. Example: [[sources]]
2756
+ id = "my_db"`
2757
+ );
2758
+ }
2759
+ if (ids.has(source.id)) {
2760
+ duplicates.push(source.id);
2761
+ } else {
2762
+ ids.add(source.id);
2763
+ }
2764
+ }
2765
+ if (duplicates.length > 0) {
2766
+ throw new Error(
2767
+ `Configuration file ${configPath}: duplicate source IDs found: ${duplicates.join(", ")}. Each source must have a unique 'id' field.`
2768
+ );
2769
+ }
2770
+ for (const source of config.sources) {
2771
+ validateSourceConfig(source, configPath);
2772
+ }
2773
+ }
2774
+ function validateSourceConfig(source, configPath) {
2775
+ const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
2776
+ if (!source.dsn && !hasConnectionParams) {
2777
+ throw new Error(
2778
+ `Configuration file ${configPath}: source '${source.id}' must have either:
2779
+ - 'dsn' field (e.g., dsn = "postgres://user:pass@host:5432/dbname")
2780
+ - OR connection parameters (type, host, database, user, password)
2781
+ - For SQLite: type = "sqlite" and database path`
2782
+ );
2783
+ }
2784
+ if (source.type) {
2785
+ const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite"];
2786
+ if (!validTypes.includes(source.type)) {
2787
+ throw new Error(
2788
+ `Configuration file ${configPath}: source '${source.id}' has invalid type '${source.type}'. Valid types: ${validTypes.join(", ")}`
2789
+ );
2790
+ }
2791
+ }
2792
+ if (source.max_rows !== void 0) {
2793
+ if (typeof source.max_rows !== "number" || source.max_rows <= 0) {
2794
+ throw new Error(
2795
+ `Configuration file ${configPath}: source '${source.id}' has invalid max_rows. Must be a positive integer.`
2796
+ );
2797
+ }
2798
+ }
2799
+ if (source.connection_timeout !== void 0) {
2800
+ if (typeof source.connection_timeout !== "number" || source.connection_timeout <= 0) {
2801
+ throw new Error(
2802
+ `Configuration file ${configPath}: source '${source.id}' has invalid connection_timeout. Must be a positive number (in seconds).`
2803
+ );
2804
+ }
2805
+ }
2806
+ if (source.request_timeout !== void 0) {
2807
+ if (typeof source.request_timeout !== "number" || source.request_timeout <= 0) {
2808
+ throw new Error(
2809
+ `Configuration file ${configPath}: source '${source.id}' has invalid request_timeout. Must be a positive number (in seconds).`
2810
+ );
2811
+ }
2812
+ }
2813
+ if (source.ssh_port !== void 0) {
2814
+ if (typeof source.ssh_port !== "number" || source.ssh_port <= 0 || source.ssh_port > 65535) {
2815
+ throw new Error(
2816
+ `Configuration file ${configPath}: source '${source.id}' has invalid ssh_port. Must be between 1 and 65535.`
2817
+ );
2818
+ }
2819
+ }
2820
+ }
2821
+ function processSourceConfigs(sources, configPath) {
2822
+ return sources.map((source) => {
2823
+ const processed = { ...source };
2824
+ if (processed.ssh_key) {
2825
+ processed.ssh_key = expandHomeDir(processed.ssh_key);
2826
+ }
2827
+ if (processed.type === "sqlite" && processed.database) {
2828
+ processed.database = expandHomeDir(processed.database);
2829
+ }
2830
+ if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) {
2831
+ processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`;
2832
+ }
2833
+ return processed;
2834
+ });
2835
+ }
2836
+ function expandHomeDir(filePath) {
2837
+ if (filePath.startsWith("~/")) {
2838
+ return path2.join(homedir3(), filePath.substring(2));
2839
+ }
2840
+ return filePath;
2841
+ }
2842
+ function buildDSNFromSource(source) {
2843
+ if (source.dsn) {
2844
+ return source.dsn;
2845
+ }
2846
+ if (!source.type) {
2847
+ throw new Error(
2848
+ `Source '${source.id}': 'type' field is required when 'dsn' is not provided`
2849
+ );
2850
+ }
2851
+ if (source.type === "sqlite") {
2852
+ if (!source.database) {
2853
+ throw new Error(
2854
+ `Source '${source.id}': 'database' field is required for SQLite`
2855
+ );
2856
+ }
2857
+ return `sqlite:///${source.database}`;
2858
+ }
2859
+ if (!source.host || !source.user || !source.password || !source.database) {
2860
+ throw new Error(
2861
+ `Source '${source.id}': missing required connection parameters. Required: type, host, user, password, database`
2862
+ );
2863
+ }
2864
+ const port = source.port || (source.type === "postgres" ? 5432 : source.type === "mysql" || source.type === "mariadb" ? 3306 : source.type === "sqlserver" ? 1433 : void 0);
2865
+ if (!port) {
2866
+ throw new Error(`Source '${source.id}': unable to determine port`);
2867
+ }
2868
+ const encodedUser = encodeURIComponent(source.user);
2869
+ const encodedPassword = encodeURIComponent(source.password);
2870
+ const encodedDatabase = encodeURIComponent(source.database);
2871
+ let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`;
2872
+ if (source.type === "sqlserver" && source.instanceName) {
2873
+ dsn += `?instanceName=${encodeURIComponent(source.instanceName)}`;
2874
+ }
2875
+ return dsn;
2876
+ }
2547
2877
 
2548
2878
  // src/connectors/manager.ts
2549
2879
  var managerInstance = null;
2550
2880
  var ConnectorManager = class {
2881
+ // Ordered list of source IDs (first is default)
2551
2882
  constructor() {
2552
- this.activeConnector = null;
2553
- this.connected = false;
2554
- this.sshTunnel = null;
2555
- this.originalDSN = null;
2556
- this.maxRows = null;
2883
+ // Maps for multi-source support
2884
+ this.connectors = /* @__PURE__ */ new Map();
2885
+ this.sshTunnels = /* @__PURE__ */ new Map();
2886
+ this.executeOptions = /* @__PURE__ */ new Map();
2887
+ this.sourceConfigs = /* @__PURE__ */ new Map();
2888
+ // Store original source configs
2889
+ this.sourceIds = [];
2557
2890
  if (!managerInstance) {
2558
2891
  managerInstance = this;
2559
2892
  }
2560
- const maxRowsData = resolveMaxRows();
2561
- if (maxRowsData) {
2562
- this.maxRows = maxRowsData.maxRows;
2563
- console.error(`Max rows limit: ${this.maxRows} (from ${maxRowsData.source})`);
2893
+ }
2894
+ /**
2895
+ * Initialize and connect to multiple databases using source configurations
2896
+ * This is the new multi-source connection method
2897
+ */
2898
+ async connectWithSources(sources) {
2899
+ if (sources.length === 0) {
2900
+ throw new Error("No sources provided");
2901
+ }
2902
+ for (const source of sources) {
2903
+ await this.connectSource(source);
2564
2904
  }
2905
+ console.error(`Successfully connected to ${sources.length} database source(s)`);
2565
2906
  }
2566
2907
  /**
2567
- * Initialize and connect to the database using a DSN
2908
+ * Connect to a single source (helper for connectWithSources)
2568
2909
  */
2569
- async connectWithDSN(dsn, initScript) {
2570
- this.originalDSN = dsn;
2571
- const sshConfig = resolveSSHConfig();
2910
+ async connectSource(source) {
2911
+ const sourceId = source.id;
2912
+ console.error(`Connecting to source '${sourceId || "(default)"}' ...`);
2913
+ const dsn = buildDSNFromSource(source);
2572
2914
  let actualDSN = dsn;
2573
- if (sshConfig) {
2574
- console.error(`SSH tunnel configuration loaded from ${sshConfig.source}`);
2915
+ if (source.ssh_host) {
2916
+ if (!source.ssh_user) {
2917
+ throw new Error(
2918
+ `Source '${sourceId}': SSH tunnel requires ssh_user`
2919
+ );
2920
+ }
2921
+ const sshConfig = {
2922
+ host: source.ssh_host,
2923
+ port: source.ssh_port || 22,
2924
+ username: source.ssh_user,
2925
+ password: source.ssh_password,
2926
+ privateKey: source.ssh_key,
2927
+ passphrase: source.ssh_passphrase
2928
+ };
2929
+ if (!sshConfig.password && !sshConfig.privateKey) {
2930
+ throw new Error(
2931
+ `Source '${sourceId}': SSH tunnel requires either ssh_password or ssh_key`
2932
+ );
2933
+ }
2575
2934
  const url = new URL(dsn);
2576
2935
  const targetHost = url.hostname;
2577
2936
  const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
2578
- this.sshTunnel = new SSHTunnel();
2579
- const tunnelInfo = await this.sshTunnel.establish(sshConfig.config, {
2937
+ const tunnel = new SSHTunnel();
2938
+ const tunnelInfo = await tunnel.establish(sshConfig, {
2580
2939
  targetHost,
2581
2940
  targetPort
2582
2941
  });
2583
2942
  url.hostname = "127.0.0.1";
2584
2943
  url.port = tunnelInfo.localPort.toString();
2585
2944
  actualDSN = url.toString();
2586
- console.error(`Database connection will use SSH tunnel through localhost:${tunnelInfo.localPort}`);
2945
+ this.sshTunnels.set(sourceId, tunnel);
2946
+ console.error(
2947
+ ` SSH tunnel established through localhost:${tunnelInfo.localPort}`
2948
+ );
2587
2949
  }
2588
- let connector = ConnectorRegistry.getConnectorForDSN(actualDSN);
2589
- if (!connector) {
2590
- throw new Error(`No connector found that can handle the DSN: ${actualDSN}`);
2950
+ const connectorPrototype = ConnectorRegistry.getConnectorForDSN(actualDSN);
2951
+ if (!connectorPrototype) {
2952
+ throw new Error(
2953
+ `Source '${sourceId}': No connector found for DSN: ${actualDSN}`
2954
+ );
2591
2955
  }
2592
- this.activeConnector = connector;
2593
- await this.activeConnector.connect(actualDSN, initScript);
2594
- this.connected = true;
2595
- }
2596
- /**
2597
- * Initialize and connect to the database using a specific connector type
2598
- */
2599
- async connectWithType(connectorType, dsn) {
2600
- const connector = ConnectorRegistry.getConnector(connectorType);
2601
- if (!connector) {
2602
- throw new Error(`Connector "${connectorType}" not found`);
2956
+ const connector = connectorPrototype.clone();
2957
+ const config = {};
2958
+ if (source.connection_timeout !== void 0) {
2959
+ config.connectionTimeoutSeconds = source.connection_timeout;
2960
+ }
2961
+ if (connector.id === "sqlserver" && source.request_timeout !== void 0) {
2962
+ config.requestTimeoutSeconds = source.request_timeout;
2963
+ }
2964
+ await connector.connect(actualDSN, source.init_script, config);
2965
+ this.connectors.set(sourceId, connector);
2966
+ this.sourceIds.push(sourceId);
2967
+ this.sourceConfigs.set(sourceId, source);
2968
+ const options = {};
2969
+ if (source.max_rows !== void 0) {
2970
+ options.maxRows = source.max_rows;
2971
+ }
2972
+ if (source.readonly !== void 0) {
2973
+ options.readonly = source.readonly;
2603
2974
  }
2604
- this.activeConnector = connector;
2605
- const connectionString = dsn || connector.dsnParser.getSampleDSN();
2606
- await this.activeConnector.connect(connectionString);
2607
- this.connected = true;
2975
+ this.executeOptions.set(sourceId, options);
2976
+ console.error(` Connected successfully`);
2608
2977
  }
2609
2978
  /**
2610
- * Close the database connection
2979
+ * Close all database connections
2611
2980
  */
2612
2981
  async disconnect() {
2613
- if (this.activeConnector && this.connected) {
2614
- await this.activeConnector.disconnect();
2615
- this.connected = false;
2982
+ for (const [sourceId, connector] of this.connectors.entries()) {
2983
+ try {
2984
+ await connector.disconnect();
2985
+ console.error(`Disconnected from source '${sourceId || "(default)"}'`);
2986
+ } catch (error) {
2987
+ console.error(`Error disconnecting from source '${sourceId}':`, error);
2988
+ }
2616
2989
  }
2617
- if (this.sshTunnel) {
2618
- await this.sshTunnel.close();
2619
- this.sshTunnel = null;
2990
+ for (const [sourceId, tunnel] of this.sshTunnels.entries()) {
2991
+ try {
2992
+ await tunnel.close();
2993
+ } catch (error) {
2994
+ console.error(`Error closing SSH tunnel for source '${sourceId}':`, error);
2995
+ }
2620
2996
  }
2621
- this.originalDSN = null;
2997
+ this.connectors.clear();
2998
+ this.sshTunnels.clear();
2999
+ this.executeOptions.clear();
3000
+ this.sourceConfigs.clear();
3001
+ this.sourceIds = [];
2622
3002
  }
2623
3003
  /**
2624
- * Get the active connector
3004
+ * Get a connector by source ID
3005
+ * If sourceId is not provided, returns the default (first) connector
2625
3006
  */
2626
- getConnector() {
2627
- if (!this.activeConnector) {
2628
- throw new Error("No active connector. Call connectWithDSN() or connectWithType() first.");
3007
+ getConnector(sourceId) {
3008
+ const id = sourceId || this.sourceIds[0];
3009
+ const connector = this.connectors.get(id);
3010
+ if (!connector) {
3011
+ if (sourceId) {
3012
+ throw new Error(
3013
+ `Source '${sourceId}' not found. Available sources: ${this.sourceIds.join(", ")}`
3014
+ );
3015
+ } else {
3016
+ throw new Error("No sources connected. Call connectWithSources() first.");
3017
+ }
2629
3018
  }
2630
- return this.activeConnector;
2631
- }
2632
- /**
2633
- * Check if there's an active connection
2634
- */
2635
- isConnected() {
2636
- return this.connected;
3019
+ return connector;
2637
3020
  }
2638
3021
  /**
2639
3022
  * Get all available connector types
@@ -2650,32 +3033,80 @@ var ConnectorManager = class {
2650
3033
  /**
2651
3034
  * Get the current active connector instance
2652
3035
  * This is used by resource and tool handlers
3036
+ * @param sourceId - Optional source ID. If not provided, returns default (first) connector
2653
3037
  */
2654
- static getCurrentConnector() {
3038
+ static getCurrentConnector(sourceId) {
2655
3039
  if (!managerInstance) {
2656
3040
  throw new Error("ConnectorManager not initialized");
2657
3041
  }
2658
- return managerInstance.getConnector();
3042
+ return managerInstance.getConnector(sourceId);
2659
3043
  }
2660
3044
  /**
2661
3045
  * Get execute options for SQL execution
3046
+ * @param sourceId - Optional source ID. If not provided, returns default options
2662
3047
  */
2663
- getExecuteOptions() {
2664
- const options = {};
2665
- if (this.maxRows !== null) {
2666
- options.maxRows = this.maxRows;
2667
- }
2668
- return options;
3048
+ getExecuteOptions(sourceId) {
3049
+ const id = sourceId || this.sourceIds[0];
3050
+ return this.executeOptions.get(id) || {};
2669
3051
  }
2670
3052
  /**
2671
3053
  * Get the current execute options
2672
3054
  * This is used by tool handlers
3055
+ * @param sourceId - Optional source ID. If not provided, returns default options
3056
+ */
3057
+ static getCurrentExecuteOptions(sourceId) {
3058
+ if (!managerInstance) {
3059
+ throw new Error("ConnectorManager not initialized");
3060
+ }
3061
+ return managerInstance.getExecuteOptions(sourceId);
3062
+ }
3063
+ /**
3064
+ * Get all available source IDs
3065
+ */
3066
+ getSourceIds() {
3067
+ return [...this.sourceIds];
3068
+ }
3069
+ /** Get all available source IDs */
3070
+ static getAvailableSourceIds() {
3071
+ if (!managerInstance) {
3072
+ throw new Error("ConnectorManager not initialized");
3073
+ }
3074
+ return managerInstance.getSourceIds();
3075
+ }
3076
+ /**
3077
+ * Get source configuration by ID
3078
+ * @param sourceId - Source ID. If not provided, returns default (first) source config
3079
+ */
3080
+ getSourceConfig(sourceId) {
3081
+ if (this.connectors.size === 0) {
3082
+ return null;
3083
+ }
3084
+ const id = sourceId || this.sourceIds[0];
3085
+ return this.sourceConfigs.get(id) || null;
3086
+ }
3087
+ /**
3088
+ * Get all source configurations
2673
3089
  */
2674
- static getCurrentExecuteOptions() {
3090
+ getAllSourceConfigs() {
3091
+ return this.sourceIds.map((id) => this.sourceConfigs.get(id));
3092
+ }
3093
+ /**
3094
+ * Get source configuration by ID (static method for external access)
3095
+ */
3096
+ static getSourceConfig(sourceId) {
2675
3097
  if (!managerInstance) {
2676
3098
  throw new Error("ConnectorManager not initialized");
2677
3099
  }
2678
- return managerInstance.getExecuteOptions();
3100
+ return managerInstance.getSourceConfig(sourceId);
3101
+ }
3102
+ /**
3103
+ * Get all source configurations (static method for external access)
3104
+ */
3105
+ static getAllSourceConfigs() {
3106
+ if (!managerInstance) {
3107
+ throw new Error("ConnectorManager not initialized");
3108
+ }
3109
+ return managerInstance.getAllSourceConfigs();
2679
3110
  }
2680
3111
  /**
2681
3112
  * Get default port for a database based on DSN protocol
@@ -2694,45 +3125,6 @@ var ConnectorManager = class {
2694
3125
  }
2695
3126
  };
2696
3127
 
2697
- // src/config/demo-loader.ts
2698
- import fs2 from "fs";
2699
- import path2 from "path";
2700
- import { fileURLToPath as fileURLToPath2 } from "url";
2701
- var __filename2 = fileURLToPath2(import.meta.url);
2702
- var __dirname2 = path2.dirname(__filename2);
2703
- var DEMO_DATA_DIR;
2704
- var projectRootPath = path2.join(__dirname2, "..", "..", "..");
2705
- var projectResourcesPath = path2.join(projectRootPath, "resources", "employee-sqlite");
2706
- var distPath = path2.join(__dirname2, "resources", "employee-sqlite");
2707
- if (fs2.existsSync(projectResourcesPath)) {
2708
- DEMO_DATA_DIR = projectResourcesPath;
2709
- } else if (fs2.existsSync(distPath)) {
2710
- DEMO_DATA_DIR = distPath;
2711
- } else {
2712
- DEMO_DATA_DIR = path2.join(process.cwd(), "resources", "employee-sqlite");
2713
- if (!fs2.existsSync(DEMO_DATA_DIR)) {
2714
- throw new Error(`Could not find employee-sqlite resources in any of the expected locations:
2715
- - ${projectResourcesPath}
2716
- - ${distPath}
2717
- - ${DEMO_DATA_DIR}`);
2718
- }
2719
- }
2720
- function loadSqlFile(fileName) {
2721
- const filePath = path2.join(DEMO_DATA_DIR, fileName);
2722
- return fs2.readFileSync(filePath, "utf8");
2723
- }
2724
- function getSqliteInMemorySetupSql() {
2725
- let sql2 = loadSqlFile("employee.sql");
2726
- const readRegex = /\.read\s+([a-zA-Z0-9_]+\.sql)/g;
2727
- let match;
2728
- while ((match = readRegex.exec(sql2)) !== null) {
2729
- const includePath = match[1];
2730
- const includeContent = loadSqlFile(includePath);
2731
- sql2 = sql2.replace(match[0], includeContent);
2732
- }
2733
- return sql2;
2734
- }
2735
-
2736
3128
  // src/resources/index.ts
2737
3129
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2738
3130
 
@@ -3091,7 +3483,8 @@ var allowedKeywords = {
3091
3483
 
3092
3484
  // src/tools/execute-sql.ts
3093
3485
  var executeSqlSchema = {
3094
- sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
3486
+ sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)"),
3487
+ source_id: z.string().optional().describe("Database source ID to execute the query against (optional, defaults to first configured source)")
3095
3488
  };
3096
3489
  function splitSQLStatements(sql2) {
3097
3490
  return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
@@ -3117,10 +3510,10 @@ function areAllStatementsReadOnly(sql2, connectorType) {
3117
3510
  const statements = splitSQLStatements(sql2);
3118
3511
  return statements.every((statement) => isReadOnlySQL(statement, connectorType));
3119
3512
  }
3120
- async function executeSqlToolHandler({ sql: sql2 }, _extra) {
3121
- const connector = ConnectorManager.getCurrentConnector();
3122
- const executeOptions = ConnectorManager.getCurrentExecuteOptions();
3513
+ async function executeSqlToolHandler({ sql: sql2, source_id }, _extra) {
3123
3514
  try {
3515
+ const connector = ConnectorManager.getCurrentConnector(source_id);
3516
+ const executeOptions = ConnectorManager.getCurrentExecuteOptions(source_id);
3124
3517
  if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
3125
3518
  return createToolErrorResponse(
3126
3519
  `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
@@ -3130,7 +3523,9 @@ async function executeSqlToolHandler({ sql: sql2 }, _extra) {
3130
3523
  const result = await connector.executeSQL(sql2, executeOptions);
3131
3524
  const responseData = {
3132
3525
  rows: result.rows,
3133
- count: result.rows.length
3526
+ count: result.rows.length,
3527
+ source_id: source_id || "(default)"
3528
+ // Include source_id in response for clarity
3134
3529
  };
3135
3530
  return createToolSuccessResponse(responseData);
3136
3531
  } catch (error) {
@@ -3564,10 +3959,100 @@ function registerPrompts(server) {
3564
3959
  );
3565
3960
  }
3566
3961
 
3962
+ // src/api/sources.ts
3963
+ function transformSourceConfig(source, isDefault) {
3964
+ if (!source.type && source.dsn) {
3965
+ const inferredType = getDatabaseTypeFromDSN(source.dsn);
3966
+ if (inferredType) {
3967
+ source.type = inferredType;
3968
+ }
3969
+ }
3970
+ if (!source.type) {
3971
+ throw new Error(`Source ${source.id} is missing required type field`);
3972
+ }
3973
+ const dataSource = {
3974
+ id: source.id,
3975
+ type: source.type,
3976
+ is_default: isDefault
3977
+ };
3978
+ if (source.host) {
3979
+ dataSource.host = source.host;
3980
+ }
3981
+ if (source.port !== void 0) {
3982
+ dataSource.port = source.port;
3983
+ }
3984
+ if (source.database) {
3985
+ dataSource.database = source.database;
3986
+ }
3987
+ if (source.user) {
3988
+ dataSource.user = source.user;
3989
+ }
3990
+ if (source.readonly !== void 0) {
3991
+ dataSource.readonly = source.readonly;
3992
+ }
3993
+ if (source.max_rows !== void 0) {
3994
+ dataSource.max_rows = source.max_rows;
3995
+ }
3996
+ if (source.ssh_host) {
3997
+ const sshTunnel = {
3998
+ enabled: true,
3999
+ ssh_host: source.ssh_host
4000
+ };
4001
+ if (source.ssh_port !== void 0) {
4002
+ sshTunnel.ssh_port = source.ssh_port;
4003
+ }
4004
+ if (source.ssh_user) {
4005
+ sshTunnel.ssh_user = source.ssh_user;
4006
+ }
4007
+ dataSource.ssh_tunnel = sshTunnel;
4008
+ }
4009
+ return dataSource;
4010
+ }
4011
+ function listSources(req, res) {
4012
+ try {
4013
+ const sourceConfigs = ConnectorManager.getAllSourceConfigs();
4014
+ const sources = sourceConfigs.map((config, index) => {
4015
+ const isDefault = index === 0;
4016
+ return transformSourceConfig(config, isDefault);
4017
+ });
4018
+ res.json(sources);
4019
+ } catch (error) {
4020
+ console.error("Error listing sources:", error);
4021
+ const errorResponse = {
4022
+ error: error instanceof Error ? error.message : "Internal server error"
4023
+ };
4024
+ res.status(500).json(errorResponse);
4025
+ }
4026
+ }
4027
+ function getSource(req, res) {
4028
+ try {
4029
+ const sourceId = req.params.sourceId;
4030
+ const sourceIds = ConnectorManager.getAvailableSourceIds();
4031
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
4032
+ if (!sourceConfig) {
4033
+ const errorResponse = {
4034
+ error: "Source not found",
4035
+ source_id: sourceId
4036
+ };
4037
+ res.status(404).json(errorResponse);
4038
+ return;
4039
+ }
4040
+ const isDefault = sourceIds[0] === sourceId;
4041
+ const dataSource = transformSourceConfig(sourceConfig, isDefault);
4042
+ res.json(dataSource);
4043
+ } catch (error) {
4044
+ console.error(`Error getting source ${req.params.sourceId}:`, error);
4045
+ const errorResponse = {
4046
+ error: error instanceof Error ? error.message : "Internal server error"
4047
+ };
4048
+ res.status(500).json(errorResponse);
4049
+ }
4050
+ }
4051
+
3567
4052
  // src/server.ts
3568
- var __filename3 = fileURLToPath3(import.meta.url);
3569
- var __dirname3 = path3.dirname(__filename3);
3570
- var packageJsonPath = path3.join(__dirname3, "..", "package.json");
4053
+ var __filename2 = fileURLToPath2(import.meta.url);
4054
+ var __dirname2 = path3.dirname(__filename2);
4055
+ var packageJsonPath = path3.join(__dirname2, "..", "package.json");
3571
4056
  var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
3572
4057
  var SERVER_NAME = "DBHub MCP Server";
3573
4058
  var SERVER_VERSION = packageJson.version;
@@ -3588,22 +4073,28 @@ async function main() {
3588
4073
  try {
3589
4074
  const idData = resolveId();
3590
4075
  const id = idData?.id;
3591
- const dsnData = resolveDSN();
3592
- if (!dsnData) {
4076
+ const sourceConfigsData = await resolveSourceConfigs();
4077
+ if (!sourceConfigsData) {
3593
4078
  const samples = ConnectorRegistry.getAllSampleDSNs();
3594
4079
  const sampleFormats = Object.entries(samples).map(([id2, dsn]) => ` - ${id2}: ${dsn}`).join("\n");
3595
4080
  console.error(`
3596
- ERROR: Database connection string (DSN) is required.
3597
- Please provide the DSN in one of these ways (in order of priority):
4081
+ ERROR: Database connection configuration is required.
4082
+ Please provide configuration in one of these ways (in order of priority):
3598
4083
 
3599
4084
  1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
3600
- 2. Command line argument: --dsn="your-connection-string"
3601
- 3. Environment variable: export DSN="your-connection-string"
3602
- 4. .env file: DSN=your-connection-string
4085
+ 2. TOML config file: --config=path/to/dbhub.toml or ./dbhub.toml
4086
+ 3. Command line argument: --dsn="your-connection-string"
4087
+ 4. Environment variable: export DSN="your-connection-string"
4088
+ 5. .env file: DSN=your-connection-string
3603
4089
 
3604
- Example formats:
4090
+ Example DSN formats:
3605
4091
  ${sampleFormats}
3606
4092
 
4093
+ Example TOML config (dbhub.toml):
4094
+ [[sources]]
4095
+ id = "my_db"
4096
+ dsn = "postgres://user:pass@localhost:5432/dbname"
4097
+
3607
4098
  See documentation for more details on configuring database connections.
3608
4099
  `);
3609
4100
  process.exit(1);
@@ -3619,24 +4110,28 @@ See documentation for more details on configuring database connections.
3619
4110
  return server;
3620
4111
  };
3621
4112
  const connectorManager = new ConnectorManager();
3622
- console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
3623
- console.error(`DSN source: ${dsnData.source}`);
4113
+ const sources = sourceConfigsData.sources;
4114
+ console.error(`Configuration source: ${sourceConfigsData.source}`);
3624
4115
  if (idData) {
3625
4116
  console.error(`ID: ${idData.id} (from ${idData.source})`);
3626
4117
  }
3627
- if (dsnData.isDemo) {
3628
- const initScript = getSqliteInMemorySetupSql();
3629
- await connectorManager.connectWithDSN(dsnData.dsn, initScript);
3630
- } else {
3631
- await connectorManager.connectWithDSN(dsnData.dsn);
4118
+ console.error(`Connecting to ${sources.length} database source(s)...`);
4119
+ for (const source of sources) {
4120
+ const dsn = source.dsn || buildDSNFromSource(source);
4121
+ console.error(` - ${source.id}: ${redactDSN(dsn)}`);
3632
4122
  }
4123
+ await connectorManager.connectWithSources(sources);
3633
4124
  const transportData = resolveTransport();
3634
- console.error(`Using transport: ${transportData.type}`);
4125
+ console.error(`MCP transport: ${transportData.type}`);
3635
4126
  console.error(`Transport source: ${transportData.source}`);
4127
+ const portData = resolvePort();
4128
+ const port = portData.port;
4129
+ console.error(`HTTP server port: ${port} (source: ${portData.source})`);
3636
4130
  const readonly = isReadOnlyMode();
3637
4131
  const activeModes = [];
3638
4132
  const modeDescriptions = [];
3639
- if (dsnData.isDemo) {
4133
+ const isDemo = isDemoMode();
4134
+ if (isDemo) {
3640
4135
  activeModes.push("DEMO");
3641
4136
  modeDescriptions.push("using sample employee database");
3642
4137
  }
@@ -3644,34 +4139,38 @@ See documentation for more details on configuring database connections.
3644
4139
  activeModes.push("READ-ONLY");
3645
4140
  modeDescriptions.push("only read only queries allowed");
3646
4141
  }
4142
+ if (sources.length > 1) {
4143
+ console.error(`Multi-source mode: ${sources.length} databases configured`);
4144
+ }
3647
4145
  if (activeModes.length > 0) {
3648
4146
  console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
3649
4147
  }
3650
4148
  console.error(generateBanner(SERVER_VERSION, activeModes));
4149
+ const app = express();
4150
+ app.use(express.json());
4151
+ app.use((req, res, next) => {
4152
+ const origin = req.headers.origin;
4153
+ if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("https://localhost")) {
4154
+ return res.status(403).json({ error: "Forbidden origin" });
4155
+ }
4156
+ res.header("Access-Control-Allow-Origin", origin || "http://localhost");
4157
+ res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4158
+ res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
4159
+ res.header("Access-Control-Allow-Credentials", "true");
4160
+ if (req.method === "OPTIONS") {
4161
+ return res.sendStatus(200);
4162
+ }
4163
+ next();
4164
+ });
4165
+ const frontendPath = path3.join(__dirname2, "public");
4166
+ app.use(express.static(frontendPath));
4167
+ app.get("/healthz", (req, res) => {
4168
+ res.status(200).send("OK");
4169
+ });
4170
+ app.get("/api/sources", listSources);
4171
+ app.get("/api/sources/:sourceId", getSource);
3651
4172
  if (transportData.type === "http") {
3652
- const app = express();
3653
- app.use(express.json());
3654
- app.use((req, res, next) => {
3655
- const origin = req.headers.origin;
3656
- if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("https://localhost")) {
3657
- return res.status(403).json({ error: "Forbidden origin" });
3658
- }
3659
- res.header("Access-Control-Allow-Origin", origin || "http://localhost");
3660
- res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3661
- res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
3662
- res.header("Access-Control-Allow-Credentials", "true");
3663
- if (req.method === "OPTIONS") {
3664
- return res.sendStatus(200);
3665
- }
3666
- next();
3667
- });
3668
- app.get("/", (req, res) => {
3669
- res.status(200).send();
3670
- });
3671
- app.get("/healthz", (req, res) => {
3672
- res.status(200).send("OK");
3673
- });
3674
- app.post("/message", async (req, res) => {
4173
+ app.post("/mcp", async (req, res) => {
3675
4174
  try {
3676
4175
  const transport = new StreamableHTTPServerTransport({
3677
4176
  sessionIdGenerator: void 0,
@@ -3689,17 +4188,29 @@ See documentation for more details on configuring database connections.
3689
4188
  }
3690
4189
  }
3691
4190
  });
3692
- const portData = resolvePort();
3693
- const port = portData.port;
3694
- console.error(`Port source: ${portData.source}`);
3695
- app.listen(port, "0.0.0.0", () => {
3696
- console.error(`DBHub server listening at http://0.0.0.0:${port}`);
3697
- console.error(`Connect to MCP server at http://0.0.0.0:${port}/message`);
3698
- });
3699
- } else {
4191
+ }
4192
+ app.get("*", (req, res) => {
4193
+ res.sendFile(path3.join(frontendPath, "index.html"));
4194
+ });
4195
+ app.listen(port, "0.0.0.0", () => {
4196
+ console.error(`DBHub HTTP server listening at http://0.0.0.0:${port}`);
4197
+ if (process.env.NODE_ENV === "development") {
4198
+ console.error("");
4199
+ console.error("\u{1F680} Development mode detected!");
4200
+ console.error(" Frontend dev server (with HMR): http://localhost:5173");
4201
+ console.error(" Backend API: http://localhost:8080");
4202
+ console.error("");
4203
+ } else {
4204
+ console.error(`Frontend accessible at http://0.0.0.0:${port}/`);
4205
+ }
4206
+ if (transportData.type === "http") {
4207
+ console.error(`MCP server endpoint at http://0.0.0.0:${port}/mcp`);
4208
+ }
4209
+ });
4210
+ if (transportData.type === "stdio") {
3700
4211
  const server = createServer2();
3701
4212
  const transport = new StdioServerTransport();
3702
- console.error("Starting with STDIO transport");
4213
+ console.error("Starting MCP with STDIO transport");
3703
4214
  await server.connect(transport);
3704
4215
  process.on("SIGINT", async () => {
3705
4216
  console.error("Shutting down...");