@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.
- package/README.md +18 -0
- package/dist/index.js +125 -5
- 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
|
|
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
|
-
|
|
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
|
|
2743
|
-
|
|
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(
|
|
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.
|
|
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
|
},
|