@bytebase/dbhub 0.10.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.
Files changed (3) hide show
  1. package/README.md +92 -34
  2. package/dist/index.js +319 -67
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -22,14 +22,15 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
22
22
  | | | | | |
23
23
  | Claude Desktop +--->+ +--->+ PostgreSQL |
24
24
  | | | | | |
25
- | Cursor +--->+ DBHub +--->+ SQL Server |
25
+ | Claude Code +--->+ +--->+ SQL Server |
26
26
  | | | | | |
27
- | Other Clients +--->+ +--->+ SQLite |
27
+ | Cursor +--->+ DBHub +--->+ SQLite |
28
28
  | | | | | |
29
- | | | +--->+ MySQL |
29
+ | Other Clients +--->+ +--->+ MySQL |
30
30
  | | | | | |
31
31
  | | | +--->+ MariaDB |
32
32
  | | | | | |
33
+ | | | | | |
33
34
  +------------------+ +--------------+ +------------------+
34
35
  MCP Clients MCP Server Databases
35
36
  ```
@@ -82,7 +83,7 @@ docker run --rm --init \
82
83
  ```
83
84
 
84
85
  ```bash
85
- # Demo mode with sample employee database
86
+ # Demo mode with sqlite sample employee database
86
87
  docker run --rm --init \
87
88
  --name dbhub \
88
89
  --publish 8080:8080 \
@@ -92,12 +93,14 @@ docker run --rm --init \
92
93
  --demo
93
94
  ```
94
95
 
95
-
96
96
  ### NPM
97
97
 
98
98
  ```bash
99
99
  # PostgreSQL example
100
100
  npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
101
+
102
+ # Demo mode with sqlite sample employee database
103
+ npx @bytebase/dbhub --transport http --port 8080 --demo
101
104
  ```
102
105
 
103
106
  ```bash
@@ -150,6 +153,10 @@ npx @bytebase/dbhub --transport http --port 8080 --demo
150
153
  }
151
154
  ```
152
155
 
156
+ ### Claude Code
157
+
158
+ Check https://docs.anthropic.com/en/docs/claude-code/mcp
159
+
153
160
  ### Cursor
154
161
 
155
162
  [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19)
@@ -161,17 +168,30 @@ npx @bytebase/dbhub --transport http --port 8080 --demo
161
168
 
162
169
  ## Usage
163
170
 
171
+ ### Read-only Mode
172
+
173
+ You can run DBHub in read-only mode, which restricts SQL query execution to read-only operations:
174
+
175
+ ```bash
176
+ # Enable read-only mode
177
+ npx @bytebase/dbhub --readonly --dsn "postgres://user:password@localhost:5432/dbname"
178
+ ```
179
+
180
+ In read-only mode, only [readonly SQL operations](https://github.com/bytebase/dbhub/blob/main/src/utils/allowed-keywords.ts) are allowed.
181
+
182
+ This provides an additional layer of security when connecting to production databases.
183
+
164
184
  ### SSL Connections
165
185
 
166
186
  You can specify the SSL mode using the `sslmode` parameter in your DSN string:
167
187
 
168
- | Database | `sslmode=disable` | `sslmode=require` | Default SSL Behavior |
169
- | ---------- | :---------------: | :---------------: | :----------------------------: |
170
- | PostgreSQL | ✅ | ✅ | Certificate verification |
171
- | MySQL | ✅ | ✅ | Certificate verification |
172
- | MariaDB | ✅ | ✅ | Certificate verification |
173
- | SQL Server | ✅ | ✅ | Certificate verification |
174
- | SQLite | ❌ | ❌ | N/A (file-based) |
188
+ | Database | `sslmode=disable` | `sslmode=require` | Default SSL Behavior |
189
+ | ---------- | :---------------: | :---------------: | :----------------------: |
190
+ | PostgreSQL | ✅ | ✅ | Certificate verification |
191
+ | MySQL | ✅ | ✅ | Certificate verification |
192
+ | MariaDB | ✅ | ✅ | Certificate verification |
193
+ | SQL Server | ✅ | ✅ | Certificate verification |
194
+ | SQLite | ❌ | ❌ | N/A (file-based) |
175
195
 
176
196
  **SSL Mode Options:**
177
197
 
@@ -193,18 +213,52 @@ postgres://user:password@localhost:5432/dbname?sslmode=require
193
213
  postgres://user:password@localhost:5432/dbname
194
214
  ```
195
215
 
196
- ### Read-only Mode
216
+ ### SSH Tunnel Support
197
217
 
198
- You can run DBHub in read-only mode, which restricts SQL query execution to read-only operations:
218
+ DBHub supports connecting to databases through SSH tunnels, enabling secure access to databases in private networks or behind firewalls.
219
+
220
+ #### SSH with Password Authentication
199
221
 
200
222
  ```bash
201
- # Enable read-only mode
202
- npx @bytebase/dbhub --readonly --dsn "postgres://user:password@localhost:5432/dbname"
223
+ npx @bytebase/dbhub \
224
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
225
+ --ssh-host bastion.example.com \
226
+ --ssh-user ubuntu \
227
+ --ssh-password mypassword
203
228
  ```
204
229
 
205
- In read-only mode, only [readonly SQL operations](https://github.com/bytebase/dbhub/blob/main/src/utils/allowed-keywords.ts) are allowed.
230
+ #### SSH with Private Key Authentication
206
231
 
207
- This provides an additional layer of security when connecting to production databases.
232
+ ```bash
233
+ npx @bytebase/dbhub \
234
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
235
+ --ssh-host bastion.example.com \
236
+ --ssh-user ubuntu \
237
+ --ssh-key ~/.ssh/id_rsa
238
+ ```
239
+
240
+ #### SSH with Private Key and Passphrase
241
+
242
+ ```bash
243
+ npx @bytebase/dbhub \
244
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
245
+ --ssh-host bastion.example.com \
246
+ --ssh-port 2222 \
247
+ --ssh-user ubuntu \
248
+ --ssh-key ~/.ssh/id_rsa \
249
+ --ssh-passphrase mykeypassphrase
250
+ ```
251
+
252
+ #### Using Environment Variables
253
+
254
+ ```bash
255
+ export SSH_HOST=bastion.example.com
256
+ export SSH_USER=ubuntu
257
+ export SSH_KEY=~/.ssh/id_rsa
258
+ npx @bytebase/dbhub --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb"
259
+ ```
260
+
261
+ **Note**: When using SSH tunnels, the database host in your DSN should be the hostname/IP as seen from the SSH server (bastion host), not from your local machine.
208
262
 
209
263
  ### Configure your database connection
210
264
 
@@ -244,14 +298,13 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
244
298
 
245
299
  DBHub supports the following database connection string formats:
246
300
 
247
- | Database | DSN Format | Example |
248
- | ---------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
249
- | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname?sslmode=disable` |
250
- | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
251
- | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
252
- | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
253
- | SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite:///:memory:` |
254
-
301
+ | Database | DSN Format | Example |
302
+ | ---------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
303
+ | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname?sslmode=disable` |
304
+ | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
305
+ | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
306
+ | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
307
+ | SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite:///:memory:` |
255
308
 
256
309
  #### SQL Server
257
310
 
@@ -276,13 +329,19 @@ Extra query parameters:
276
329
 
277
330
  ### Command line options
278
331
 
279
- | Option | Environment Variable | Description | Default |
280
- | --------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
281
- | dsn | `DSN` | Database connection string | Required if not in demo mode |
282
- | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
283
- | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
284
- | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
285
- | demo | N/A | Run in demo mode with sample employee database | `false` |
332
+ | Option | Environment Variable | Description | Default |
333
+ | -------------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
334
+ | dsn | `DSN` | Database connection string | Required if not in demo mode |
335
+ | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
336
+ | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
337
+ | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
338
+ | demo | N/A | Run in demo mode with sample employee database | `false` |
339
+ | ssh-host | `SSH_HOST` | SSH server hostname for tunnel connection | N/A |
340
+ | ssh-port | `SSH_PORT` | SSH server port | `22` |
341
+ | ssh-user | `SSH_USER` | SSH username | N/A |
342
+ | ssh-password | `SSH_PASSWORD` | SSH password (for password authentication) | N/A |
343
+ | ssh-key | `SSH_KEY` | Path to SSH private key file | N/A |
344
+ | ssh-passphrase | `SSH_PASSPHRASE` | Passphrase for SSH private key | N/A |
286
345
 
287
346
  The demo mode uses an in-memory SQLite database loaded with the [sample employee database](https://github.com/bytebase/dbhub/tree/main/resources/employee-sqlite) that includes tables for employees, departments, titles, salaries, department employees, and department managers. The sample database includes SQL scripts for table creation, data loading, and testing.
288
347
 
@@ -379,7 +438,6 @@ docker pull mcr.microsoft.com/mssql/server:2019-latest
379
438
  - Ensure Docker has sufficient memory allocated (4GB+ recommended)
380
439
  - Consider running SQL Server tests separately if experiencing timeouts
381
440
 
382
-
383
441
  **Network/Resource Issues:**
384
442
 
385
443
  ```bash
package/dist/index.js CHANGED
@@ -1881,89 +1881,148 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
1881
1881
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1882
1882
  import express from "express";
1883
1883
  import path3 from "path";
1884
- import { readFileSync } from "fs";
1884
+ import { readFileSync as readFileSync2 } from "fs";
1885
1885
  import { fileURLToPath as fileURLToPath3 } from "url";
1886
1886
 
1887
- // src/connectors/manager.ts
1888
- var managerInstance = null;
1889
- 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 {
1890
1892
  constructor() {
1891
- this.activeConnector = null;
1892
- this.connected = false;
1893
- if (!managerInstance) {
1894
- managerInstance = this;
1895
- }
1893
+ this.sshClient = null;
1894
+ this.localServer = null;
1895
+ this.tunnelInfo = null;
1896
+ this.isConnected = false;
1896
1897
  }
1897
1898
  /**
1898
- * 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
1899
1903
  */
1900
- async connectWithDSN(dsn, initScript) {
1901
- let connector = ConnectorRegistry.getConnectorForDSN(dsn);
1902
- if (!connector) {
1903
- throw new Error(`No connector found that can handle the DSN: ${dsn}`);
1904
- }
1905
- this.activeConnector = connector;
1906
- await this.activeConnector.connect(dsn, initScript);
1907
- 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
+ });
1908
1986
  }
1909
1987
  /**
1910
- * Initialize and connect to the database using a specific connector type
1988
+ * Close the SSH tunnel and clean up resources
1911
1989
  */
1912
- async connectWithType(connectorType, dsn) {
1913
- const connector = ConnectorRegistry.getConnector(connectorType);
1914
- if (!connector) {
1915
- throw new Error(`Connector "${connectorType}" not found`);
1916
- }
1917
- this.activeConnector = connector;
1918
- const connectionString = dsn || connector.dsnParser.getSampleDSN();
1919
- await this.activeConnector.connect(connectionString);
1920
- 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
+ });
1921
2000
  }
1922
2001
  /**
1923
- * Close the database connection
2002
+ * Clean up resources
1924
2003
  */
1925
- async disconnect() {
1926
- if (this.activeConnector && this.connected) {
1927
- await this.activeConnector.disconnect();
1928
- this.connected = false;
2004
+ cleanup() {
2005
+ if (this.localServer) {
2006
+ this.localServer.close();
2007
+ this.localServer = null;
1929
2008
  }
1930
- }
1931
- /**
1932
- * Get the active connector
1933
- */
1934
- getConnector() {
1935
- if (!this.activeConnector) {
1936
- throw new Error("No active connector. Call connectWithDSN() or connectWithType() first.");
2009
+ if (this.sshClient) {
2010
+ this.sshClient.end();
2011
+ this.sshClient = null;
1937
2012
  }
1938
- return this.activeConnector;
2013
+ this.tunnelInfo = null;
1939
2014
  }
1940
2015
  /**
1941
- * Check if there's an active connection
2016
+ * Get current tunnel information
1942
2017
  */
1943
- isConnected() {
1944
- return this.connected;
2018
+ getTunnelInfo() {
2019
+ return this.tunnelInfo;
1945
2020
  }
1946
2021
  /**
1947
- * Get all available connector types
2022
+ * Check if tunnel is connected
1948
2023
  */
1949
- static getAvailableConnectors() {
1950
- return ConnectorRegistry.getAvailableConnectors();
1951
- }
1952
- /**
1953
- * Get sample DSNs for all available connectors
1954
- */
1955
- static getAllSampleDSNs() {
1956
- return ConnectorRegistry.getAllSampleDSNs();
1957
- }
1958
- /**
1959
- * Get the current active connector instance
1960
- * This is used by resource and tool handlers
1961
- */
1962
- static getCurrentConnector() {
1963
- if (!managerInstance) {
1964
- throw new Error("ConnectorManager not initialized");
1965
- }
1966
- return managerInstance.getConnector();
2024
+ getIsConnected() {
2025
+ return this.isConnected;
1967
2026
  }
1968
2027
  };
1969
2028
 
@@ -2086,6 +2145,196 @@ function redactDSN(dsn) {
2086
2145
  return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
2087
2146
  }
2088
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
+ };
2089
2338
 
2090
2339
  // src/config/demo-loader.ts
2091
2340
  import fs2 from "fs";
@@ -2948,7 +3197,7 @@ function registerPrompts(server) {
2948
3197
  var __filename3 = fileURLToPath3(import.meta.url);
2949
3198
  var __dirname3 = path3.dirname(__filename3);
2950
3199
  var packageJsonPath = path3.join(__dirname3, "..", "package.json");
2951
- var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
3200
+ var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
2952
3201
  var SERVER_NAME = "DBHub MCP Server";
2953
3202
  var SERVER_VERSION = packageJson.version;
2954
3203
  function generateBanner(version, modes = []) {
@@ -2986,7 +3235,7 @@ See documentation for more details on configuring database connections.
2986
3235
  `);
2987
3236
  process.exit(1);
2988
3237
  }
2989
- const createServer = () => {
3238
+ const createServer2 = () => {
2990
3239
  const server = new McpServer2({
2991
3240
  name: SERVER_NAME,
2992
3241
  version: SERVER_VERSION
@@ -3040,6 +3289,9 @@ See documentation for more details on configuring database connections.
3040
3289
  }
3041
3290
  next();
3042
3291
  });
3292
+ app.get("/healthz", (req, res) => {
3293
+ res.status(200).send("OK");
3294
+ });
3043
3295
  app.post("/message", async (req, res) => {
3044
3296
  try {
3045
3297
  const transport = new StreamableHTTPServerTransport({
@@ -3048,7 +3300,7 @@ See documentation for more details on configuring database connections.
3048
3300
  enableJsonResponse: false
3049
3301
  // Use SSE streaming
3050
3302
  });
3051
- const server = createServer();
3303
+ const server = createServer2();
3052
3304
  await server.connect(transport);
3053
3305
  await transport.handleRequest(req, res, req.body);
3054
3306
  } catch (error) {
@@ -3066,7 +3318,7 @@ See documentation for more details on configuring database connections.
3066
3318
  console.error(`Connect to MCP server at http://0.0.0.0:${port}/message`);
3067
3319
  });
3068
3320
  } else {
3069
- const server = createServer();
3321
+ const server = createServer2();
3070
3322
  const transport = new StdioServerTransport();
3071
3323
  console.error("Starting with STDIO transport");
3072
3324
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -25,6 +25,7 @@
25
25
  "mssql": "^11.0.1",
26
26
  "mysql2": "^3.13.0",
27
27
  "pg": "^8.13.3",
28
+ "ssh2": "^1.16.0",
28
29
  "zod": "^3.24.2"
29
30
  },
30
31
  "devDependencies": {
@@ -37,6 +38,7 @@
37
38
  "@types/mssql": "^9.1.7",
38
39
  "@types/node": "^22.13.10",
39
40
  "@types/pg": "^8.11.11",
41
+ "@types/ssh2": "^1.15.5",
40
42
  "cross-env": "^7.0.3",
41
43
  "husky": "^9.0.11",
42
44
  "lint-staged": "^15.2.2",