@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/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 path2 from "path";
1641
- import { readFileSync } from "fs";
1642
- import { fileURLToPath as fileURLToPath2 } from "url";
1883
+ import path3 from "path";
1884
+ import { readFileSync as readFileSync2 } from "fs";
1885
+ import { fileURLToPath as fileURLToPath3 } from "url";
1643
1886
 
1644
- // src/connectors/manager.ts
1645
- var managerInstance = null;
1646
- var ConnectorManager = class {
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.activeConnector = null;
1649
- this.connected = false;
1650
- if (!managerInstance) {
1651
- managerInstance = this;
1652
- }
1893
+ this.sshClient = null;
1894
+ this.localServer = null;
1895
+ this.tunnelInfo = null;
1896
+ this.isConnected = false;
1653
1897
  }
1654
1898
  /**
1655
- * Initialize and connect to the database using a DSN
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 connectWithDSN(dsn, initScript) {
1658
- let connector = ConnectorRegistry.getConnectorForDSN(dsn);
1659
- if (!connector) {
1660
- throw new Error(`No connector found that can handle the DSN: ${dsn}`);
1661
- }
1662
- this.activeConnector = connector;
1663
- await this.activeConnector.connect(dsn, initScript);
1664
- this.connected = true;
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
- * Initialize and connect to the database using a specific connector type
1988
+ * Close the SSH tunnel and clean up resources
1668
1989
  */
1669
- async connectWithType(connectorType, dsn) {
1670
- const connector = ConnectorRegistry.getConnector(connectorType);
1671
- if (!connector) {
1672
- throw new Error(`Connector "${connectorType}" not found`);
1673
- }
1674
- this.activeConnector = connector;
1675
- const connectionString = dsn || connector.dsnParser.getSampleDSN();
1676
- await this.activeConnector.connect(connectionString);
1677
- this.connected = true;
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
- * Close the database connection
2002
+ * Clean up resources
1681
2003
  */
1682
- async disconnect() {
1683
- if (this.activeConnector && this.connected) {
1684
- await this.activeConnector.disconnect();
1685
- this.connected = false;
2004
+ cleanup() {
2005
+ if (this.localServer) {
2006
+ this.localServer.close();
2007
+ this.localServer = null;
1686
2008
  }
1687
- }
1688
- /**
1689
- * Get the active connector
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
- return this.activeConnector;
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 all available connector types
2016
+ * Get current tunnel information
1705
2017
  */
1706
- static getAvailableConnectors() {
1707
- return ConnectorRegistry.getAvailableConnectors();
2018
+ getTunnelInfo() {
2019
+ return this.tunnelInfo;
1708
2020
  }
1709
2021
  /**
1710
- * Get sample DSNs for all available connectors
2022
+ * Check if tunnel is connected
1711
2023
  */
1712
- static getAllSampleDSNs() {
1713
- return ConnectorRegistry.getAllSampleDSNs();
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 __filename2 = fileURLToPath2(import.meta.url);
2647
- var __dirname2 = path2.dirname(__filename2);
2648
- var packageJsonPath = path2.join(__dirname2, "..", "package.json");
2649
- var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
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. Command line argument: --dsn="your-connection-string"
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 createServer = () => {
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
- await connectorManager.connectWithDSN(dsnData.dsn);
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 = createServer();
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 = createServer();
3321
+ const server = createServer2();
2758
3322
  const transport = new StdioServerTransport();
2759
3323
  console.error("Starting with STDIO transport");
2760
3324
  await server.connect(transport);