@bytebase/dbhub 0.9.0 → 0.11.0
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 +137 -45
- package/dist/index.js +638 -74
- package/dist/resources/employee-sqlite/employee.sql +117 -0
- package/dist/resources/employee-sqlite/load_department.sql +10 -0
- package/dist/resources/employee-sqlite/load_dept_emp.sql +1103 -0
- package/dist/resources/employee-sqlite/load_dept_manager.sql +17 -0
- package/dist/resources/employee-sqlite/load_employee.sql +1000 -0
- package/dist/resources/employee-sqlite/load_salary1.sql +9488 -0
- package/dist/resources/employee-sqlite/load_title.sql +1470 -0
- package/dist/resources/employee-sqlite/object.sql +74 -0
- package/dist/resources/employee-sqlite/show_elapsed.sql +4 -0
- package/dist/resources/employee-sqlite/test_employee_md5.sql +119 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -159,6 +159,9 @@ function obfuscateDSNPassword(dsn) {
|
|
|
159
159
|
return dsn;
|
|
160
160
|
}
|
|
161
161
|
const protocol = protocolMatch[1];
|
|
162
|
+
if (protocol === "sqlite") {
|
|
163
|
+
return dsn;
|
|
164
|
+
}
|
|
162
165
|
const protocolPart = dsn.split("://")[1];
|
|
163
166
|
if (!protocolPart) {
|
|
164
167
|
return dsn;
|
|
@@ -876,6 +879,246 @@ var SQLServerConnector = class {
|
|
|
876
879
|
var sqlServerConnector = new SQLServerConnector();
|
|
877
880
|
ConnectorRegistry.register(sqlServerConnector);
|
|
878
881
|
|
|
882
|
+
// src/connectors/sqlite/index.ts
|
|
883
|
+
import Database from "better-sqlite3";
|
|
884
|
+
var SQLiteDSNParser = class {
|
|
885
|
+
async parse(dsn) {
|
|
886
|
+
if (!this.isValidDSN(dsn)) {
|
|
887
|
+
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
888
|
+
const expectedFormat = this.getSampleDSN();
|
|
889
|
+
throw new Error(
|
|
890
|
+
`Invalid SQLite DSN format.
|
|
891
|
+
Provided: ${obfuscatedDSN}
|
|
892
|
+
Expected: ${expectedFormat}`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const url = new SafeURL(dsn);
|
|
897
|
+
let dbPath;
|
|
898
|
+
if (url.hostname === "" && url.pathname === "/:memory:") {
|
|
899
|
+
dbPath = ":memory:";
|
|
900
|
+
} else {
|
|
901
|
+
if (url.pathname.startsWith("//")) {
|
|
902
|
+
dbPath = url.pathname.substring(2);
|
|
903
|
+
} else {
|
|
904
|
+
dbPath = url.pathname;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return { dbPath };
|
|
908
|
+
} catch (error) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
getSampleDSN() {
|
|
915
|
+
return "sqlite:///path/to/database.db";
|
|
916
|
+
}
|
|
917
|
+
isValidDSN(dsn) {
|
|
918
|
+
try {
|
|
919
|
+
return dsn.startsWith("sqlite://");
|
|
920
|
+
} catch (error) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
var SQLiteConnector = class {
|
|
926
|
+
constructor() {
|
|
927
|
+
this.id = "sqlite";
|
|
928
|
+
this.name = "SQLite";
|
|
929
|
+
this.dsnParser = new SQLiteDSNParser();
|
|
930
|
+
this.db = null;
|
|
931
|
+
this.dbPath = ":memory:";
|
|
932
|
+
}
|
|
933
|
+
// Default to in-memory database
|
|
934
|
+
async connect(dsn, initScript) {
|
|
935
|
+
const config = await this.dsnParser.parse(dsn);
|
|
936
|
+
this.dbPath = config.dbPath;
|
|
937
|
+
try {
|
|
938
|
+
this.db = new Database(this.dbPath);
|
|
939
|
+
console.error("Successfully connected to SQLite database");
|
|
940
|
+
if (initScript) {
|
|
941
|
+
this.db.exec(initScript);
|
|
942
|
+
console.error("Successfully initialized database with script");
|
|
943
|
+
}
|
|
944
|
+
} catch (error) {
|
|
945
|
+
console.error("Failed to connect to SQLite database:", error);
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async disconnect() {
|
|
950
|
+
if (this.db) {
|
|
951
|
+
try {
|
|
952
|
+
this.db.close();
|
|
953
|
+
this.db = null;
|
|
954
|
+
} catch (error) {
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return Promise.resolve();
|
|
959
|
+
}
|
|
960
|
+
async getSchemas() {
|
|
961
|
+
if (!this.db) {
|
|
962
|
+
throw new Error("Not connected to SQLite database");
|
|
963
|
+
}
|
|
964
|
+
return ["main"];
|
|
965
|
+
}
|
|
966
|
+
async getTables(schema) {
|
|
967
|
+
if (!this.db) {
|
|
968
|
+
throw new Error("Not connected to SQLite database");
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const rows = this.db.prepare(
|
|
972
|
+
`
|
|
973
|
+
SELECT name FROM sqlite_master
|
|
974
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
975
|
+
ORDER BY name
|
|
976
|
+
`
|
|
977
|
+
).all();
|
|
978
|
+
return rows.map((row) => row.name);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async tableExists(tableName, schema) {
|
|
984
|
+
if (!this.db) {
|
|
985
|
+
throw new Error("Not connected to SQLite database");
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const row = this.db.prepare(
|
|
989
|
+
`
|
|
990
|
+
SELECT name FROM sqlite_master
|
|
991
|
+
WHERE type='table' AND name = ?
|
|
992
|
+
`
|
|
993
|
+
).get(tableName);
|
|
994
|
+
return !!row;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async getTableIndexes(tableName, schema) {
|
|
1000
|
+
if (!this.db) {
|
|
1001
|
+
throw new Error("Not connected to SQLite database");
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const indexInfoRows = this.db.prepare(
|
|
1005
|
+
`
|
|
1006
|
+
SELECT
|
|
1007
|
+
name as index_name,
|
|
1008
|
+
0 as is_unique
|
|
1009
|
+
FROM sqlite_master
|
|
1010
|
+
WHERE type = 'index'
|
|
1011
|
+
AND tbl_name = ?
|
|
1012
|
+
`
|
|
1013
|
+
).all(tableName);
|
|
1014
|
+
const indexListRows = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
|
|
1015
|
+
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
1016
|
+
for (const indexListRow of indexListRows) {
|
|
1017
|
+
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
1018
|
+
}
|
|
1019
|
+
const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
1020
|
+
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
1021
|
+
const results = [];
|
|
1022
|
+
for (const indexInfo of indexInfoRows) {
|
|
1023
|
+
const indexDetailRows = this.db.prepare(`PRAGMA index_info(${indexInfo.index_name})`).all();
|
|
1024
|
+
const columnNames = indexDetailRows.map((row) => row.name);
|
|
1025
|
+
results.push({
|
|
1026
|
+
index_name: indexInfo.index_name,
|
|
1027
|
+
column_names: columnNames,
|
|
1028
|
+
is_unique: indexUniqueMap.get(indexInfo.index_name) || false,
|
|
1029
|
+
is_primary: false
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (pkColumns.length > 0) {
|
|
1033
|
+
results.push({
|
|
1034
|
+
index_name: "PRIMARY",
|
|
1035
|
+
column_names: pkColumns,
|
|
1036
|
+
is_unique: true,
|
|
1037
|
+
is_primary: true
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return results;
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async getTableSchema(tableName, schema) {
|
|
1046
|
+
if (!this.db) {
|
|
1047
|
+
throw new Error("Not connected to SQLite database");
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
1051
|
+
const columns = rows.map((row) => ({
|
|
1052
|
+
column_name: row.name,
|
|
1053
|
+
data_type: row.type,
|
|
1054
|
+
// In SQLite, primary key columns are automatically NOT NULL even if notnull=0
|
|
1055
|
+
is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
|
|
1056
|
+
column_default: row.dflt_value
|
|
1057
|
+
}));
|
|
1058
|
+
return columns;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async getStoredProcedures(schema) {
|
|
1064
|
+
if (!this.db) {
|
|
1065
|
+
throw new Error("Not connected to SQLite database");
|
|
1066
|
+
}
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
async getStoredProcedureDetail(procedureName, schema) {
|
|
1070
|
+
if (!this.db) {
|
|
1071
|
+
throw new Error("Not connected to SQLite database");
|
|
1072
|
+
}
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
async executeSQL(sql2) {
|
|
1078
|
+
if (!this.db) {
|
|
1079
|
+
throw new Error("Not connected to SQLite database");
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1083
|
+
if (statements.length === 1) {
|
|
1084
|
+
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
1085
|
+
const isReadStatement = trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"));
|
|
1086
|
+
if (isReadStatement) {
|
|
1087
|
+
const rows = this.db.prepare(statements[0]).all();
|
|
1088
|
+
return { rows };
|
|
1089
|
+
} else {
|
|
1090
|
+
this.db.prepare(statements[0]).run();
|
|
1091
|
+
return { rows: [] };
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
const readStatements = [];
|
|
1095
|
+
const writeStatements = [];
|
|
1096
|
+
for (const statement of statements) {
|
|
1097
|
+
const trimmedStatement = statement.toLowerCase().trim();
|
|
1098
|
+
if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"))) {
|
|
1099
|
+
readStatements.push(statement);
|
|
1100
|
+
} else {
|
|
1101
|
+
writeStatements.push(statement);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (writeStatements.length > 0) {
|
|
1105
|
+
this.db.exec(writeStatements.join("; "));
|
|
1106
|
+
}
|
|
1107
|
+
let allRows = [];
|
|
1108
|
+
for (const statement of readStatements) {
|
|
1109
|
+
const result = this.db.prepare(statement).all();
|
|
1110
|
+
allRows.push(...result);
|
|
1111
|
+
}
|
|
1112
|
+
return { rows: allRows };
|
|
1113
|
+
}
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
var sqliteConnector = new SQLiteConnector();
|
|
1120
|
+
ConnectorRegistry.register(sqliteConnector);
|
|
1121
|
+
|
|
879
1122
|
// src/connectors/mysql/index.ts
|
|
880
1123
|
import mysql from "mysql2/promise";
|
|
881
1124
|
var MySQLDSNParser = class {
|
|
@@ -1637,90 +1880,149 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
|
|
|
1637
1880
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1638
1881
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1639
1882
|
import express from "express";
|
|
1640
|
-
import
|
|
1641
|
-
import { readFileSync } from "fs";
|
|
1642
|
-
import { fileURLToPath as
|
|
1883
|
+
import path3 from "path";
|
|
1884
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1885
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1643
1886
|
|
|
1644
|
-
// src/
|
|
1645
|
-
|
|
1646
|
-
|
|
1887
|
+
// src/utils/ssh-tunnel.ts
|
|
1888
|
+
import { Client } from "ssh2";
|
|
1889
|
+
import { readFileSync } from "fs";
|
|
1890
|
+
import { createServer } from "net";
|
|
1891
|
+
var SSHTunnel = class {
|
|
1647
1892
|
constructor() {
|
|
1648
|
-
this.
|
|
1649
|
-
this.
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
}
|
|
1893
|
+
this.sshClient = null;
|
|
1894
|
+
this.localServer = null;
|
|
1895
|
+
this.tunnelInfo = null;
|
|
1896
|
+
this.isConnected = false;
|
|
1653
1897
|
}
|
|
1654
1898
|
/**
|
|
1655
|
-
*
|
|
1899
|
+
* Establish an SSH tunnel
|
|
1900
|
+
* @param config SSH connection configuration
|
|
1901
|
+
* @param options Tunnel options including target host and port
|
|
1902
|
+
* @returns Promise resolving to tunnel information including local port
|
|
1656
1903
|
*/
|
|
1657
|
-
async
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1904
|
+
async establish(config, options) {
|
|
1905
|
+
if (this.isConnected) {
|
|
1906
|
+
throw new Error("SSH tunnel is already established");
|
|
1907
|
+
}
|
|
1908
|
+
return new Promise((resolve, reject) => {
|
|
1909
|
+
this.sshClient = new Client();
|
|
1910
|
+
const sshConfig = {
|
|
1911
|
+
host: config.host,
|
|
1912
|
+
port: config.port || 22,
|
|
1913
|
+
username: config.username
|
|
1914
|
+
};
|
|
1915
|
+
if (config.password) {
|
|
1916
|
+
sshConfig.password = config.password;
|
|
1917
|
+
} else if (config.privateKey) {
|
|
1918
|
+
try {
|
|
1919
|
+
const privateKey = readFileSync(config.privateKey);
|
|
1920
|
+
sshConfig.privateKey = privateKey;
|
|
1921
|
+
if (config.passphrase) {
|
|
1922
|
+
sshConfig.passphrase = config.passphrase;
|
|
1923
|
+
}
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
reject(new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`));
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
} else {
|
|
1929
|
+
reject(new Error("Either password or privateKey must be provided for SSH authentication"));
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
this.sshClient.on("error", (err) => {
|
|
1933
|
+
this.cleanup();
|
|
1934
|
+
reject(new Error(`SSH connection error: ${err.message}`));
|
|
1935
|
+
});
|
|
1936
|
+
this.sshClient.on("ready", () => {
|
|
1937
|
+
console.error("SSH connection established");
|
|
1938
|
+
this.localServer = createServer((localSocket) => {
|
|
1939
|
+
this.sshClient.forwardOut(
|
|
1940
|
+
"127.0.0.1",
|
|
1941
|
+
0,
|
|
1942
|
+
options.targetHost,
|
|
1943
|
+
options.targetPort,
|
|
1944
|
+
(err, stream) => {
|
|
1945
|
+
if (err) {
|
|
1946
|
+
console.error("SSH forward error:", err);
|
|
1947
|
+
localSocket.end();
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
localSocket.pipe(stream).pipe(localSocket);
|
|
1951
|
+
stream.on("error", (err2) => {
|
|
1952
|
+
console.error("SSH stream error:", err2);
|
|
1953
|
+
localSocket.end();
|
|
1954
|
+
});
|
|
1955
|
+
localSocket.on("error", (err2) => {
|
|
1956
|
+
console.error("Local socket error:", err2);
|
|
1957
|
+
stream.end();
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
);
|
|
1961
|
+
});
|
|
1962
|
+
const localPort = options.localPort || 0;
|
|
1963
|
+
this.localServer.listen(localPort, "127.0.0.1", () => {
|
|
1964
|
+
const address = this.localServer.address();
|
|
1965
|
+
if (!address || typeof address === "string") {
|
|
1966
|
+
this.cleanup();
|
|
1967
|
+
reject(new Error("Failed to get local server address"));
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
this.tunnelInfo = {
|
|
1971
|
+
localPort: address.port,
|
|
1972
|
+
targetHost: options.targetHost,
|
|
1973
|
+
targetPort: options.targetPort
|
|
1974
|
+
};
|
|
1975
|
+
this.isConnected = true;
|
|
1976
|
+
console.error(`SSH tunnel established: localhost:${address.port} -> ${options.targetHost}:${options.targetPort}`);
|
|
1977
|
+
resolve(this.tunnelInfo);
|
|
1978
|
+
});
|
|
1979
|
+
this.localServer.on("error", (err) => {
|
|
1980
|
+
this.cleanup();
|
|
1981
|
+
reject(new Error(`Local server error: ${err.message}`));
|
|
1982
|
+
});
|
|
1983
|
+
});
|
|
1984
|
+
this.sshClient.connect(sshConfig);
|
|
1985
|
+
});
|
|
1665
1986
|
}
|
|
1666
1987
|
/**
|
|
1667
|
-
*
|
|
1988
|
+
* Close the SSH tunnel and clean up resources
|
|
1668
1989
|
*/
|
|
1669
|
-
async
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1990
|
+
async close() {
|
|
1991
|
+
if (!this.isConnected) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
return new Promise((resolve) => {
|
|
1995
|
+
this.cleanup();
|
|
1996
|
+
this.isConnected = false;
|
|
1997
|
+
console.error("SSH tunnel closed");
|
|
1998
|
+
resolve();
|
|
1999
|
+
});
|
|
1678
2000
|
}
|
|
1679
2001
|
/**
|
|
1680
|
-
*
|
|
2002
|
+
* Clean up resources
|
|
1681
2003
|
*/
|
|
1682
|
-
|
|
1683
|
-
if (this.
|
|
1684
|
-
|
|
1685
|
-
this.
|
|
2004
|
+
cleanup() {
|
|
2005
|
+
if (this.localServer) {
|
|
2006
|
+
this.localServer.close();
|
|
2007
|
+
this.localServer = null;
|
|
1686
2008
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
*/
|
|
1691
|
-
getConnector() {
|
|
1692
|
-
if (!this.activeConnector) {
|
|
1693
|
-
throw new Error("No active connector. Call connectWithDSN() or connectWithType() first.");
|
|
2009
|
+
if (this.sshClient) {
|
|
2010
|
+
this.sshClient.end();
|
|
2011
|
+
this.sshClient = null;
|
|
1694
2012
|
}
|
|
1695
|
-
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Check if there's an active connection
|
|
1699
|
-
*/
|
|
1700
|
-
isConnected() {
|
|
1701
|
-
return this.connected;
|
|
2013
|
+
this.tunnelInfo = null;
|
|
1702
2014
|
}
|
|
1703
2015
|
/**
|
|
1704
|
-
* Get
|
|
2016
|
+
* Get current tunnel information
|
|
1705
2017
|
*/
|
|
1706
|
-
|
|
1707
|
-
return
|
|
2018
|
+
getTunnelInfo() {
|
|
2019
|
+
return this.tunnelInfo;
|
|
1708
2020
|
}
|
|
1709
2021
|
/**
|
|
1710
|
-
*
|
|
2022
|
+
* Check if tunnel is connected
|
|
1711
2023
|
*/
|
|
1712
|
-
|
|
1713
|
-
return
|
|
1714
|
-
}
|
|
1715
|
-
/**
|
|
1716
|
-
* Get the current active connector instance
|
|
1717
|
-
* This is used by resource and tool handlers
|
|
1718
|
-
*/
|
|
1719
|
-
static getCurrentConnector() {
|
|
1720
|
-
if (!managerInstance) {
|
|
1721
|
-
throw new Error("ConnectorManager not initialized");
|
|
1722
|
-
}
|
|
1723
|
-
return managerInstance.getConnector();
|
|
2024
|
+
getIsConnected() {
|
|
2025
|
+
return this.isConnected;
|
|
1724
2026
|
}
|
|
1725
2027
|
};
|
|
1726
2028
|
|
|
@@ -1773,6 +2075,10 @@ function loadEnvFiles() {
|
|
|
1773
2075
|
}
|
|
1774
2076
|
return null;
|
|
1775
2077
|
}
|
|
2078
|
+
function isDemoMode() {
|
|
2079
|
+
const args = parseCommandLineArgs();
|
|
2080
|
+
return args.demo === "true";
|
|
2081
|
+
}
|
|
1776
2082
|
function isReadOnlyMode() {
|
|
1777
2083
|
const args = parseCommandLineArgs();
|
|
1778
2084
|
if (args.readonly !== void 0) {
|
|
@@ -1785,6 +2091,13 @@ function isReadOnlyMode() {
|
|
|
1785
2091
|
}
|
|
1786
2092
|
function resolveDSN() {
|
|
1787
2093
|
const args = parseCommandLineArgs();
|
|
2094
|
+
if (isDemoMode()) {
|
|
2095
|
+
return {
|
|
2096
|
+
dsn: "sqlite:///:memory:",
|
|
2097
|
+
source: "demo mode",
|
|
2098
|
+
isDemo: true
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
1788
2101
|
if (args.dsn) {
|
|
1789
2102
|
return { dsn: args.dsn, source: "command line argument" };
|
|
1790
2103
|
}
|
|
@@ -1832,6 +2145,235 @@ function redactDSN(dsn) {
|
|
|
1832
2145
|
return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
|
|
1833
2146
|
}
|
|
1834
2147
|
}
|
|
2148
|
+
function resolveSSHConfig() {
|
|
2149
|
+
const args = parseCommandLineArgs();
|
|
2150
|
+
const hasSSHArgs = args["ssh-host"] || process.env.SSH_HOST;
|
|
2151
|
+
if (!hasSSHArgs) {
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
const config = {};
|
|
2155
|
+
let sources = [];
|
|
2156
|
+
if (args["ssh-host"]) {
|
|
2157
|
+
config.host = args["ssh-host"];
|
|
2158
|
+
sources.push("ssh-host from command line");
|
|
2159
|
+
} else if (process.env.SSH_HOST) {
|
|
2160
|
+
config.host = process.env.SSH_HOST;
|
|
2161
|
+
sources.push("SSH_HOST from environment");
|
|
2162
|
+
}
|
|
2163
|
+
if (args["ssh-port"]) {
|
|
2164
|
+
config.port = parseInt(args["ssh-port"], 10);
|
|
2165
|
+
sources.push("ssh-port from command line");
|
|
2166
|
+
} else if (process.env.SSH_PORT) {
|
|
2167
|
+
config.port = parseInt(process.env.SSH_PORT, 10);
|
|
2168
|
+
sources.push("SSH_PORT from environment");
|
|
2169
|
+
}
|
|
2170
|
+
if (args["ssh-user"]) {
|
|
2171
|
+
config.username = args["ssh-user"];
|
|
2172
|
+
sources.push("ssh-user from command line");
|
|
2173
|
+
} else if (process.env.SSH_USER) {
|
|
2174
|
+
config.username = process.env.SSH_USER;
|
|
2175
|
+
sources.push("SSH_USER from environment");
|
|
2176
|
+
}
|
|
2177
|
+
if (args["ssh-password"]) {
|
|
2178
|
+
config.password = args["ssh-password"];
|
|
2179
|
+
sources.push("ssh-password from command line");
|
|
2180
|
+
} else if (process.env.SSH_PASSWORD) {
|
|
2181
|
+
config.password = process.env.SSH_PASSWORD;
|
|
2182
|
+
sources.push("SSH_PASSWORD from environment");
|
|
2183
|
+
}
|
|
2184
|
+
if (args["ssh-key"]) {
|
|
2185
|
+
config.privateKey = args["ssh-key"];
|
|
2186
|
+
if (config.privateKey.startsWith("~/")) {
|
|
2187
|
+
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
2188
|
+
}
|
|
2189
|
+
sources.push("ssh-key from command line");
|
|
2190
|
+
} else if (process.env.SSH_KEY) {
|
|
2191
|
+
config.privateKey = process.env.SSH_KEY;
|
|
2192
|
+
if (config.privateKey.startsWith("~/")) {
|
|
2193
|
+
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
2194
|
+
}
|
|
2195
|
+
sources.push("SSH_KEY from environment");
|
|
2196
|
+
}
|
|
2197
|
+
if (args["ssh-passphrase"]) {
|
|
2198
|
+
config.passphrase = args["ssh-passphrase"];
|
|
2199
|
+
sources.push("ssh-passphrase from command line");
|
|
2200
|
+
} else if (process.env.SSH_PASSPHRASE) {
|
|
2201
|
+
config.passphrase = process.env.SSH_PASSPHRASE;
|
|
2202
|
+
sources.push("SSH_PASSPHRASE from environment");
|
|
2203
|
+
}
|
|
2204
|
+
if (!config.host || !config.username) {
|
|
2205
|
+
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
2206
|
+
}
|
|
2207
|
+
if (!config.password && !config.privateKey) {
|
|
2208
|
+
throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key for authentication");
|
|
2209
|
+
}
|
|
2210
|
+
return {
|
|
2211
|
+
config,
|
|
2212
|
+
source: sources.join(", ")
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/connectors/manager.ts
|
|
2217
|
+
var managerInstance = null;
|
|
2218
|
+
var ConnectorManager = class {
|
|
2219
|
+
constructor() {
|
|
2220
|
+
this.activeConnector = null;
|
|
2221
|
+
this.connected = false;
|
|
2222
|
+
this.sshTunnel = null;
|
|
2223
|
+
this.originalDSN = null;
|
|
2224
|
+
if (!managerInstance) {
|
|
2225
|
+
managerInstance = this;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Initialize and connect to the database using a DSN
|
|
2230
|
+
*/
|
|
2231
|
+
async connectWithDSN(dsn, initScript) {
|
|
2232
|
+
this.originalDSN = dsn;
|
|
2233
|
+
const sshConfig = resolveSSHConfig();
|
|
2234
|
+
let actualDSN = dsn;
|
|
2235
|
+
if (sshConfig) {
|
|
2236
|
+
console.error(`SSH tunnel configuration loaded from ${sshConfig.source}`);
|
|
2237
|
+
const url = new URL(dsn);
|
|
2238
|
+
const targetHost = url.hostname;
|
|
2239
|
+
const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
|
|
2240
|
+
this.sshTunnel = new SSHTunnel();
|
|
2241
|
+
const tunnelInfo = await this.sshTunnel.establish(sshConfig.config, {
|
|
2242
|
+
targetHost,
|
|
2243
|
+
targetPort
|
|
2244
|
+
});
|
|
2245
|
+
url.hostname = "127.0.0.1";
|
|
2246
|
+
url.port = tunnelInfo.localPort.toString();
|
|
2247
|
+
actualDSN = url.toString();
|
|
2248
|
+
console.error(`Database connection will use SSH tunnel through localhost:${tunnelInfo.localPort}`);
|
|
2249
|
+
}
|
|
2250
|
+
let connector = ConnectorRegistry.getConnectorForDSN(actualDSN);
|
|
2251
|
+
if (!connector) {
|
|
2252
|
+
throw new Error(`No connector found that can handle the DSN: ${actualDSN}`);
|
|
2253
|
+
}
|
|
2254
|
+
this.activeConnector = connector;
|
|
2255
|
+
await this.activeConnector.connect(actualDSN, initScript);
|
|
2256
|
+
this.connected = true;
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Initialize and connect to the database using a specific connector type
|
|
2260
|
+
*/
|
|
2261
|
+
async connectWithType(connectorType, dsn) {
|
|
2262
|
+
const connector = ConnectorRegistry.getConnector(connectorType);
|
|
2263
|
+
if (!connector) {
|
|
2264
|
+
throw new Error(`Connector "${connectorType}" not found`);
|
|
2265
|
+
}
|
|
2266
|
+
this.activeConnector = connector;
|
|
2267
|
+
const connectionString = dsn || connector.dsnParser.getSampleDSN();
|
|
2268
|
+
await this.activeConnector.connect(connectionString);
|
|
2269
|
+
this.connected = true;
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Close the database connection
|
|
2273
|
+
*/
|
|
2274
|
+
async disconnect() {
|
|
2275
|
+
if (this.activeConnector && this.connected) {
|
|
2276
|
+
await this.activeConnector.disconnect();
|
|
2277
|
+
this.connected = false;
|
|
2278
|
+
}
|
|
2279
|
+
if (this.sshTunnel) {
|
|
2280
|
+
await this.sshTunnel.close();
|
|
2281
|
+
this.sshTunnel = null;
|
|
2282
|
+
}
|
|
2283
|
+
this.originalDSN = null;
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Get the active connector
|
|
2287
|
+
*/
|
|
2288
|
+
getConnector() {
|
|
2289
|
+
if (!this.activeConnector) {
|
|
2290
|
+
throw new Error("No active connector. Call connectWithDSN() or connectWithType() first.");
|
|
2291
|
+
}
|
|
2292
|
+
return this.activeConnector;
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Check if there's an active connection
|
|
2296
|
+
*/
|
|
2297
|
+
isConnected() {
|
|
2298
|
+
return this.connected;
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Get all available connector types
|
|
2302
|
+
*/
|
|
2303
|
+
static getAvailableConnectors() {
|
|
2304
|
+
return ConnectorRegistry.getAvailableConnectors();
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Get sample DSNs for all available connectors
|
|
2308
|
+
*/
|
|
2309
|
+
static getAllSampleDSNs() {
|
|
2310
|
+
return ConnectorRegistry.getAllSampleDSNs();
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Get the current active connector instance
|
|
2314
|
+
* This is used by resource and tool handlers
|
|
2315
|
+
*/
|
|
2316
|
+
static getCurrentConnector() {
|
|
2317
|
+
if (!managerInstance) {
|
|
2318
|
+
throw new Error("ConnectorManager not initialized");
|
|
2319
|
+
}
|
|
2320
|
+
return managerInstance.getConnector();
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Get default port for a database based on DSN protocol
|
|
2324
|
+
*/
|
|
2325
|
+
getDefaultPort(dsn) {
|
|
2326
|
+
if (dsn.startsWith("postgres://") || dsn.startsWith("postgresql://")) {
|
|
2327
|
+
return 5432;
|
|
2328
|
+
} else if (dsn.startsWith("mysql://")) {
|
|
2329
|
+
return 3306;
|
|
2330
|
+
} else if (dsn.startsWith("mariadb://")) {
|
|
2331
|
+
return 3306;
|
|
2332
|
+
} else if (dsn.startsWith("sqlserver://")) {
|
|
2333
|
+
return 1433;
|
|
2334
|
+
}
|
|
2335
|
+
return 0;
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
// src/config/demo-loader.ts
|
|
2340
|
+
import fs2 from "fs";
|
|
2341
|
+
import path2 from "path";
|
|
2342
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2343
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2344
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
2345
|
+
var DEMO_DATA_DIR;
|
|
2346
|
+
var projectRootPath = path2.join(__dirname2, "..", "..", "..");
|
|
2347
|
+
var projectResourcesPath = path2.join(projectRootPath, "resources", "employee-sqlite");
|
|
2348
|
+
var distPath = path2.join(__dirname2, "resources", "employee-sqlite");
|
|
2349
|
+
if (fs2.existsSync(projectResourcesPath)) {
|
|
2350
|
+
DEMO_DATA_DIR = projectResourcesPath;
|
|
2351
|
+
} else if (fs2.existsSync(distPath)) {
|
|
2352
|
+
DEMO_DATA_DIR = distPath;
|
|
2353
|
+
} else {
|
|
2354
|
+
DEMO_DATA_DIR = path2.join(process.cwd(), "resources", "employee-sqlite");
|
|
2355
|
+
if (!fs2.existsSync(DEMO_DATA_DIR)) {
|
|
2356
|
+
throw new Error(`Could not find employee-sqlite resources in any of the expected locations:
|
|
2357
|
+
- ${projectResourcesPath}
|
|
2358
|
+
- ${distPath}
|
|
2359
|
+
- ${DEMO_DATA_DIR}`);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function loadSqlFile(fileName) {
|
|
2363
|
+
const filePath = path2.join(DEMO_DATA_DIR, fileName);
|
|
2364
|
+
return fs2.readFileSync(filePath, "utf8");
|
|
2365
|
+
}
|
|
2366
|
+
function getSqliteInMemorySetupSql() {
|
|
2367
|
+
let sql2 = loadSqlFile("employee.sql");
|
|
2368
|
+
const readRegex = /\.read\s+([a-zA-Z0-9_]+\.sql)/g;
|
|
2369
|
+
let match;
|
|
2370
|
+
while ((match = readRegex.exec(sql2)) !== null) {
|
|
2371
|
+
const includePath = match[1];
|
|
2372
|
+
const includeContent = loadSqlFile(includePath);
|
|
2373
|
+
sql2 = sql2.replace(match[0], includeContent);
|
|
2374
|
+
}
|
|
2375
|
+
return sql2;
|
|
2376
|
+
}
|
|
1835
2377
|
|
|
1836
2378
|
// src/resources/index.ts
|
|
1837
2379
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -2185,6 +2727,7 @@ var allowedKeywords = {
|
|
|
2185
2727
|
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2186
2728
|
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2187
2729
|
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2730
|
+
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2188
2731
|
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2189
2732
|
};
|
|
2190
2733
|
|
|
@@ -2252,6 +2795,9 @@ async function sqlGeneratorPromptHandler({
|
|
|
2252
2795
|
case "postgres":
|
|
2253
2796
|
sqlDialect = "postgres";
|
|
2254
2797
|
break;
|
|
2798
|
+
case "sqlite":
|
|
2799
|
+
sqlDialect = "sqlite";
|
|
2800
|
+
break;
|
|
2255
2801
|
case "mysql":
|
|
2256
2802
|
sqlDialect = "mysql";
|
|
2257
2803
|
break;
|
|
@@ -2312,6 +2858,11 @@ ${accessibleSchemas.map(
|
|
|
2312
2858
|
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
2313
2859
|
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
2314
2860
|
],
|
|
2861
|
+
sqlite: [
|
|
2862
|
+
"SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
|
|
2863
|
+
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
2864
|
+
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
2865
|
+
],
|
|
2315
2866
|
mysql: [
|
|
2316
2867
|
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
|
|
2317
2868
|
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
@@ -2643,10 +3194,10 @@ function registerPrompts(server) {
|
|
|
2643
3194
|
}
|
|
2644
3195
|
|
|
2645
3196
|
// src/server.ts
|
|
2646
|
-
var
|
|
2647
|
-
var
|
|
2648
|
-
var packageJsonPath =
|
|
2649
|
-
var packageJson = JSON.parse(
|
|
3197
|
+
var __filename3 = fileURLToPath3(import.meta.url);
|
|
3198
|
+
var __dirname3 = path3.dirname(__filename3);
|
|
3199
|
+
var packageJsonPath = path3.join(__dirname3, "..", "package.json");
|
|
3200
|
+
var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
2650
3201
|
var SERVER_NAME = "DBHub MCP Server";
|
|
2651
3202
|
var SERVER_VERSION = packageJson.version;
|
|
2652
3203
|
function generateBanner(version, modes = []) {
|
|
@@ -2672,7 +3223,8 @@ async function main() {
|
|
|
2672
3223
|
ERROR: Database connection string (DSN) is required.
|
|
2673
3224
|
Please provide the DSN in one of these ways (in order of priority):
|
|
2674
3225
|
|
|
2675
|
-
1.
|
|
3226
|
+
1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
|
|
3227
|
+
2. Command line argument: --dsn="your-connection-string"
|
|
2676
3228
|
3. Environment variable: export DSN="your-connection-string"
|
|
2677
3229
|
4. .env file: DSN=your-connection-string
|
|
2678
3230
|
|
|
@@ -2683,7 +3235,7 @@ See documentation for more details on configuring database connections.
|
|
|
2683
3235
|
`);
|
|
2684
3236
|
process.exit(1);
|
|
2685
3237
|
}
|
|
2686
|
-
const
|
|
3238
|
+
const createServer2 = () => {
|
|
2687
3239
|
const server = new McpServer2({
|
|
2688
3240
|
name: SERVER_NAME,
|
|
2689
3241
|
version: SERVER_VERSION
|
|
@@ -2696,13 +3248,22 @@ See documentation for more details on configuring database connections.
|
|
|
2696
3248
|
const connectorManager = new ConnectorManager();
|
|
2697
3249
|
console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
|
|
2698
3250
|
console.error(`DSN source: ${dsnData.source}`);
|
|
2699
|
-
|
|
3251
|
+
if (dsnData.isDemo) {
|
|
3252
|
+
const initScript = getSqliteInMemorySetupSql();
|
|
3253
|
+
await connectorManager.connectWithDSN(dsnData.dsn, initScript);
|
|
3254
|
+
} else {
|
|
3255
|
+
await connectorManager.connectWithDSN(dsnData.dsn);
|
|
3256
|
+
}
|
|
2700
3257
|
const transportData = resolveTransport();
|
|
2701
3258
|
console.error(`Using transport: ${transportData.type}`);
|
|
2702
3259
|
console.error(`Transport source: ${transportData.source}`);
|
|
2703
3260
|
const readonly = isReadOnlyMode();
|
|
2704
3261
|
const activeModes = [];
|
|
2705
3262
|
const modeDescriptions = [];
|
|
3263
|
+
if (dsnData.isDemo) {
|
|
3264
|
+
activeModes.push("DEMO");
|
|
3265
|
+
modeDescriptions.push("using sample employee database");
|
|
3266
|
+
}
|
|
2706
3267
|
if (readonly) {
|
|
2707
3268
|
activeModes.push("READ-ONLY");
|
|
2708
3269
|
modeDescriptions.push("only read only queries allowed");
|
|
@@ -2728,6 +3289,9 @@ See documentation for more details on configuring database connections.
|
|
|
2728
3289
|
}
|
|
2729
3290
|
next();
|
|
2730
3291
|
});
|
|
3292
|
+
app.get("/healthz", (req, res) => {
|
|
3293
|
+
res.status(200).send("OK");
|
|
3294
|
+
});
|
|
2731
3295
|
app.post("/message", async (req, res) => {
|
|
2732
3296
|
try {
|
|
2733
3297
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -2736,7 +3300,7 @@ See documentation for more details on configuring database connections.
|
|
|
2736
3300
|
enableJsonResponse: false
|
|
2737
3301
|
// Use SSE streaming
|
|
2738
3302
|
});
|
|
2739
|
-
const server =
|
|
3303
|
+
const server = createServer2();
|
|
2740
3304
|
await server.connect(transport);
|
|
2741
3305
|
await transport.handleRequest(req, res, req.body);
|
|
2742
3306
|
} catch (error) {
|
|
@@ -2754,7 +3318,7 @@ See documentation for more details on configuring database connections.
|
|
|
2754
3318
|
console.error(`Connect to MCP server at http://0.0.0.0:${port}/message`);
|
|
2755
3319
|
});
|
|
2756
3320
|
} else {
|
|
2757
|
-
const server =
|
|
3321
|
+
const server = createServer2();
|
|
2758
3322
|
const transport = new StdioServerTransport();
|
|
2759
3323
|
console.error("Starting with STDIO transport");
|
|
2760
3324
|
await server.connect(transport);
|