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