@bytebase/dbhub 0.11.5 → 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/README.md +28 -623
- package/dist/demo-loader-EOVFD32T.js +46 -0
- package/dist/index.js +751 -239
- package/package.json +14 -11
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
|
|
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
|
-
|
|
321
|
+
config2.ssl = false;
|
|
306
322
|
} else if (value === "require") {
|
|
307
|
-
|
|
323
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
308
324
|
} else {
|
|
309
|
-
|
|
325
|
+
config2.ssl = true;
|
|
310
326
|
}
|
|
311
327
|
}
|
|
312
328
|
});
|
|
313
|
-
|
|
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
|
-
|
|
357
|
+
clone() {
|
|
358
|
+
return new _PostgresConnector();
|
|
359
|
+
}
|
|
360
|
+
async connect(dsn, initScript, config) {
|
|
339
361
|
try {
|
|
340
|
-
const
|
|
341
|
-
this.pool = new Pool(
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
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
|
-
|
|
1339
|
+
config2.ssl = void 0;
|
|
1262
1340
|
} else if (value === "require") {
|
|
1263
|
-
|
|
1341
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1264
1342
|
} else {
|
|
1265
|
-
|
|
1343
|
+
config2.ssl = {};
|
|
1266
1344
|
}
|
|
1267
1345
|
}
|
|
1268
1346
|
});
|
|
1269
|
-
|
|
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
|
-
|
|
1375
|
+
clone() {
|
|
1376
|
+
return new _MySQLConnector();
|
|
1377
|
+
}
|
|
1378
|
+
async connect(dsn, initScript, config) {
|
|
1295
1379
|
try {
|
|
1296
|
-
const
|
|
1297
|
-
this.pool = mysql.createPool(
|
|
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
|
|
1682
|
+
const results = await conn.query(processedSQL);
|
|
1598
1683
|
const [firstResult] = results;
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
|
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
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
1729
|
+
config2.ssl = void 0;
|
|
1650
1730
|
} else if (value === "require") {
|
|
1651
|
-
|
|
1731
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1652
1732
|
} else {
|
|
1653
|
-
|
|
1733
|
+
config2.ssl = {};
|
|
1654
1734
|
}
|
|
1655
1735
|
}
|
|
1656
1736
|
});
|
|
1657
|
-
return
|
|
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
|
-
|
|
1762
|
+
clone() {
|
|
1763
|
+
return new _MariaDBConnector();
|
|
1764
|
+
}
|
|
1765
|
+
async connect(dsn, initScript, config) {
|
|
1683
1766
|
try {
|
|
1684
|
-
const
|
|
1685
|
-
this.pool = mariadb.createPool(
|
|
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
|
|
1987
|
-
|
|
1988
|
-
|
|
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
|
|
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
|
-
|
|
2553
|
-
this.
|
|
2554
|
-
this.
|
|
2555
|
-
this.
|
|
2556
|
-
this.
|
|
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
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
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
|
-
*
|
|
2906
|
+
* Connect to a single source (helper for connectWithSources)
|
|
2568
2907
|
*/
|
|
2569
|
-
async
|
|
2570
|
-
|
|
2571
|
-
|
|
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 (
|
|
2574
|
-
|
|
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
|
-
|
|
2579
|
-
const tunnelInfo = await
|
|
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
|
-
|
|
2943
|
+
this.sshTunnels.set(sourceId, tunnel);
|
|
2944
|
+
console.error(
|
|
2945
|
+
` SSH tunnel established through localhost:${tunnelInfo.localPort}`
|
|
2946
|
+
);
|
|
2587
2947
|
}
|
|
2588
|
-
|
|
2589
|
-
if (!
|
|
2590
|
-
throw new Error(
|
|
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
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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;
|
|
2603
2961
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
this.
|
|
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;
|
|
2972
|
+
}
|
|
2973
|
+
this.executeOptions.set(sourceId, options);
|
|
2974
|
+
console.error(` Connected successfully`);
|
|
2608
2975
|
}
|
|
2609
2976
|
/**
|
|
2610
|
-
* Close
|
|
2977
|
+
* Close all database connections
|
|
2611
2978
|
*/
|
|
2612
2979
|
async disconnect() {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
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.
|
|
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
|
|
3002
|
+
* Get a connector by source ID
|
|
3003
|
+
* If sourceId is not provided, returns the default (first) connector
|
|
2625
3004
|
*/
|
|
2626
|
-
getConnector() {
|
|
2627
|
-
|
|
2628
|
-
|
|
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
|
|
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
|
|
2665
|
-
|
|
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
|
|
3087
|
+
*/
|
|
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)
|
|
2673
3093
|
*/
|
|
2674
|
-
static
|
|
3094
|
+
static getSourceConfig(sourceId) {
|
|
2675
3095
|
if (!managerInstance) {
|
|
2676
3096
|
throw new Error("ConnectorManager not initialized");
|
|
2677
3097
|
}
|
|
2678
|
-
return managerInstance.
|
|
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
|
|
3569
|
-
var
|
|
3570
|
-
var packageJsonPath = path3.join(
|
|
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
|
|
3592
|
-
if (!
|
|
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
|
|
3597
|
-
Please provide
|
|
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.
|
|
3601
|
-
3.
|
|
3602
|
-
4.
|
|
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
|
-
|
|
3623
|
-
console.error(`
|
|
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
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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(`
|
|
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
|
-
|
|
4131
|
+
const isDemo = isDemoMode();
|
|
4132
|
+
if (isDemo) {
|
|
3640
4133
|
activeModes.push("DEMO");
|
|
3641
4134
|
modeDescriptions.push("using sample employee database");
|
|
3642
4135
|
}
|
|
@@ -3644,31 +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
|
-
|
|
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("/healthz", (req, res) => {
|
|
3669
|
-
res.status(200).send("OK");
|
|
3670
|
-
});
|
|
3671
|
-
app.post("/message", async (req, res) => {
|
|
4171
|
+
app.post("/mcp", async (req, res) => {
|
|
3672
4172
|
try {
|
|
3673
4173
|
const transport = new StreamableHTTPServerTransport({
|
|
3674
4174
|
sessionIdGenerator: void 0,
|
|
@@ -3686,17 +4186,29 @@ See documentation for more details on configuring database connections.
|
|
|
3686
4186
|
}
|
|
3687
4187
|
}
|
|
3688
4188
|
});
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
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") {
|
|
3697
4209
|
const server = createServer2();
|
|
3698
4210
|
const transport = new StdioServerTransport();
|
|
3699
|
-
console.error("Starting with STDIO transport");
|
|
4211
|
+
console.error("Starting MCP with STDIO transport");
|
|
3700
4212
|
await server.connect(transport);
|
|
3701
4213
|
process.on("SIGINT", async () => {
|
|
3702
4214
|
console.error("Shutting down...");
|