@bytebase/dbhub 0.10.0 → 0.11.1

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 +110 -34
  2. package/dist/index.js +425 -67
  3. package/package.json +4 -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,70 @@ 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
+ #### Using SSH Config File (Recommended)
221
+
222
+ DBHub can read SSH connection settings from your `~/.ssh/config` file. Simply use the host alias from your SSH config:
199
223
 
200
224
  ```bash
201
- # Enable read-only mode
202
- npx @bytebase/dbhub --readonly --dsn "postgres://user:password@localhost:5432/dbname"
225
+ # If you have this in ~/.ssh/config:
226
+ # Host mybastion
227
+ # HostName bastion.example.com
228
+ # User ubuntu
229
+ # IdentityFile ~/.ssh/id_rsa
230
+
231
+ npx @bytebase/dbhub \
232
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
233
+ --ssh-host mybastion
203
234
  ```
204
235
 
205
- In read-only mode, only [readonly SQL operations](https://github.com/bytebase/dbhub/blob/main/src/utils/allowed-keywords.ts) are allowed.
236
+ DBHub will automatically use the settings from your SSH config, including hostname, user, port, and identity file. If no identity file is specified in the config, DBHub will try common default locations (`~/.ssh/id_rsa`, `~/.ssh/id_ed25519`, etc.).
206
237
 
207
- This provides an additional layer of security when connecting to production databases.
238
+ #### SSH with Password Authentication
239
+
240
+ ```bash
241
+ npx @bytebase/dbhub \
242
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
243
+ --ssh-host bastion.example.com \
244
+ --ssh-user ubuntu \
245
+ --ssh-password mypassword
246
+ ```
247
+
248
+ #### SSH with Private Key Authentication
249
+
250
+ ```bash
251
+ npx @bytebase/dbhub \
252
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
253
+ --ssh-host bastion.example.com \
254
+ --ssh-user ubuntu \
255
+ --ssh-key ~/.ssh/id_rsa
256
+ ```
257
+
258
+ #### SSH with Private Key and Passphrase
259
+
260
+ ```bash
261
+ npx @bytebase/dbhub \
262
+ --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb" \
263
+ --ssh-host bastion.example.com \
264
+ --ssh-port 2222 \
265
+ --ssh-user ubuntu \
266
+ --ssh-key ~/.ssh/id_rsa \
267
+ --ssh-passphrase mykeypassphrase
268
+ ```
269
+
270
+ #### Using Environment Variables
271
+
272
+ ```bash
273
+ export SSH_HOST=bastion.example.com
274
+ export SSH_USER=ubuntu
275
+ export SSH_KEY=~/.ssh/id_rsa
276
+ npx @bytebase/dbhub --dsn "postgres://dbuser:dbpass@database.internal:5432/mydb"
277
+ ```
278
+
279
+ **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
280
 
209
281
  ### Configure your database connection
210
282
 
@@ -244,14 +316,13 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
244
316
 
245
317
  DBHub supports the following database connection string formats:
246
318
 
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
-
319
+ | Database | DSN Format | Example |
320
+ | ---------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
321
+ | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname?sslmode=disable` |
322
+ | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
323
+ | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
324
+ | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
325
+ | 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
326
 
256
327
  #### SQL Server
257
328
 
@@ -276,13 +347,19 @@ Extra query parameters:
276
347
 
277
348
  ### Command line options
278
349
 
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` |
350
+ | Option | Environment Variable | Description | Default |
351
+ | -------------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
352
+ | dsn | `DSN` | Database connection string | Required if not in demo mode |
353
+ | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
354
+ | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
355
+ | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
356
+ | demo | N/A | Run in demo mode with sample employee database | `false` |
357
+ | ssh-host | `SSH_HOST` | SSH server hostname for tunnel connection | N/A |
358
+ | ssh-port | `SSH_PORT` | SSH server port | `22` |
359
+ | ssh-user | `SSH_USER` | SSH username | N/A |
360
+ | ssh-password | `SSH_PASSWORD` | SSH password (for password authentication) | N/A |
361
+ | ssh-key | `SSH_KEY` | Path to SSH private key file | N/A |
362
+ | ssh-passphrase | `SSH_PASSPHRASE` | Passphrase for SSH private key | N/A |
286
363
 
287
364
  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
365
 
@@ -379,7 +456,6 @@ docker pull mcr.microsoft.com/mssql/server:2019-latest
379
456
  - Ensure Docker has sufficient memory allocated (4GB+ recommended)
380
457
  - Consider running SQL Server tests separately if experiencing timeouts
381
458
 
382
-
383
459
  **Network/Resource Issues:**
384
460
 
385
461
  ```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 readFileSync3 } 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
1942
- */
1943
- isConnected() {
1944
- return this.connected;
1945
- }
1946
- /**
1947
- * Get all available connector types
2016
+ * Get current tunnel information
1948
2017
  */
1949
- static getAvailableConnectors() {
1950
- return ConnectorRegistry.getAvailableConnectors();
2018
+ getTunnelInfo() {
2019
+ return this.tunnelInfo;
1951
2020
  }
1952
2021
  /**
1953
- * Get sample DSNs for all available connectors
2022
+ * Check if tunnel is connected
1954
2023
  */
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
 
@@ -1972,6 +2031,102 @@ import dotenv from "dotenv";
1972
2031
  import path from "path";
1973
2032
  import fs from "fs";
1974
2033
  import { fileURLToPath } from "url";
2034
+
2035
+ // src/utils/ssh-config-parser.ts
2036
+ import { readFileSync as readFileSync2, existsSync } from "fs";
2037
+ import { homedir } from "os";
2038
+ import { join } from "path";
2039
+ import SSHConfig from "ssh-config";
2040
+ var DEFAULT_SSH_KEYS = [
2041
+ "~/.ssh/id_rsa",
2042
+ "~/.ssh/id_ed25519",
2043
+ "~/.ssh/id_ecdsa",
2044
+ "~/.ssh/id_dsa"
2045
+ ];
2046
+ function expandTilde(filePath) {
2047
+ if (filePath.startsWith("~/")) {
2048
+ return join(homedir(), filePath.substring(2));
2049
+ }
2050
+ return filePath;
2051
+ }
2052
+ function fileExists(filePath) {
2053
+ try {
2054
+ return existsSync(expandTilde(filePath));
2055
+ } catch {
2056
+ return false;
2057
+ }
2058
+ }
2059
+ function findDefaultSSHKey() {
2060
+ for (const keyPath of DEFAULT_SSH_KEYS) {
2061
+ if (fileExists(keyPath)) {
2062
+ return expandTilde(keyPath);
2063
+ }
2064
+ }
2065
+ return void 0;
2066
+ }
2067
+ function parseSSHConfig(hostAlias, configPath) {
2068
+ const sshConfigPath = configPath || join(homedir(), ".ssh", "config");
2069
+ if (!existsSync(sshConfigPath)) {
2070
+ return null;
2071
+ }
2072
+ try {
2073
+ const configContent = readFileSync2(sshConfigPath, "utf8");
2074
+ const config = SSHConfig.parse(configContent);
2075
+ const hostConfig = config.compute(hostAlias);
2076
+ if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
2077
+ return null;
2078
+ }
2079
+ const sshConfig = {};
2080
+ if (hostConfig.HostName) {
2081
+ sshConfig.host = hostConfig.HostName;
2082
+ } else {
2083
+ sshConfig.host = hostAlias;
2084
+ }
2085
+ if (hostConfig.Port) {
2086
+ sshConfig.port = parseInt(hostConfig.Port, 10);
2087
+ }
2088
+ if (hostConfig.User) {
2089
+ sshConfig.username = hostConfig.User;
2090
+ }
2091
+ if (hostConfig.IdentityFile) {
2092
+ const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
2093
+ const expandedPath = expandTilde(identityFile);
2094
+ if (fileExists(expandedPath)) {
2095
+ sshConfig.privateKey = expandedPath;
2096
+ }
2097
+ }
2098
+ if (!sshConfig.privateKey) {
2099
+ const defaultKey = findDefaultSSHKey();
2100
+ if (defaultKey) {
2101
+ sshConfig.privateKey = defaultKey;
2102
+ }
2103
+ }
2104
+ if (hostConfig.ProxyJump || hostConfig.ProxyCommand) {
2105
+ console.error("Warning: ProxyJump/ProxyCommand in SSH config is not yet supported by DBHub");
2106
+ }
2107
+ if (!sshConfig.host || !sshConfig.username) {
2108
+ return null;
2109
+ }
2110
+ return sshConfig;
2111
+ } catch (error) {
2112
+ console.error(`Error parsing SSH config: ${error instanceof Error ? error.message : String(error)}`);
2113
+ return null;
2114
+ }
2115
+ }
2116
+ function looksLikeSSHAlias(host) {
2117
+ if (host.includes(".")) {
2118
+ return false;
2119
+ }
2120
+ if (/^[\d:]+$/.test(host)) {
2121
+ return false;
2122
+ }
2123
+ if (/^[0-9a-fA-F:]+$/.test(host) && host.includes(":")) {
2124
+ return false;
2125
+ }
2126
+ return true;
2127
+ }
2128
+
2129
+ // src/config/env.ts
1975
2130
  var __filename = fileURLToPath(import.meta.url);
1976
2131
  var __dirname = path.dirname(__filename);
1977
2132
  function parseCommandLineArgs() {
@@ -2086,6 +2241,206 @@ function redactDSN(dsn) {
2086
2241
  return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
2087
2242
  }
2088
2243
  }
2244
+ function resolveSSHConfig() {
2245
+ const args = parseCommandLineArgs();
2246
+ const hasSSHArgs = args["ssh-host"] || process.env.SSH_HOST;
2247
+ if (!hasSSHArgs) {
2248
+ return null;
2249
+ }
2250
+ let config = {};
2251
+ let sources = [];
2252
+ let sshConfigHost;
2253
+ if (args["ssh-host"]) {
2254
+ sshConfigHost = args["ssh-host"];
2255
+ config.host = args["ssh-host"];
2256
+ sources.push("ssh-host from command line");
2257
+ } else if (process.env.SSH_HOST) {
2258
+ sshConfigHost = process.env.SSH_HOST;
2259
+ config.host = process.env.SSH_HOST;
2260
+ sources.push("SSH_HOST from environment");
2261
+ }
2262
+ if (sshConfigHost && looksLikeSSHAlias(sshConfigHost)) {
2263
+ const sshConfigData = parseSSHConfig(sshConfigHost);
2264
+ if (sshConfigData) {
2265
+ config = { ...sshConfigData };
2266
+ sources.push(`SSH config for host '${sshConfigHost}'`);
2267
+ }
2268
+ }
2269
+ if (args["ssh-port"]) {
2270
+ config.port = parseInt(args["ssh-port"], 10);
2271
+ sources.push("ssh-port from command line");
2272
+ } else if (process.env.SSH_PORT) {
2273
+ config.port = parseInt(process.env.SSH_PORT, 10);
2274
+ sources.push("SSH_PORT from environment");
2275
+ }
2276
+ if (args["ssh-user"]) {
2277
+ config.username = args["ssh-user"];
2278
+ sources.push("ssh-user from command line");
2279
+ } else if (process.env.SSH_USER) {
2280
+ config.username = process.env.SSH_USER;
2281
+ sources.push("SSH_USER from environment");
2282
+ }
2283
+ if (args["ssh-password"]) {
2284
+ config.password = args["ssh-password"];
2285
+ sources.push("ssh-password from command line");
2286
+ } else if (process.env.SSH_PASSWORD) {
2287
+ config.password = process.env.SSH_PASSWORD;
2288
+ sources.push("SSH_PASSWORD from environment");
2289
+ }
2290
+ if (args["ssh-key"]) {
2291
+ config.privateKey = args["ssh-key"];
2292
+ if (config.privateKey.startsWith("~/")) {
2293
+ config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
2294
+ }
2295
+ sources.push("ssh-key from command line");
2296
+ } else if (process.env.SSH_KEY) {
2297
+ config.privateKey = process.env.SSH_KEY;
2298
+ if (config.privateKey.startsWith("~/")) {
2299
+ config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
2300
+ }
2301
+ sources.push("SSH_KEY from environment");
2302
+ }
2303
+ if (args["ssh-passphrase"]) {
2304
+ config.passphrase = args["ssh-passphrase"];
2305
+ sources.push("ssh-passphrase from command line");
2306
+ } else if (process.env.SSH_PASSPHRASE) {
2307
+ config.passphrase = process.env.SSH_PASSPHRASE;
2308
+ sources.push("SSH_PASSPHRASE from environment");
2309
+ }
2310
+ if (!config.host || !config.username) {
2311
+ throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
2312
+ }
2313
+ if (!config.password && !config.privateKey) {
2314
+ throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key for authentication");
2315
+ }
2316
+ return {
2317
+ config,
2318
+ source: sources.join(", ")
2319
+ };
2320
+ }
2321
+
2322
+ // src/connectors/manager.ts
2323
+ var managerInstance = null;
2324
+ var ConnectorManager = class {
2325
+ constructor() {
2326
+ this.activeConnector = null;
2327
+ this.connected = false;
2328
+ this.sshTunnel = null;
2329
+ this.originalDSN = null;
2330
+ if (!managerInstance) {
2331
+ managerInstance = this;
2332
+ }
2333
+ }
2334
+ /**
2335
+ * Initialize and connect to the database using a DSN
2336
+ */
2337
+ async connectWithDSN(dsn, initScript) {
2338
+ this.originalDSN = dsn;
2339
+ const sshConfig = resolveSSHConfig();
2340
+ let actualDSN = dsn;
2341
+ if (sshConfig) {
2342
+ console.error(`SSH tunnel configuration loaded from ${sshConfig.source}`);
2343
+ const url = new URL(dsn);
2344
+ const targetHost = url.hostname;
2345
+ const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
2346
+ this.sshTunnel = new SSHTunnel();
2347
+ const tunnelInfo = await this.sshTunnel.establish(sshConfig.config, {
2348
+ targetHost,
2349
+ targetPort
2350
+ });
2351
+ url.hostname = "127.0.0.1";
2352
+ url.port = tunnelInfo.localPort.toString();
2353
+ actualDSN = url.toString();
2354
+ console.error(`Database connection will use SSH tunnel through localhost:${tunnelInfo.localPort}`);
2355
+ }
2356
+ let connector = ConnectorRegistry.getConnectorForDSN(actualDSN);
2357
+ if (!connector) {
2358
+ throw new Error(`No connector found that can handle the DSN: ${actualDSN}`);
2359
+ }
2360
+ this.activeConnector = connector;
2361
+ await this.activeConnector.connect(actualDSN, initScript);
2362
+ this.connected = true;
2363
+ }
2364
+ /**
2365
+ * Initialize and connect to the database using a specific connector type
2366
+ */
2367
+ async connectWithType(connectorType, dsn) {
2368
+ const connector = ConnectorRegistry.getConnector(connectorType);
2369
+ if (!connector) {
2370
+ throw new Error(`Connector "${connectorType}" not found`);
2371
+ }
2372
+ this.activeConnector = connector;
2373
+ const connectionString = dsn || connector.dsnParser.getSampleDSN();
2374
+ await this.activeConnector.connect(connectionString);
2375
+ this.connected = true;
2376
+ }
2377
+ /**
2378
+ * Close the database connection
2379
+ */
2380
+ async disconnect() {
2381
+ if (this.activeConnector && this.connected) {
2382
+ await this.activeConnector.disconnect();
2383
+ this.connected = false;
2384
+ }
2385
+ if (this.sshTunnel) {
2386
+ await this.sshTunnel.close();
2387
+ this.sshTunnel = null;
2388
+ }
2389
+ this.originalDSN = null;
2390
+ }
2391
+ /**
2392
+ * Get the active connector
2393
+ */
2394
+ getConnector() {
2395
+ if (!this.activeConnector) {
2396
+ throw new Error("No active connector. Call connectWithDSN() or connectWithType() first.");
2397
+ }
2398
+ return this.activeConnector;
2399
+ }
2400
+ /**
2401
+ * Check if there's an active connection
2402
+ */
2403
+ isConnected() {
2404
+ return this.connected;
2405
+ }
2406
+ /**
2407
+ * Get all available connector types
2408
+ */
2409
+ static getAvailableConnectors() {
2410
+ return ConnectorRegistry.getAvailableConnectors();
2411
+ }
2412
+ /**
2413
+ * Get sample DSNs for all available connectors
2414
+ */
2415
+ static getAllSampleDSNs() {
2416
+ return ConnectorRegistry.getAllSampleDSNs();
2417
+ }
2418
+ /**
2419
+ * Get the current active connector instance
2420
+ * This is used by resource and tool handlers
2421
+ */
2422
+ static getCurrentConnector() {
2423
+ if (!managerInstance) {
2424
+ throw new Error("ConnectorManager not initialized");
2425
+ }
2426
+ return managerInstance.getConnector();
2427
+ }
2428
+ /**
2429
+ * Get default port for a database based on DSN protocol
2430
+ */
2431
+ getDefaultPort(dsn) {
2432
+ if (dsn.startsWith("postgres://") || dsn.startsWith("postgresql://")) {
2433
+ return 5432;
2434
+ } else if (dsn.startsWith("mysql://")) {
2435
+ return 3306;
2436
+ } else if (dsn.startsWith("mariadb://")) {
2437
+ return 3306;
2438
+ } else if (dsn.startsWith("sqlserver://")) {
2439
+ return 1433;
2440
+ }
2441
+ return 0;
2442
+ }
2443
+ };
2089
2444
 
2090
2445
  // src/config/demo-loader.ts
2091
2446
  import fs2 from "fs";
@@ -2948,7 +3303,7 @@ function registerPrompts(server) {
2948
3303
  var __filename3 = fileURLToPath3(import.meta.url);
2949
3304
  var __dirname3 = path3.dirname(__filename3);
2950
3305
  var packageJsonPath = path3.join(__dirname3, "..", "package.json");
2951
- var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
3306
+ var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
2952
3307
  var SERVER_NAME = "DBHub MCP Server";
2953
3308
  var SERVER_VERSION = packageJson.version;
2954
3309
  function generateBanner(version, modes = []) {
@@ -2986,7 +3341,7 @@ See documentation for more details on configuring database connections.
2986
3341
  `);
2987
3342
  process.exit(1);
2988
3343
  }
2989
- const createServer = () => {
3344
+ const createServer2 = () => {
2990
3345
  const server = new McpServer2({
2991
3346
  name: SERVER_NAME,
2992
3347
  version: SERVER_VERSION
@@ -3040,6 +3395,9 @@ See documentation for more details on configuring database connections.
3040
3395
  }
3041
3396
  next();
3042
3397
  });
3398
+ app.get("/healthz", (req, res) => {
3399
+ res.status(200).send("OK");
3400
+ });
3043
3401
  app.post("/message", async (req, res) => {
3044
3402
  try {
3045
3403
  const transport = new StreamableHTTPServerTransport({
@@ -3048,7 +3406,7 @@ See documentation for more details on configuring database connections.
3048
3406
  enableJsonResponse: false
3049
3407
  // Use SSE streaming
3050
3408
  });
3051
- const server = createServer();
3409
+ const server = createServer2();
3052
3410
  await server.connect(transport);
3053
3411
  await transport.handleRequest(req, res, req.body);
3054
3412
  } catch (error) {
@@ -3066,7 +3424,7 @@ See documentation for more details on configuring database connections.
3066
3424
  console.error(`Connect to MCP server at http://0.0.0.0:${port}/message`);
3067
3425
  });
3068
3426
  } else {
3069
- const server = createServer();
3427
+ const server = createServer2();
3070
3428
  const transport = new StdioServerTransport();
3071
3429
  console.error("Starting with STDIO transport");
3072
3430
  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.1",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -25,6 +25,8 @@
25
25
  "mssql": "^11.0.1",
26
26
  "mysql2": "^3.13.0",
27
27
  "pg": "^8.13.3",
28
+ "ssh-config": "^5.0.3",
29
+ "ssh2": "^1.16.0",
28
30
  "zod": "^3.24.2"
29
31
  },
30
32
  "devDependencies": {
@@ -37,6 +39,7 @@
37
39
  "@types/mssql": "^9.1.7",
38
40
  "@types/node": "^22.13.10",
39
41
  "@types/pg": "^8.11.11",
42
+ "@types/ssh2": "^1.15.5",
40
43
  "cross-env": "^7.0.3",
41
44
  "husky": "^9.0.11",
42
45
  "lint-staged": "^15.2.2",