@bytebase/dbhub 0.11.0 → 0.11.2

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 +18 -0
  2. package/dist/index.js +125 -5
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -217,6 +217,24 @@ postgres://user:password@localhost:5432/dbname
217
217
 
218
218
  DBHub supports connecting to databases through SSH tunnels, enabling secure access to databases in private networks or behind firewalls.
219
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:
223
+
224
+ ```bash
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
234
+ ```
235
+
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.).
237
+
220
238
  #### SSH with Password Authentication
221
239
 
222
240
  ```bash
package/dist/index.js CHANGED
@@ -1881,7 +1881,7 @@ 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 as readFileSync2 } from "fs";
1884
+ import { readFileSync as readFileSync3 } from "fs";
1885
1885
  import { fileURLToPath as fileURLToPath3 } from "url";
1886
1886
 
1887
1887
  // src/utils/ssh-tunnel.ts
@@ -2031,6 +2031,103 @@ import dotenv from "dotenv";
2031
2031
  import path from "path";
2032
2032
  import fs from "fs";
2033
2033
  import { fileURLToPath } from "url";
2034
+ import { homedir as homedir2 } from "os";
2035
+
2036
+ // src/utils/ssh-config-parser.ts
2037
+ import { readFileSync as readFileSync2, existsSync } from "fs";
2038
+ import { homedir } from "os";
2039
+ import { join } from "path";
2040
+ import SSHConfig from "ssh-config";
2041
+ var DEFAULT_SSH_KEYS = [
2042
+ "~/.ssh/id_rsa",
2043
+ "~/.ssh/id_ed25519",
2044
+ "~/.ssh/id_ecdsa",
2045
+ "~/.ssh/id_dsa"
2046
+ ];
2047
+ function expandTilde(filePath) {
2048
+ if (filePath.startsWith("~/")) {
2049
+ return join(homedir(), filePath.substring(2));
2050
+ }
2051
+ return filePath;
2052
+ }
2053
+ function fileExists(filePath) {
2054
+ try {
2055
+ return existsSync(expandTilde(filePath));
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ }
2060
+ function findDefaultSSHKey() {
2061
+ for (const keyPath of DEFAULT_SSH_KEYS) {
2062
+ if (fileExists(keyPath)) {
2063
+ return expandTilde(keyPath);
2064
+ }
2065
+ }
2066
+ return void 0;
2067
+ }
2068
+ function parseSSHConfig(hostAlias, configPath) {
2069
+ const sshConfigPath = configPath;
2070
+ if (!existsSync(sshConfigPath)) {
2071
+ return null;
2072
+ }
2073
+ try {
2074
+ const configContent = readFileSync2(sshConfigPath, "utf8");
2075
+ const config = SSHConfig.parse(configContent);
2076
+ const hostConfig = config.compute(hostAlias);
2077
+ if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
2078
+ return null;
2079
+ }
2080
+ const sshConfig = {};
2081
+ if (hostConfig.HostName) {
2082
+ sshConfig.host = hostConfig.HostName;
2083
+ } else {
2084
+ sshConfig.host = hostAlias;
2085
+ }
2086
+ if (hostConfig.Port) {
2087
+ sshConfig.port = parseInt(hostConfig.Port, 10);
2088
+ }
2089
+ if (hostConfig.User) {
2090
+ sshConfig.username = hostConfig.User;
2091
+ }
2092
+ if (hostConfig.IdentityFile) {
2093
+ const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
2094
+ const expandedPath = expandTilde(identityFile);
2095
+ if (fileExists(expandedPath)) {
2096
+ sshConfig.privateKey = expandedPath;
2097
+ }
2098
+ }
2099
+ if (!sshConfig.privateKey) {
2100
+ const defaultKey = findDefaultSSHKey();
2101
+ if (defaultKey) {
2102
+ sshConfig.privateKey = defaultKey;
2103
+ }
2104
+ }
2105
+ if (hostConfig.ProxyJump || hostConfig.ProxyCommand) {
2106
+ console.error("Warning: ProxyJump/ProxyCommand in SSH config is not yet supported by DBHub");
2107
+ }
2108
+ if (!sshConfig.host || !sshConfig.username) {
2109
+ return null;
2110
+ }
2111
+ return sshConfig;
2112
+ } catch (error) {
2113
+ console.error(`Error parsing SSH config: ${error instanceof Error ? error.message : String(error)}`);
2114
+ return null;
2115
+ }
2116
+ }
2117
+ function looksLikeSSHAlias(host) {
2118
+ if (host.includes(".")) {
2119
+ return false;
2120
+ }
2121
+ if (/^[\d:]+$/.test(host)) {
2122
+ return false;
2123
+ }
2124
+ if (/^[0-9a-fA-F:]+$/.test(host) && host.includes(":")) {
2125
+ return false;
2126
+ }
2127
+ return true;
2128
+ }
2129
+
2130
+ // src/config/env.ts
2034
2131
  var __filename = fileURLToPath(import.meta.url);
2035
2132
  var __dirname = path.dirname(__filename);
2036
2133
  function parseCommandLineArgs() {
@@ -2151,15 +2248,27 @@ function resolveSSHConfig() {
2151
2248
  if (!hasSSHArgs) {
2152
2249
  return null;
2153
2250
  }
2154
- const config = {};
2251
+ let config = {};
2155
2252
  let sources = [];
2253
+ let sshConfigHost;
2156
2254
  if (args["ssh-host"]) {
2255
+ sshConfigHost = args["ssh-host"];
2157
2256
  config.host = args["ssh-host"];
2158
2257
  sources.push("ssh-host from command line");
2159
2258
  } else if (process.env.SSH_HOST) {
2259
+ sshConfigHost = process.env.SSH_HOST;
2160
2260
  config.host = process.env.SSH_HOST;
2161
2261
  sources.push("SSH_HOST from environment");
2162
2262
  }
2263
+ if (sshConfigHost && looksLikeSSHAlias(sshConfigHost)) {
2264
+ const sshConfigPath = path.join(homedir2(), ".ssh", "config");
2265
+ console.error(`Attempting to parse SSH config for host '${sshConfigHost}' from: ${sshConfigPath}`);
2266
+ const sshConfigData = parseSSHConfig(sshConfigHost, sshConfigPath);
2267
+ if (sshConfigData) {
2268
+ config = { ...sshConfigData };
2269
+ sources.push(`SSH config for host '${sshConfigHost}'`);
2270
+ }
2271
+ }
2163
2272
  if (args["ssh-port"]) {
2164
2273
  config.port = parseInt(args["ssh-port"], 10);
2165
2274
  sources.push("ssh-port from command line");
@@ -2738,9 +2847,20 @@ var executeSqlSchema = {
2738
2847
  function splitSQLStatements(sql2) {
2739
2848
  return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
2740
2849
  }
2850
+ function stripSQLComments(sql2) {
2851
+ let cleaned = sql2.split("\n").map((line) => {
2852
+ const commentIndex = line.indexOf("--");
2853
+ return commentIndex >= 0 ? line.substring(0, commentIndex) : line;
2854
+ }).join("\n");
2855
+ cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, " ");
2856
+ return cleaned.trim();
2857
+ }
2741
2858
  function isReadOnlySQL(sql2, connectorType) {
2742
- const normalizedSQL = sql2.trim().toLowerCase();
2743
- const firstWord = normalizedSQL.split(/\s+/)[0];
2859
+ const cleanedSQL = stripSQLComments(sql2).toLowerCase();
2860
+ if (!cleanedSQL) {
2861
+ return true;
2862
+ }
2863
+ const firstWord = cleanedSQL.split(/\s+/)[0];
2744
2864
  const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
2745
2865
  return keywordList.includes(firstWord);
2746
2866
  }
@@ -3197,7 +3317,7 @@ function registerPrompts(server) {
3197
3317
  var __filename3 = fileURLToPath3(import.meta.url);
3198
3318
  var __dirname3 = path3.dirname(__filename3);
3199
3319
  var packageJsonPath = path3.join(__dirname3, "..", "package.json");
3200
- var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
3320
+ var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
3201
3321
  var SERVER_NAME = "DBHub MCP Server";
3202
3322
  var SERVER_VERSION = packageJson.version;
3203
3323
  function generateBanner(version, modes = []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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
+ "ssh-config": "^5.0.3",
28
29
  "ssh2": "^1.16.0",
29
30
  "zod": "^3.24.2"
30
31
  },