@dbx-app/mcp-server 0.2.2 → 0.3.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.
@@ -10,6 +10,12 @@ export interface ConnectionConfig {
10
10
  database?: string;
11
11
  url_params?: string;
12
12
  ssh_enabled: boolean;
13
+ proxy_enabled?: boolean;
14
+ proxy_type?: "socks5" | "http";
15
+ proxy_host?: string;
16
+ proxy_port?: number;
17
+ proxy_username?: string;
18
+ proxy_password?: string;
13
19
  ssl: boolean;
14
20
  }
15
21
  export declare function loadConnections(): Promise<ConnectionConfig[]>;
@@ -31,6 +31,8 @@ export async function loadConnections() {
31
31
  config.id = row.id;
32
32
  if (!config.password)
33
33
  config.password = getSecret(db, row.id, "password");
34
+ if (!config.proxy_password)
35
+ config.proxy_password = getSecret(db, row.id, "proxy_password");
34
36
  configs.push(config);
35
37
  }
36
38
  db.close();
@@ -68,6 +70,12 @@ export async function addConnection(config) {
68
70
  ssh_key_path: "",
69
71
  ssh_key_passphrase: "",
70
72
  ssh_expose_lan: false,
73
+ proxy_enabled: config.proxy_enabled ?? false,
74
+ proxy_type: config.proxy_type ?? "socks5",
75
+ proxy_host: config.proxy_host ?? "",
76
+ proxy_port: config.proxy_port ?? 1080,
77
+ proxy_username: config.proxy_username ?? "",
78
+ proxy_password: "",
71
79
  ssl: config.ssl ?? false,
72
80
  sysdba: false,
73
81
  connection_string: null,
@@ -78,6 +86,9 @@ export async function addConnection(config) {
78
86
  if (config.password) {
79
87
  db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "password", config.password);
80
88
  }
89
+ if (config.proxy_password) {
90
+ db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "proxy_password", config.proxy_password);
91
+ }
81
92
  });
82
93
  insert();
83
94
  db.close();
package/dist/database.js CHANGED
@@ -1,7 +1,12 @@
1
+ import { createServer, connect as netConnect } from "node:net";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { homedir, platform } from "node:os";
1
5
  const MAX_ROWS = 100;
2
6
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
3
7
  const QUERY_TIMEOUT_MS = 30_000;
4
8
  const pools = new Map();
9
+ const proxyTunnels = new Map();
5
10
  function poolKey(config) {
6
11
  return `${config.id}:${config.database || ""}`;
7
12
  }
@@ -26,8 +31,9 @@ async function getPgPool(config) {
26
31
  return existing.pool;
27
32
  }
28
33
  const pg = await import("pg");
34
+ const endpoint = await connectionEndpoint(config);
29
35
  const pool = new pg.default.Pool({
30
- connectionString: buildConnectionUrl(config),
36
+ connectionString: buildConnectionUrl(config, endpoint),
31
37
  max: 3,
32
38
  idleTimeoutMillis: 30_000,
33
39
  connectionTimeoutMillis: 10_000,
@@ -46,8 +52,9 @@ async function getMysqlPool(config) {
46
52
  return existing.pool;
47
53
  }
48
54
  const mysql = await import("mysql2/promise");
55
+ const endpoint = await connectionEndpoint(config);
49
56
  const pool = mysql.default.createPool({
50
- uri: buildConnectionUrl(config),
57
+ uri: buildConnectionUrl(config, endpoint),
51
58
  connectionLimit: 3,
52
59
  idleTimeout: 30_000,
53
60
  connectTimeout: 10_000,
@@ -57,18 +64,194 @@ async function getMysqlPool(config) {
57
64
  resetIdleTimer(key, entry);
58
65
  return pool;
59
66
  }
60
- function buildConnectionUrl(config) {
67
+ async function connectionEndpoint(config) {
68
+ if (!config.proxy_enabled || !config.proxy_host)
69
+ return { host: config.host, port: config.port };
70
+ const existing = proxyTunnels.get(config.id);
71
+ if (existing)
72
+ return { host: "127.0.0.1", port: existing.port };
73
+ const server = createServer((inbound) => {
74
+ connectViaProxy(config)
75
+ .then((outbound) => {
76
+ inbound.pipe(outbound);
77
+ outbound.pipe(inbound);
78
+ })
79
+ .catch(() => inbound.destroy());
80
+ });
81
+ const port = await new Promise((resolve, reject) => {
82
+ server.once("error", reject);
83
+ server.listen(0, "127.0.0.1", () => {
84
+ const address = server.address();
85
+ if (address && typeof address === "object")
86
+ resolve(address.port);
87
+ else
88
+ reject(new Error("Failed to bind proxy tunnel"));
89
+ });
90
+ });
91
+ proxyTunnels.set(config.id, { server, port });
92
+ return { host: "127.0.0.1", port };
93
+ }
94
+ function buildConnectionUrl(config, endpoint) {
61
95
  const db = config.database || "";
62
96
  const params = config.url_params || "";
63
97
  const suffix = params ? `?${params}` : "";
64
98
  if (isMysqlType(config.db_type)) {
65
- return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
99
+ return `mysql://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
100
+ }
101
+ return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
102
+ }
103
+ function connectViaProxy(config) {
104
+ return new Promise((resolve, reject) => {
105
+ const socket = netConnect(config.proxy_port || 1080, config.proxy_host || "127.0.0.1");
106
+ socket.once("error", reject);
107
+ socket.once("connect", () => {
108
+ if ((config.proxy_type || "socks5") === "http") {
109
+ httpConnect(socket, config, resolve, reject);
110
+ }
111
+ else {
112
+ socks5Connect(socket, config, resolve, reject);
113
+ }
114
+ });
115
+ });
116
+ }
117
+ function httpConnect(socket, config, resolve, reject) {
118
+ const target = `${config.host}:${config.port}`;
119
+ const lines = [`CONNECT ${target} HTTP/1.1`, `Host: ${target}`];
120
+ if (config.proxy_username || config.proxy_password) {
121
+ const token = Buffer.from(`${config.proxy_username || ""}:${config.proxy_password || ""}`).toString("base64");
122
+ lines.push(`Proxy-Authorization: Basic ${token}`);
66
123
  }
67
- return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${config.host}:${config.port}/${db}${suffix}`;
124
+ socket.write(`${lines.join("\r\n")}\r\n\r\n`);
125
+ let buffer = Buffer.alloc(0);
126
+ socket.on("data", function onData(chunk) {
127
+ buffer = Buffer.concat([buffer, chunk]);
128
+ const end = buffer.indexOf("\r\n\r\n");
129
+ if (end < 0)
130
+ return;
131
+ socket.off("data", onData);
132
+ const head = buffer.subarray(0, end).toString("utf8");
133
+ if (!/^HTTP\/1\.[01] 200\b/.test(head)) {
134
+ reject(new Error(`HTTP proxy CONNECT failed: ${head.split("\r\n")[0] || "invalid response"}`));
135
+ socket.destroy();
136
+ return;
137
+ }
138
+ const rest = buffer.subarray(end + 4);
139
+ if (rest.length)
140
+ socket.unshift(rest);
141
+ resolve(socket);
142
+ });
143
+ }
144
+ function socks5Connect(socket, config, resolve, reject) {
145
+ const wantsAuth = !!(config.proxy_username || config.proxy_password);
146
+ socket.write(Buffer.from(wantsAuth ? [0x05, 0x02, 0x00, 0x02] : [0x05, 0x01, 0x00]));
147
+ socket.once("data", (method) => {
148
+ if (method.length < 2 || method[0] !== 0x05) {
149
+ reject(new Error("Invalid SOCKS greeting"));
150
+ socket.destroy();
151
+ return;
152
+ }
153
+ if (method[1] === 0x02) {
154
+ const user = Buffer.from(config.proxy_username || "");
155
+ const pass = Buffer.from(config.proxy_password || "");
156
+ socket.write(Buffer.concat([Buffer.from([0x01, user.length]), user, Buffer.from([pass.length]), pass]));
157
+ socket.once("data", (auth) => {
158
+ if (auth.length < 2 || auth[1] !== 0x00) {
159
+ reject(new Error("SOCKS proxy authentication failed"));
160
+ socket.destroy();
161
+ return;
162
+ }
163
+ sendSocksConnect(socket, config, resolve, reject);
164
+ });
165
+ }
166
+ else if (method[1] === 0x00) {
167
+ sendSocksConnect(socket, config, resolve, reject);
168
+ }
169
+ else {
170
+ reject(new Error("SOCKS proxy rejected authentication methods"));
171
+ socket.destroy();
172
+ }
173
+ });
174
+ }
175
+ function sendSocksConnect(socket, config, resolve, reject) {
176
+ const host = Buffer.from(config.host);
177
+ socket.write(Buffer.concat([Buffer.from([0x05, 0x01, 0x00, 0x03, host.length]), host, portBytes(config.port)]));
178
+ socket.once("data", (res) => {
179
+ if (res.length < 4 || res[0] !== 0x05 || res[1] !== 0x00) {
180
+ reject(new Error(`SOCKS proxy connect failed with code ${res[1] ?? "unknown"}`));
181
+ socket.destroy();
182
+ return;
183
+ }
184
+ resolve(socket);
185
+ });
186
+ }
187
+ function portBytes(port) {
188
+ const buf = Buffer.alloc(2);
189
+ buf.writeUInt16BE(port);
190
+ return buf;
68
191
  }
69
192
  function isMysqlType(dbType) {
70
193
  return dbType === "mysql" || dbType === "doris" || dbType === "starrocks";
71
194
  }
195
+ function isDirectType(dbType) {
196
+ switch (dbType) {
197
+ case "postgres":
198
+ case "redshift":
199
+ case "mysql":
200
+ case "doris":
201
+ case "starrocks":
202
+ return true;
203
+ default:
204
+ return false;
205
+ }
206
+ }
207
+ function bridgeAppDataDir() {
208
+ const home = homedir();
209
+ switch (platform()) {
210
+ case "darwin":
211
+ return join(home, "Library", "Application Support", "com.dbx.app");
212
+ case "win32":
213
+ return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "com.dbx.app");
214
+ default:
215
+ return join(home, ".config", "com.dbx.app");
216
+ }
217
+ }
218
+ async function bridgeDataRequest(path, body) {
219
+ let bridgeUrl;
220
+ try {
221
+ const portFile = join(bridgeAppDataDir(), "mcp-bridge-port");
222
+ const port = (await readFile(portFile, "utf-8")).trim();
223
+ bridgeUrl = `http://127.0.0.1:${port}`;
224
+ }
225
+ catch {
226
+ throw new Error("DBX desktop app is not running. This database type requires DBX to be running for query execution.");
227
+ }
228
+ const res = await fetch(`${bridgeUrl}${path}`, {
229
+ method: "POST",
230
+ headers: { "Content-Type": "application/json" },
231
+ body: JSON.stringify(body),
232
+ });
233
+ if (!res.ok) {
234
+ const errBody = await res.text().catch(() => "");
235
+ let errorMsg;
236
+ try {
237
+ const parsed = JSON.parse(errBody);
238
+ errorMsg = parsed.error || errBody;
239
+ }
240
+ catch {
241
+ errorMsg = errBody;
242
+ }
243
+ throw new Error(errorMsg || `Bridge request failed: ${res.status}`);
244
+ }
245
+ return res.json();
246
+ }
247
+ function convertBridgeQueryResult(result) {
248
+ const rows = result.rows.slice(0, MAX_ROWS).map((row) => {
249
+ const obj = {};
250
+ result.columns.forEach((col, i) => { obj[col] = row[i]; });
251
+ return obj;
252
+ });
253
+ return { columns: result.columns, rows, row_count: rows.length };
254
+ }
72
255
  function withTimeout(promise, ms) {
73
256
  return new Promise((resolve, reject) => {
74
257
  const timer = setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms);
@@ -114,9 +297,25 @@ async function query(config, sql, params) {
114
297
  return pgQuery(config, sql, params);
115
298
  }
116
299
  export async function executeQuery(config, sql) {
117
- return query(config, sql);
300
+ if (isDirectType(config.db_type)) {
301
+ return query(config, sql);
302
+ }
303
+ const result = await bridgeDataRequest("/data/execute-query", {
304
+ connection_name: config.name,
305
+ database: config.database || "",
306
+ sql,
307
+ });
308
+ return convertBridgeQueryResult(result);
118
309
  }
119
310
  export async function listTables(config, schema) {
311
+ if (!isDirectType(config.db_type)) {
312
+ const tables = await bridgeDataRequest("/data/list-tables", {
313
+ connection_name: config.name,
314
+ database: config.database || "",
315
+ schema: schema || "",
316
+ });
317
+ return tables.map((t) => ({ name: t.name, type: t.table_type || "TABLE" }));
318
+ }
120
319
  let result;
121
320
  if (isMysqlType(config.db_type)) {
122
321
  result = await query(config, `SELECT TABLE_NAME AS name, TABLE_TYPE AS type FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME`);
@@ -127,6 +326,22 @@ export async function listTables(config, schema) {
127
326
  return result.rows.map((r) => ({ name: String(r.name || r.NAME), type: String(r.type || r.TYPE || "TABLE") }));
128
327
  }
129
328
  export async function describeTable(config, table, schema) {
329
+ if (!isDirectType(config.db_type)) {
330
+ const columns = await bridgeDataRequest("/data/describe-table", {
331
+ connection_name: config.name,
332
+ database: config.database || "",
333
+ schema: schema || "",
334
+ table,
335
+ });
336
+ return columns.map((c) => ({
337
+ name: c.name,
338
+ data_type: c.data_type,
339
+ is_nullable: c.is_nullable,
340
+ column_default: c.column_default,
341
+ is_primary_key: c.is_primary_key,
342
+ comment: c.comment,
343
+ }));
344
+ }
130
345
  let result;
131
346
  if (isMysqlType(config.db_type)) {
132
347
  result = await query(config, `SELECT c.COLUMN_NAME AS name, c.DATA_TYPE AS data_type, c.IS_NULLABLE = 'YES' AS is_nullable, c.COLUMN_DEFAULT AS column_default, c.COLUMN_KEY = 'PRI' AS is_primary_key, c.COLUMN_COMMENT AS comment FROM information_schema.COLUMNS c WHERE c.TABLE_SCHEMA = DATABASE() AND c.TABLE_NAME = ? ORDER BY c.ORDINAL_POSITION`, [table]);
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ function mdTable(headers, rows) {
36
36
  }
37
37
  const server = new McpServer({
38
38
  name: "dbx",
39
- version: "0.2.1",
39
+ version: "0.3.0",
40
40
  });
41
41
  server.tool("dbx_list_connections", "List all database connections configured in DBX", {}, async () => {
42
42
  const connections = await backend.loadConnections();
@@ -99,7 +99,7 @@ server.tool("dbx_execute_query", "Execute a SQL query on a database connection (
99
99
  });
100
100
  server.tool("dbx_add_connection", "Add a new database connection to DBX", {
101
101
  name: z.string().describe("Connection name"),
102
- db_type: z.string().describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch"),
102
+ db_type: z.string().describe("Database type: postgres, mysql, sqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, doris, starrocks, redshift, dameng, kingbase, vastbase, goldendb, gaussdb, h2, snowflake, trino, hive, db2, informix, neo4j, cassandra, bigquery, kylin, sundb, jdbc"),
103
103
  host: z.string().describe("Database host"),
104
104
  port: z.number().describe("Database port"),
105
105
  username: z.string().default("").describe("Username"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbx-app/mcp-server",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.t8y2/dbx",
5
5
  "description": "MCP server for DBX \u2014 query databases from Claude Code, Cursor, and other AI agents",
6
6
  "type": "module",