@bytebase/dbhub 0.11.6 → 0.11.7

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